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 production

Langkah 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 production

Postmortem: 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 tanpa CONCURRENTLY pada tabel berukuran jutaan baris membutuhkan exclusive lock yang memblokir semua operasi baca/tulis aplikasi lain.
  • Desain probe Kubernetes yang tidak realistis: Nilai readinessProbe.timeoutSeconds atau failureThreshold yang 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: false

Jalankan 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-migrations

2. 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: true

Catatan: Menambah lock-retry-count wajib diimbangi dengan penyesuaian nilai readinessProbe.initialDelaySeconds atau memanfaatkan startupProbe Kubernetes agar kubelet tidak membunuh pod yang sedang sah menunggu migrasi.