Menjalankan migrasi database otomatis saat inisialisasi aplikasi sering kali menjadi bumerang pada arsitektur microservices modern. Ketika aplikasi Spring Boot di-deploy ke Kubernetes dengan konfigurasi multi-replica, eksekusi Flyway pada fase application startup rentan memicu perebutan lock skema. Jika skrip migrasi memakan waktu lebih lama daripada batas toleransi Kubernetes probe, deployment dapat terhenti total akibat kegagalan readiness probe dan CrashLoopBackOff.
Anatomi Masalah: Perebutan Schema Lock saat Rolling Update
Secara default, Spring Boot mengeksekusi Flyway.migrate() secara sinkron di dalam main thread sebelum embedded web server (Tomcat/Jetty) mulai menerima trafik. Mekanisme kerja Flyway menjamin integritas skema dengan cara mengambil lock eksklusif di database sebelum menjalankan file SQL migrasi:
- PostgreSQL: Menggunakan advisory lock tingkat koneksi/transaksi (misal via fungsi internal Flyway yang mengeksekusi
pg_advisory_lock). - MySQL/MariaDB/Oracle: Menggunakan mekanisme table-level lock eksplisit atau baris locking pada tabel metadata
flyway_schema_history.
Saat proses rolling update dimulai, Kubernetes membuat Pod baru secara simultan. Pod pertama berhasil memperoleh lock dan mulai mengeksekusi DDL. Namun, Pod kedua dan seterusnya akan tertahan (blocking) pada fase pengambilan lock database. Karena main thread Spring Boot terhambat, ApplicationContext belum selesai terbentuk, sehingga endpoint /actuator/health/readiness tidak merespons.
Kubelet mendeteksi bahwa Pod tidak siap melewati ambang batas initialDelaySeconds dan failureThreshold. Akibatnya, kubelet mematikan kontainer secara paksa (SIGKILL). Kematian kontainer yang sedang memegang lock database berisiko meninggalkan dangling/stale lock, sementara kontainer baru terus masuk ke status CrashLoopBackOff.
Observability: Identifikasi Masalah via Log dan Actuator
Deteksi dini insiden ini memerlukan korelasi antara log inisialisasi kontainer, metrik runtime thread, dan inspeksi lock database engine.
1. Analisis Pola Log Startup Pod
Ketika Pod tertahan menunggu lock atau gagal karena timeout, log aplikasi menampilkan baris penanda akuisisi lock dari paket org.flywaydb:
2026-03-30T10:15:20.104Z INFO 1 --- [main] o.f.c.i.database.base.BaseDatabaseType : Database: jdbc:postgresql://postgres-db:5432/app_db
2026-03-30T10:15:20.310Z INFO 1 --- [main] o.f.core.internal.command.DbMigrate : Current version of schema "public": 1.4
2026-03-30T10:15:20.350Z WARN 1 --- [main] o.f.c.i.database.base.Table : Waiting for lock on table "public"."flyway_schema_history"...
2026-03-30T10:17:20.355Z ERROR 1 --- [main] o.s.boot.SpringApplication : Application run failed
org.flywaydb.core.api.FlywayException: Unable to obtain table lock - another Flyway instance may be running.2. Diagnostik via Actuator Thread Dump
Jika server manajemen dijalankan pada port terpisah (misalnya management.server.port=8081) dan port tersebut dapat diakses saat inisialisasi, endpoint /actuator/threaddump menunjukkan thread state dari main thread yang terhenti pada pemanggilan driver JDBC:
"main" #1 prio=5 os_prio=0 cpu=1420.31ms elapsed=180.20s tid=0x00007f9c88009800 nid=0x8 runnable [0x00007f9c8e8ad000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0([email protected]/Native Method)
at java.net.SocketInputStream.read([email protected]/SocketInputStream.java:150)
at org.postgresql.core.VisibleBufferedInputStream.readMore(VisibleBufferedInputStream.java:161)
at org.postgresql.core.v3.QueryExecutorImpl.receiveFastpathResult(QueryExecutorImpl.java:620)
at org.flywaydb.database.postgresql.PostgreSQLAdvisoryLockTemplate.execute(PostgreSQLAdvisoryLockTemplate.java:64)
at org.flywaydb.core.internal.database.base.Table.lock(Table.java:82)3. Identifikasi Lock di Database (PostgreSQL)
Jalankan kueri berikut untuk menemukan sesi database yang memegang lock Flyway tanpa progres:
SELECT
pid,
usename,
client_addr,
state,
wait_event_type,
wait_event,
query,
backend_start
FROM pg_stat_activity
WHERE query ILIKE '%flyway%'
OR wait_event ILIKE '%advisory%';Runbook Mitigasi Darurat
Ikuti urutan langkah berikut secara berurutan saat deployment berada dalam kondisi deadlock di lingkungan produksi.
Langkah 1: Batalkan Rollout Deployment
Hentikan siklus pembuatan pod baru agar koneksi ke database tidak bertambah banyak:
# Batalkan deployment yang sedang berlangsung
kubectl rollout undo deployment/order-service -n production
# Atau jeda rollout untuk menghentikan pembuatan replica baru
kubectl rollout pause deployment/order-service -n productionLangkah 2: Rilis Lock Database Secara Aman
Pastikan tidak ada DDL yang sedang berjalan aktif sebelum memutus lock. Periksa status proses di database. Jika Pod pemegang lock telah di-terminate oleh Kubernetes namun koneksinya tertinggal (stale connection), terminasi sesi database terkait:
-- Jalankan di PostgreSQL dengan hak akses DBA
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE query ILIKE '%flyway%'
AND pid <> pg_backend_pid();Jika Flyway meninggalkan entri migrasi dengan status gagal pada tabel metadata (kolom success = false), aplikasi tidak akan dapat boot kembali hingga status diperbaiki:
-- Cek status migrasi terakhir
SELECT installed_rank, version, description, type, script, success
FROM flyway_schema_history
ORDER BY installed_rank DESC
LIMIT 1;
-- PERINGATAN: Hanya hapus baris jika perubahan DDL belum teraplikasi sebagian di DB!
-- Alternatif terbaik adalah menjalankan command Flyway Repair via CLI.
DELETE FROM flyway_schema_history WHERE success = false;Langkah 3: Skalakan Deployment ke Pod Stabil
Pastikan pod lama yang stabil melayani beban kerja secara penuh sambil menginvestigasi migrasi:
kubectl rollout resume deployment/order-service -n production
kubectl scale deployment/order-service --replicas=3 -n productionPostmortem: Akar Masalah DDL Berat saat Boot
Menggabungkan DDL berukuran besar atau berdurasi panjang ke dalam inisialisasi aplikasi runtime adalah sebuah anti-pattern. Penyebab utama kegagalan deployment umumnya mencakup:
- Locking tabel secara eksklusif (Exclusive Table Lock): Perintah seperti
ALTER TABLE orders ADD COLUMN metadata JSONB;atau penambahan indeks tanpaCONCURRENTLYpada tabel berukuran jutaan baris membutuhkan exclusive lock yang memblokir semua operasi baca/tulis aplikasi lain. - Desain probe Kubernetes yang tidak realistis: Nilai
readinessProbe.timeoutSecondsataufailureThresholdyang disamakan dengan durasi traffic runtime biasa, tanpa memperhitungkan potensi durasi migrasi schema. - Kegagalan idempotensi: Skrip migrasi tidak dirancang agar dapat dilanjutkan kembali (non-reentrant) saat proses terputus paksa di tengah jalan.
Pencegahan Struktural
Untuk mengeliminasi risiko deadlock di masa depan, pisahkan siklus hidup migrasi database dari siklus hidup deployment pod aplikasi.
1. Decoupling Migrasi ke Kubernetes Job (Pre-deployment)
Nonaktifkan eksekusi Flyway pada pod aplikasi Spring Boot dalam file konfigurasi application.yml:
spring:
flyway:
enabled: falseJalankan migrasi database sebagai satu instance terisolasi menggunakan Kubernetes Job atau ArgoCD/Helm pre-install/pre-upgrade hook sebelum pod aplikasi utama diperbarui:
apiVersion: batch/v1
kind: Job
metadata:
name: order-service-migration-v1-5
namespace: production
spec:
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: flyway-migration
image: flyway/flyway:10.8.1
args:
- -url=jdbc:postgresql://postgres-db:5432/app_db
- -user=db_migration_user
- -password=$(DB_PASSWORD)
- -locations=filesystem:/flyway/sql
- migrate
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
volumeMounts:
- name: migration-sql
mountPath: /flyway/sql
volumes:
- name: migration-sql
configMap:
name: order-service-migrations2. Tuning Konfigurasi Lock Flyway
Jika tim Anda belum dapat mengadopsi Kubernetes Job dan masih harus menjalankan migrasi via pod, sesuaikan konfigurasi toleransi lock pada application.yml agar pod kedua tidak langsung melempar exception saat menunggu antrean:
spring:
flyway:
enabled: true
# Tambah toleransi waktu tunggu antrean lock
lock-retry-count: 30
# Interval pengecekan lock (default biasanya 10 detik, total tunggu: 30 * 10s = 300s)
connect-retries: 5
baseline-on-migrate: trueCatatan: Menambah
lock-retry-countwajib diimbangi dengan penyesuaian nilaireadinessProbe.initialDelaySecondsatau memanfaatkanstartupProbeKubernetes agar kubelet tidak membunuh pod yang sedang sah menunggu migrasi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!