Menghadapi malam terburuk DBA dengan kepala dingin: mendiagnosis deadlock, lock contention, dan connection exhaustion dengan pg_stat_activity dan pg_locks, membedakan akar masalah versus gejala, mengatasi incident dengan runbook, dan menutupnya dengan post-mortem yang berujung perbaikan sistem — bukan mencari kambing hitam

Semua yang kita bangun — HA, backup, monitoring, IaC — berujung pada satu momen: incident. Database produksi melambat, aplikasi error, halaman pembayaran macet, dan kalian yang dipanggil. Di momen itu, pengetahuan saja tidak cukup: butuh metode, kecepatan, dan mental yang tenang.
Kabar baiknya: incident database hampir selalu jatuh ke beberapa pola yang bisa dipelajari. Episode ini memetakan tiga pola paling umum (deadlock, lock contention, connection exhaustion) beserta alat diagnosanya, lalu menutup dengan budaya post-mortem yang mengubah kegagalan menjadi perbaikan.
Aturan pertama DBA saat incident: jangan menyembuhkan gejala, temukan akarnya. Query lambat adalah gejala; penyebabnya bisa index hilang, lock menahan tabel, atau disk I/O penuh — tiga perlakuan berbeda.
Alur diagnosa yang disiplin:
pg_stat_activity, pg_locks, log, dashboard episode 7.Tool utama kalian di PostgreSQL:
SELECT pid, state, wait_event_type, wait_event,
now() - query_start AS durasi,
left(query, 60) AS query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY durasi DESC;Kolom wait_event_type adalah emas: Lock, IO, Client, CPU menunjukkan di mana proses sedang tersangkut — inilah kompas diagnosa pertama.
Konspirasi paling umum: query panjang memegang lock, query lain mengantre di belakangnya. Contoh nyata: ALTER TABLE atau VACUUM FULL menahan AccessExclusiveLock, sementara aplikasi menunggu SELECT.
Identifikasi pengantre:
SELECT pid, relation::regclass, mode, granted
FROM pg_locks
WHERE relation IS NOT NULL
ORDER BY granted DESC;Perhatikan granted = false — itu proses yang menunggu; yang granted = true dengan mode eksklusif adalah pemegang lock (tersangka utama). Langkah penyelesaian:
pg_stat_activity).pg_terminate_backend(pid) — tindakan terakhir, bukan pertama.lock_timeout, atau migration expand-contract.ALTER ROLE app_user SET lock_timeout = '5s';
ALTER ROLE app_user SET statement_timeout = '30s';lock_timeout mencegah aplikasi menggantung tanpa batas saat antre lock — salah satu jaring pengaman termurah yang pernah ada.
Deadlock terjadi saat dua transaksi saling memegang resource yang dibutuhkan yang lain. PostgreSQL mendeteksi dan membatalkan salah satunya, lalu menulis log:
ERROR: deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 555;
blocked by process 12346. Process 12346 waits for
ShareLock on transaction 444; blocked by process 12345.Penyebab paling umum di produksi: urutan update tidak konsisten. Transaksi A meng-update orders lalu users; transaksi B meng-update users lalu orders — suatu saat keduanya menabrak. Perbaikannya di level aplikasi: selalu update dalam urutan yang sama (misal berdasarkan id), atau persingkat transaksi (semakin singkat, semakin kecil jendela deadlock). Pencarian bukti historis: grep deadlock /var/log/postgresql/*.log — log adalah saksi terbaik.
Gejala klasik: aplikasi error "connection limit exceeded" / "remaining connection slots are reserved for superuser". Artinya max_connections tercapai. Penyebab biasanya bukan "traffic naik", melainkan:
Diagnosa cepat:
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
-- idle in transaction = tersangka kebocoran transaksi
SELECT pid, state, now() - xact_start AS xact_dur
FROM pg_stat_activity
WHERE state = 'idle in transaction' ORDER BY xact_dur DESC;Perhatikan idle in transaction — koneksi yang sudah selesai tapi transaksinya belum di-commit/rollback; ini klasik bocor dari kode aplikasi. Penyelesaian: fix aplikasi, tambah pooler (episode 3), dan jika darurat: pg_terminate_backend untuk koneksi idle-in-transaction tua.
Important
Ingat baris ini dari log PostgreSQL: remaining connection slots are reserved for superuser. Bahkan saat semua slot habis, PostgreSQL selalu menyisakan beberapa untuk superuser — itulah jalan darurat kalian masuk untuk pg_terminate_backend. Jangan pernah menghabiskan slot itu untuk aplikasi: jaga superuser_reserved_connections di nilai defaultnya.
Incident bukan waktu untuk berimprovisasi — adalah waktu untuk mengeksekusi runbook yang sudah disiapkan. Runbook yang baik per incident:
Tulis runbook untuk tiga pola di atas, simpan di repositori (bukan di kepala), dan latih lewat drill (gaya episode 15). Saat incident nyata datang, kalian tinggal mengeksekusi dengan tenang.
Setiap incident ditutup dengan post-mortem — tapi post-mortem yang benar bukan untuk mencari siapa yang salah, melainkan apa yang gagal di sistem sehingga manusia bisa salah. Budaya blameless ini yang membedakan tim matang.
Struktur post-mortem yang baik:
Dari pola incident kita: perbaikan yang sering lahir — index baru (episode 6), lock_timeout dipasang, migration dipisah (episode 17), monitoring ditambah (episode 7), runbook ditulis (episode ini). Setiap insiden menjadikan sistem lebih kuat — itulah siklus SRE yang sebenarnya.
wait_event: menebak tanpa melihat kolom ini seperti mengobati tanpa diagnosa.pg_terminate_backend membabi buta: membunuh query yang tidak bersalah bisa membuat situasi lebih buruk — bidik yang jelas menunggu lock atau idle-in-transaction.Inti yang harus dibawa pulang:
pg_stat_activity, pg_locks) → hipotesis → uji → verifikasi.granted = false, bidik pemegang lock; pasang lock_timeout.idle in transaction, pakai pooler, terminasi yang jelas bocor.Di episode 21 selanjutnya kita mendorong performansi ke level lanjutan: performance engineering lanjutan — lapisan cache in-memory, table partitioning, materialized views, dan tuning index tingkat tinggi, semuanya diuji dengan benchmark yang jujur. Sampai jumpa di episode 21!