Runbook incident queue dibutuhkan ketika sistem worker mulai berperilaku tidak konsisten: job menumpuk, lock tidak pernah lepas, retry meningkat tanpa hasil, atau data lama terus disajikan dari cache. Dalam kondisi insiden, masalah utamanya bukan hanya bug teknis, tetapi juga fakta bahwa sinyal antar-komponen sering tidak bisa dipercaya sepenuhnya: log bisa terlambat masuk, metrik bisa teragregasi, event bisa duplikat, dan status cache bisa menyesatkan.
Karena itu, diagnosis harus dirancang dengan pola pikir adversarial communication: anggap setiap sinyal dari queue, worker, lock store, cache, database, dan upstream bisa ambigu atau tidak lengkap. Artikel ini memberi runbook praktis untuk engineer backend dan DevOps: gejala yang perlu dikenali, root cause yang umum, langkah triase, keputusan rollback vs drain queue, penanganan poison message, timeout lock, idempotensi, cache invalidation, dan trade-off konsistensi.
Prinsip operasional: jangan percaya satu sinyal saja
Pada sistem queue terdistribusi, satu gejala bisa memiliki banyak penjelasan. Misalnya, backlog queue naik belum tentu berarti worker mati; bisa juga karena producer spike, downstream lambat, lock contention, atau poison message yang terus di-retry. Demikian juga, lock yang terlihat masih ada belum tentu benar-benar aktif; bisa jadi owner worker sudah crash tetapi TTL lock terlalu panjang.
Pola pikir yang berguna saat insiden:
- Asumsikan sinyal bisa terlambat: log dari worker A bisa muncul setelah worker B menyelesaikan job terkait.
- Asumsikan sinyal bisa duplikat: satu event publish dua kali, satu job diproses lebih dari sekali, atau callback upstream dikirim ulang.
- Asumsikan sinyal bisa parsial: metrik sukses ada, tetapi metrik timeout tertinggal karena exporter bermasalah.
- Asumsikan sinyal bisa menyesatkan: cache hit tinggi belum tentu baik bila isinya basi.
Implikasinya: verifikasi kondisi dengan lebih dari satu sumber bukti. Jangan mengambil tindakan destruktif hanya dari satu dashboard.
Model insiden yang paling sering: queue, worker, lock, cache
1. Lock stuck
Gejala umum:
- Job dengan key yang sama tidak pernah maju.
- Banyak log “resource locked”, “already processing”, atau “could not acquire lock”.
- Backlog naik, tetapi throughput worker tetap atau turun.
- Tidak ada error fatal yang jelas, namun job berputar di antrean.
Root cause yang sering:
- Worker crash setelah mendapat lock tetapi sebelum release.
- TTL lock terlalu panjang dibanding durasi normal job.
- Lock tidak punya owner token, sehingga release salah target.
- Clock skew atau timeout jaringan membuat lock tampak hidup lebih lama.
- Bug pada kode yang mengembalikan lebih awal tanpa finally untuk release lock.
2. Retry loop
Gejala umum:
- Jumlah retry melonjak tajam.
- Queue depth stagnan atau terus naik walau worker aktif.
- CPU worker tinggi, tetapi bisnis outcome tidak bertambah.
- Error yang sama muncul berulang untuk payload identik.
Root cause yang sering:
- Downstream dependency gagal sementara, tetapi retry terlalu agresif.
- Poison message: payload invalid atau state data tidak bisa diperbaiki dengan retry.
- Idempotensi buruk: retry memicu side effect baru lalu memperburuk kondisi.
- Timeout terlalu pendek, menyebabkan job yang sebenarnya bisa selesai dianggap gagal.
3. Cache basi
Gejala umum:
- User atau service membaca data lama setelah update berhasil.
- Database benar, tetapi API response tetap salah.
- Setelah restart atau flush cache parsial, masalah hilang sementara.
- Perbedaan hasil antar-instance aplikasi.
Root cause yang sering:
- Invalidasi cache gagal atau event invalidation hilang.
- Urutan event terbalik: cache diisi ulang oleh state lama setelah update baru.
- TTL terlalu lama untuk data yang sering berubah.
- Beberapa layer cache aktif tanpa strategi koherensi yang jelas.
Triase awal: 15 menit pertama saat insiden
Tujuan triase adalah menjawab tiga pertanyaan: apakah masalah masih berlangsung, apa blast radius-nya, dan apakah tindakan kita berisiko memperparah keadaan.
Checklist triase cepat
- Konfirmasi gejala dari lebih dari satu sumber
Cek dashboard queue depth, age of oldest message, retry count, error rate, dan log worker. - Tentukan ruang lingkup
Apakah semua queue terdampak, hanya satu topic, satu tenant, satu shard, atau satu jenis job? - Bedakan producer vs consumer problem
Apakah job diproduksi terlalu cepat atau consumer melambat? - Cek downstream dependency
Database, API eksternal, object storage, cache store, dan lock store. - Identifikasi perubahan terakhir
Deploy baru, perubahan konfigurasi timeout, perubahan schema, migrasi data, atau rotasi credential. - Tahan dorongan untuk flush/clear massal
Flush queue atau cache tanpa diagnosis sering menghapus bukti dan memperbesar dampak.
Metrik yang sebaiknya dilihat
- Queue depth: jumlah job menunggu.
- Oldest message age: indikator backlog lebih bermakna daripada count murni.
- Processing rate: job per menit yang benar-benar selesai.
- Retry rate: apakah kegagalan transient atau loop.
- Dead-letter rate: apakah sistem sudah mulai mengisolasi job bermasalah.
- Lock acquisition latency/failure: tanda contention atau lock store bermasalah.
- Cache hit ratio: berguna, tetapi harus dikaitkan dengan freshness.
- Downstream latency/error rate: database, HTTP dependency, RPC, storage.
Log yang perlu dicari
Pastikan log memiliki korelasi minimal: job_id, message_id, tenant_id, lock_key, dan attempt. Tanpa ini, incident response akan lebih lambat.
timestamp=... level=INFO job_id=job-812 type=invoice.sync attempt=4 lock_key=invoice:193 status=received
timestamp=... level=INFO job_id=job-812 lock_key=invoice:193 status=lock_acquired owner=worker-7
timestamp=... level=ERROR job_id=job-812 status=downstream_timeout dependency=billing-api timeout_ms=3000
timestamp=... level=WARN job_id=job-812 status=retry_scheduled backoff_s=60
Jika Anda tidak punya korelasi seperti ini, salah satu aksi pasca-insiden yang paling penting adalah menambahkannya.
Runbook per jenis insiden
Runbook 1: Lock stuck
Tujuan: memastikan apakah lock benar-benar hidup, yatim, atau hanya terlihat macet karena observabilitas yang tidak lengkap.
- Identifikasi key lock yang dominan
Ambil sampel job gagal acquire lock dan kelompokkan berdasarkan resource key. - Cari owner lock dan waktu hidupnya
Jika sistem lock mendukung owner token dan TTL, cek siapa pemilik lock dan sisa TTL. - Cocokkan dengan status worker
Apakah worker pemilik lock masih hidup, sedang bekerja, atau sudah hilang dari heartbeat? - Cek durasi normal job
Jika job biasanya selesai 10 detik tetapi TTL lock 30 menit, lock orphan akan lama memblokir progres. - Putuskan: tunggu TTL habis, force release, atau drain subset queue
Force release hanya aman jika Anda yakin owner sudah mati dan job bersifat idempotent.
Keputusan operasional:
- Tunggu TTL habis bila owner masih hidup atau status belum jelas.
- Force release bila owner hilang, heartbeat mati, dan side effect job aman terhadap duplikasi.
- Drain queue subset bila lock contention menyebabkan satu kelompok resource menghambat keseluruhan throughput.
Kesalahan umum:
- Melepas lock tanpa owner check.
- Menghapus semua lock sekaligus.
- Memperpendek TTL ekstrem tanpa memahami durasi puncak job, sehingga lock kedaluwarsa saat kerja masih berlangsung.
Pola implementasi yang lebih aman:
owner = generate_random_token()
acquired = lock.acquire(key="order:123", owner=owner, ttl_seconds=120)
if not acquired:
return RETRY_LATER
try:
process_order("123")
finally:
lock.release_if_owner(key="order:123", owner=owner)
Pola ini bekerja karena release hanya berhasil jika token owner cocok. Ini mencegah worker lain melepas lock yang bukan miliknya.
Runbook 2: Retry loop dan poison message
Tujuan: membedakan kegagalan sementara dari pesan yang memang tidak akan pernah berhasil.
- Ambil sampel payload dari job yang paling sering retry
Cari pola yang sama: field hilang, referensi data tidak ada, format invalid, atau kondisi bisnis yang tidak terpenuhi. - Kelompokkan error
Timeout dan 5xx biasanya kandidat transient; validation error atau foreign key missing lebih dekat ke poison message. - Periksa idempotensi side effect
Apakah setiap retry membuat charge, email, atau update eksternal baru? - Aktifkan backoff yang masuk akal
Retry rapat hanya menambah beban bila dependency sedang lambat. - Pindahkan poison message ke DLQ
Jangan biarkan satu payload merusak antrean utama.
Kapan rollback, kapan drain queue?
- Rollback deploy jika lonjakan retry mulai tepat setelah perubahan kode/config dan error tersebar luas.
- Drain queue bila producer terus menghasilkan job yang diketahui buruk, atau bila Anda perlu menghentikan konsumsi sambil memperbaiki dependency/downstream.
- Requeue selektif lebih aman daripada replay massal jika hanya subset job yang valid untuk dicoba ulang.
Strategi poison message:
- Beri batas attempt yang jelas.
- Gunakan DLQ dengan alasan kegagalan.
- Simpan payload asli dan metadata korelasi.
- Sediakan tool replay dengan filter, bukan replay semua.
if error.is_validation_error():
send_to_dlq(job, reason="validation_error")
elif error.is_transient():
retry_with_backoff(job)
else:
send_to_dlq(job, reason="unknown_non_retryable")
Debugging tip: jika retry count tinggi tetapi downstream terlihat sehat, jangan langsung menyalahkan worker. Bisa jadi payload lama tidak lagi kompatibel dengan schema atau aturan bisnis terbaru.
Runbook 3: Cache basi
Tujuan: memastikan apakah data salah berasal dari cache, invalidasi event, atau urutan update yang balapan.
- Bandingkan sumber kebenaran
Ambil satu entitas bermasalah. Cek database, cache, dan response API untuk key yang sama. - Telusuri alur write
Apakah aplikasi melakukan write-through, write-behind, atau cache-aside? - Cek invalidasi
Apakah event invalidation dipublish? Apakah consumer invalidation berjalan? Apakah key yang dihapus benar? - Cari race condition
Kasus umum: proses A update DB, proses B membaca state lama lalu mengisi cache sesaat setelah invalidasi. - Putuskan mitigasi
Invalidate key spesifik, pendekkan TTL sementara, bypass cache untuk endpoint kritis, atau matikan populasi asinkron yang rusak.
Trade-off konsistensi yang perlu dipahami:
- Cache-aside sederhana, tetapi rentan race dan stale read singkat.
- Write-through lebih konsisten pada read path, tetapi write jadi lebih kompleks dan lambat.
- Short TTL mengurangi stale duration, tetapi meningkatkan beban backend.
- Event-driven invalidation efisien, tetapi bergantung pada delivery event yang tidak selalu andal.
Pola cache-aside yang perlu diwaspadai:
def update_user(user_id, patch):
db.update(user_id, patch)
cache.delete(f"user:{user_id}")
def get_user(user_id):
data = cache.get(f"user:{user_id}")
if data is not None:
return data
data = db.get(user_id)
cache.set(f"user:{user_id}", data, ttl=300)
return data
Pola ini umum, tetapi masih bisa menghasilkan data basi jika ada pembaca paralel yang membaca versi lama lalu menulisnya kembali ke cache setelah update. Jika data sangat sensitif terhadap urutan, pertimbangkan versi data, token generasi, atau strategi invalidasi yang lebih kuat.
Skenario produksi: backlog naik, lock menumpuk, cache terlihat sehat
Misalkan sistem pemrosesan invoice memiliki queue invoice.sync. Gejalanya:
- Oldest message age naik cepat.
- Retry count meningkat.
- Dashboard cache menunjukkan hit ratio tinggi, sehingga sekilas terlihat baik.
- Log banyak berisi “could not acquire lock invoice:{id}”.
Diagnosis yang tahan salah paham:
- Jangan simpulkan cache sehat hanya dari hit ratio.
Hit ratio tinggi bisa berarti data lama terus disajikan. - Ambil 20 sampel invoice_id yang macet.
Cek apakah id yang sama terus berulang. Jika iya, ada hot key atau poison resource. - Cari owner lock untuk invoice yang sama.
Ternyata satu worker pernah timeout saat memanggil billing API setelah lock acquired. - Cek heartbeat worker.
Worker pemilik lock sudah hilang. Lock TTL 30 menit, terlalu panjang untuk job normal 15 detik. - Cek retry policy.
Retry setiap 5 detik tanpa jitter, sehingga job yang sama berebut lock terus-menerus. - Cek data invoice di cache.
Cache masih menyimpan status lama karena invalidasi hanya terjadi saat job sukses, padahal banyak job berhenti sebelum tahap itu.
Mitigasi yang aman:
- Pause consumer untuk queue yang terdampak.
- Force release hanya lock yang owner-nya dipastikan mati.
- Pindahkan payload tertentu ke DLQ bila setelah inspeksi ternyata ada data invalid.
- Naikkan backoff retry dan tambahkan jitter.
- Bypass cache sementara untuk endpoint status invoice yang kritis.
- Rollback jika insiden mulai setelah perubahan timeout, TTL, atau alur invalidasi.
Akar masalahnya bukan satu komponen tunggal, melainkan kombinasi lock TTL terlalu panjang, retry policy agresif, dan invalidasi cache yang hanya terjadi pada jalur sukses.
Rollback vs drain queue: cara memilih saat tekanan tinggi
Rollback cocok ketika
- Insiden bertepatan jelas dengan deploy atau perubahan konfigurasi.
- Error pattern konsisten pada banyak job yang sebelumnya sehat.
- Perubahan ada di consumer logic, timeout, serializer, schema mapping, atau invalidasi cache.
Drain queue cocok ketika
- Producer membanjiri antrean dengan payload buruk atau duplikat.
- Downstream sedang rusak dan memproses job hanya akan memperparah retry storm.
- Anda perlu menahan konsumsi untuk inspeksi aman tanpa kehilangan pesan.
Hindari keputusan ini
- Replay semua job secara massal tanpa validasi idempotensi.
- Flush cache global saat backend sedang rapuh, karena bisa memicu thundering herd.
- Menghapus DLQ demi “membersihkan dashboard”.
Idempotensi, timeout, dan desain yang memudahkan incident response
Banyak insiden queue memburuk karena sistem tidak didesain untuk menerima duplikasi. Padahal pada sistem terdistribusi, at-least-once delivery dan retry adalah kenyataan operasional. Karena itu:
- Gunakan idempotency key untuk side effect penting seperti pembayaran, sinkronisasi status, atau pengiriman notifikasi.
- Simpan hasil atau jejak pemrosesan berdasarkan kunci bisnis, bukan hanya job ID.
- Pisahkan retryable dan non-retryable error.
- Atur timeout berdasarkan observasi nyata, bukan tebakan. Timeout terlalu pendek menciptakan kegagalan palsu; terlalu panjang memperlambat recovery.
def process_payment(command):
key = command.idempotency_key
existing = db.find_processed_result(key)
if existing:
return existing
result = payment_gateway.charge(command)
db.store_processed_result(key, result)
return result
Idempotensi seperti ini tidak menghilangkan semua masalah, tetapi sangat menurunkan risiko saat Anda perlu melakukan retry, replay, atau force release lock.
Checklist runbook yang bisa dipakai saat on-call
A. Identifikasi
- Apakah queue depth naik, oldest age naik, atau keduanya?
- Queue mana yang terdampak?
- Apakah masalah mulai setelah deploy/perubahan config?
- Apakah semua tenant/resource terdampak atau hanya subset?
B. Verifikasi silang
- Cek metrik queue, worker, lock store, cache, DB, dan downstream.
- Cocokkan log dengan sampel job nyata.
- Jangan percaya satu panel observabilitas saja.
C. Lock
- Key lock apa yang paling sering gagal diakuisisi?
- Siapa owner lock? Apakah heartbeat owner masih hidup?
- Berapa TTL lock dibanding durasi normal job?
- Apakah aman force release berdasarkan idempotensi?
D. Retry dan DLQ
- Apakah error transient atau poison message?
- Berapa attempt maksimum?
- Apakah ada backoff dan jitter?
- Apakah replay subset lebih aman daripada replay massal?
E. Cache
- Apakah data salah di DB atau hanya di cache/API?
- Apakah invalidasi event terkirim dan terkonsumsi?
- Apakah ada multi-layer cache yang saling tidak sinkron?
- Apakah perlu bypass cache sementara untuk jalur kritis?
F. Keputusan mitigasi
- Pause consumer?
- Drain queue tertentu?
- Rollback deploy/config?
- Force release lock selektif?
- Pindahkan poison message ke DLQ?
Pencegahan pasca-insiden
Runbook yang baik bukan hanya membantu saat insiden, tetapi juga mengarah pada perbaikan sistemik setelahnya.
1. Perbaiki observabilitas
- Tambahkan correlation ID, lock key, attempt number, dan owner token di log.
- Tambahkan metrik untuk oldest message age, lock acquisition failure, DLQ inflow, dan cache freshness.
- Buat dashboard yang memisahkan producer rate, consumer rate, dan success rate.
2. Perkuat kontrol retry
- Gunakan exponential backoff dengan jitter.
- Tentukan error yang boleh retry dan yang harus langsung ke DLQ.
- Batasi replay massal dengan persetujuan atau tooling khusus.
3. Perbaiki desain lock
- Selalu gunakan owner token.
- Sesuaikan TTL dengan durasi p95/p99 yang realistis, bukan rata-rata semata.
- Gunakan heartbeat atau renewal hanya jika benar-benar dibutuhkan dan dapat diobservasi.
4. Desain cache yang eksplisit
- Tentukan source of truth dan model konsistensinya.
- Dokumentasikan key invalidation dan ownership per service.
- Untuk data kritis, pertimbangkan bypass cache saat state baru belum stabil.
5. Uji skenario kegagalan
- Worker crash setelah acquire lock.
- Downstream timeout di tengah side effect.
- Duplikasi message dari broker.
- Invalidasi cache datang terlambat atau hilang.
Catatan penting: sistem queue yang sehat bukan sistem yang mengasumsikan komunikasi antar-komponen selalu benar dan tepat waktu, melainkan sistem yang tetap aman ketika komunikasi itu ambigu, terlambat, duplikat, atau sebagian salah.
Penutup
Runbook incident queue untuk kasus lock stuck, retry loop, dan cache basi harus berangkat dari kenyataan operasional bahwa sinyal sistem sering tidak sempurna. Diagnosis yang baik mengandalkan verifikasi silang, membedakan gejala dari akar masalah, dan memilih mitigasi yang aman: rollback bila perubahan baru merusak perilaku umum, drain queue bila antrean perlu diisolasi, force release lock hanya jika owner jelas mati, dan replay hanya jika idempotensi terjamin.
Jika harus memilih satu investasi pasca-insiden, pilih yang paling menurunkan ambiguitas: korelasi log yang baik, pemisahan error retryable vs non-retryable, owner token untuk lock, dan strategi cache invalidation yang eksplisit. Empat hal itu biasanya memberi dampak paling besar pada kecepatan diagnosis insiden berikutnya.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!