Runbook anti-kacau untuk queue worker dan cache di produksi pada dasarnya adalah seperangkat langkah yang bisa dijalankan tim secara tenang saat sistem mulai menunjukkan gejala: backlog naik, job macet, job dobel, cache stale, lock tidak lepas, atau retry storm. Tujuannya bukan membuat satu engineer jadi pahlawan, melainkan membuat operasi aman, bisa diulang, dan mudah dipindahkan antar shift.
Kalau ada satu prinsip yang layak dipegang, itu adalah semangat kerja ala “We’re Going to Make Out Like Bandits”: hasil bagus datang dari kebiasaan tim yang konsisten, bukan heroics saat sistem sudah terbakar. Untuk queue dan cache, itu berarti observability yang memadai, idempotensi yang kuat, kebijakan retry yang masuk akal, dan keputusan operasional yang jelas tentang kapan harus pause worker, kapan invalidasi cache selektif, dan kapan flush adalah pilihan terburuk.
Tujuan runbook: tenang, aman, dan bisa diulang
Queue worker dan cache sering gagal dengan cara yang tampak acak, padahal polanya berulang. Runbook yang baik harus membantu menjawab empat pertanyaan dengan cepat:
- Apa gejalanya? Misalnya job berhenti diproses, latensi melonjak, atau data lama terus muncul.
- Apa radius dampaknya? Hanya satu queue, satu tenant, satu jenis job, atau seluruh sistem?
- Apa mitigasi tercepat yang paling aman? Pause worker, kurangi concurrency, matikan retry, atau invalidasi subset cache.
- Apa pencegahan jangka panjangnya? Biasanya terkait idempotensi, locking, TTL, backoff, dan desain job.
Tanpa runbook, tim cenderung melakukan tindakan impulsif: restart semua worker, flush semua cache, requeue semua job, atau menambah worker secara membabi buta. Tindakan seperti ini sering memperburuk keadaan karena menyamarkan akar masalah atau menciptakan thundering herd.
Metrik yang wajib dipantau
Sebelum membahas diagnosis, pastikan sinyal sistem memang terlihat. Banyak insiden queue dan cache memburuk bukan karena bug awalnya paling parah, tetapi karena tim tidak tahu apa yang sedang terjadi.
Queue dan worker
- Queue depth/backlog: jumlah job menunggu per queue.
- Oldest job age: umur job tertua yang belum diproses. Ini sering lebih berguna daripada sekadar panjang queue.
- Throughput: jumlah job sukses per menit.
- Failure rate: jumlah job gagal per interval.
- Retry rate: indikasi awal retry storm.
- Processing latency: waktu dari enqueue ke mulai diproses, dan dari mulai ke selesai.
- Worker concurrency: jumlah worker aktif dan utilisasinya.
- Worker restart/crash count: sinyal OOM, panic, deadlock, atau deployment bermasalah.
Cache dan locking
- Cache hit ratio: turun tajam bisa berarti key berubah, TTL terlalu pendek, atau stampede.
- Eviction rate: menandakan memory pressure.
- Latency cache read/write: penting untuk Redis atau cache jaringan.
- Jumlah key lock aktif: terutama lock yang umurnya melebihi ekspektasi.
- TTL distribution: apakah banyak key penting hampir habis bersamaan.
- Miss rate per endpoint/use case: membantu membedakan masalah global dari lokal.
Dependensi hilir
- Database latency dan connection pool saturation
- API pihak ketiga: error rate, timeout, rate limit
- CPU, memory, disk I/O, network saturation
Kalau Anda hanya punya satu metrik queue, pilih oldest job age. Queue bisa terlihat “pendek” tapi tetap bermasalah kalau job tertentu menggantung dan tidak pernah selesai.
Checklist diagnosis cepat saat insiden
Tujuan checklist ini adalah mempersempit masalah sebelum menyentuh produksi terlalu jauh.
1. Tentukan kelas masalahnya
- Apakah job tidak diambil worker?
- Apakah job diambil tapi tidak selesai?
- Apakah job selesai tapi efeknya dobel?
- Apakah cache menyajikan data stale?
- Apakah lock tertinggal sehingga job berikutnya tidak jalan?
- Apakah retry bertambah eksponensial dan memenuhi queue?
2. Batasi radius dampak
- Queue mana yang terdampak?
- Jenis job mana?
- Tenant, region, atau shard mana?
- Apakah mulai terjadi setelah deploy, migrasi schema, perubahan TTL, atau gangguan dependensi?
3. Periksa health worker
- Apakah worker hidup dan mengambil job?
- Apakah ada crash loop, OOM, deadlock, atau saturasi CPU?
- Apakah concurrency terlalu tinggi sehingga menabrak database atau API downstream?
4. Periksa health dependensi
- Database lambat atau lock contention?
- Redis atau cache store mengalami latency tinggi?
- API eksternal timeout atau membalas 429/5xx?
5. Validasi semantics job
- Apakah job idempotent?
- Apakah retry aman dijalankan ulang?
- Apakah job menyimpan state transisional yang bisa membuat hasil dobel saat worker restart?
6. Evaluasi risiko tindakan operasional
- Pause worker aman atau justru memperbesar backlog yang sensitif waktu?
- Flush cache akan menghilangkan stale data, tetapi apakah akan memicu stampede ke database?
- Requeue massal aman, atau akan memproses side effect lagi?
Gejala umum dan cara menanganinya
Job macet: di-queue tapi tidak selesai
Gejalanya biasanya backlog naik, umur job tertua meningkat, throughput turun, tetapi error rate belum tentu tinggi. Penyebab umum:
- Worker tidak jalan atau tidak subscribe ke queue yang benar.
- Concurrency terlalu rendah.
- Job menunggu lock, I/O, atau query lambat.
- Worker menggantung pada panggilan jaringan tanpa timeout memadai.
Mitigasi cepat:
- Periksa apakah worker benar-benar aktif dan mengambil job.
- Lihat satu contoh job yang macet dan telusuri langkah eksekusinya.
- Jika ada dependensi lambat, kurangi concurrency sebelum menambah worker. Menambah worker di atas sistem hilir yang sudah jenuh biasanya memperburuk latensi.
- Pause queue tertentu jika job tersebut berisiko menumpuk efek samping atau menahan lock.
Pencegahan: timeout yang eksplisit, circuit breaker ke API eksternal, dan pemecahan job besar menjadi job kecil yang durasinya lebih stabil.
Job dobel: side effect terjadi dua kali
Ini biasanya muncul saat worker crash setelah side effect terjadi tetapi sebelum status selesai dicatat, atau saat retry terjadi pada job yang tidak idempotent.
Contoh sumber masalah:
- Mengirim email dua kali.
- Membuat invoice dua kali.
- Mengurangi stok dua kali.
Mitigasi cepat:
- Hentikan sementara worker untuk job yang side effect-nya sensitif.
- Matikan retry otomatis jika sumber gagal belum stabil.
- Tambahkan pagar sementara berbasis deduplication key atau pengecekan status di storage utama.
Pencegahan: setiap job yang menghasilkan side effect harus punya idempotency key yang bisa diverifikasi dari penyimpanan yang tahan restart, bukan hanya memory lokal worker.
// Pseudocode: idempotent job handler
function processPaymentJob(job) {
const key = `payment:${job.paymentId}:captured`;
if (db.exists('processed_operations', key)) {
return; // sudah diproses, aman di-skip
}
beginTransaction();
try {
capturePayment(job.paymentId);
db.insert('processed_operations', { key, processed_at: now() });
commit();
} catch (err) {
rollback();
throw err;
}
}Poin pentingnya bukan tabel atau sintaksnya, melainkan fakta bahwa bukti pemrosesan disimpan di sistem yang menjadi sumber kebenaran, sehingga retry tidak menghasilkan efek ganda.
Cache stale: data lama terus tampil
Cache stale biasanya bukan sekadar TTL terlalu panjang. Sering kali akar masalahnya adalah invalidasi yang tidak sinkron dengan perubahan data, atau strategi cache-aside yang tidak mempertimbangkan race condition.
Mitigasi cepat:
- Identifikasi apakah stale terjadi pada subset key tertentu atau seluruh namespace.
- Jika memungkinkan, lakukan invalidasi selektif, bukan flush total.
- Periksa apakah writer berhasil memperbarui database tetapi gagal menghapus cache.
- Jika ada banyak pembaruan bersamaan, pertimbangkan menambah versioned key sementara.
Kapan flush cache? Hampir selalu opsi terakhir. Flush global aman hanya jika:
- Anda paham beban baca pasca-flush masih bisa ditanggung database.
- Anda yakin stale data bersifat sistemik, bukan hanya pada segmen tertentu.
- Anda punya perlindungan terhadap cache stampede.
Pencegahan:
- Gunakan versioned keys untuk objek penting.
- Tambahkan TTL dengan jitter agar key tidak kedaluwarsa bersamaan.
- Untuk data yang sangat sensitif, pertimbangkan write-through atau event-driven invalidation.
// Pseudocode cache-aside dengan versioned key
function getUserProfile(userId) {
const version = db.getUserCacheVersion(userId);
const key = `user:${userId}:v${version}`;
let data = cache.get(key);
if (data) return data;
data = db.loadUserProfile(userId);
cache.set(key, data, { ttlSeconds: 300, jitterSeconds: 30 });
return data;
}
function updateUserProfile(userId, payload) {
db.updateUserProfile(userId, payload);
db.incrementUserCacheVersion(userId);
}Pendekatan ini menghindari keharusan menghapus key lama satu per satu saat race condition terjadi. Trade-off-nya adalah potensi sisa key lama sampai TTL habis.
Lock tidak lepas
Lock tertinggal sering muncul saat worker mati mendadak, waktu eksekusi melebihi TTL lock, atau mekanisme unlock tidak dijalankan dalam semua jalur error.
Mitigasi cepat:
- Verifikasi apakah lock itu benar-benar orphan atau masih dipakai proses aktif.
- Jika lock berbasis TTL, periksa apakah TTL terlalu panjang atau terlalu pendek.
- Lepas lock secara manual hanya jika Anda yakin tidak ada worker aktif yang masih memegang konteks kerja tersebut.
Pencegahan:
- Selalu beri lease/TTL pada lock.
- Gunakan token kepemilikan lock sehingga hanya pemilik yang boleh melepaskan.
- Jika job bisa lama, pakai perpanjangan lease yang eksplisit dan terukur.
// Pseudocode lock dengan token kepemilikan
const token = randomToken();
const acquired = cache.set('lock:report:123', token, { nx: true, ttlSeconds: 60 });
if (!acquired) return;
try {
generateReport();
} finally {
const current = cache.get('lock:report:123');
if (current === token) {
cache.delete('lock:report:123');
}
}Tanpa token, worker lain berisiko menghapus lock yang sebenarnya sedang sah dipakai proses lain.
Retry storm
Retry storm terjadi saat banyak job gagal karena akar masalah yang sama, lalu semuanya mencoba lagi hampir bersamaan. Ini sering menenggelamkan queue dan menghantam dependensi hilir yang sebenarnya sudah sakit.
Mitigasi cepat:
- Pause worker atau nonaktifkan konsumsi pada queue yang gagal masif.
- Kurangi concurrency agar beban ke dependensi turun.
- Matikan retry otomatis sementara, atau naikkan backoff secara signifikan.
- Pisahkan job gagal ke dead-letter queue bila tersedia.
Pencegahan:
- Exponential backoff dengan jitter.
- Batas retry yang realistis.
- Circuit breaker pada integrasi eksternal.
- Klasifikasi error: retry hanya untuk error transien, bukan error validasi atau bug deterministik.
Backlog naik terus
Backlog yang naik tidak selalu berarti kekurangan worker. Sering justru throughput turun karena worker sibuk mengulang job yang gagal, menunggu lock, atau dibatasi dependensi eksternal.
Diagnosis cepat:
- Apakah enqueue rate memang naik?
- Apakah service time job membesar?
- Apakah queue diisi oleh satu jenis job berat yang menahan pekerjaan lain?
Mitigasi cepat:
- Pisahkan queue menurut prioritas atau kelas kerja.
- Throttle job berat.
- Tambahkan worker hanya jika bottleneck bukan di dependency downstream.
Kapan harus pause worker dan kapan jangan
Pause worker adalah alat penting, bukan tanda panik. Tujuannya menghentikan kerusakan lanjutan sambil memberi ruang observasi.
Pause worker jika:
- Job tidak idempotent dan sedang menghasilkan side effect ganda.
- Retry storm sedang menghantam database atau API eksternal.
- Lock bermasalah menyebabkan antrean makin kusut.
- Deploy baru jelas memperkenalkan bug deterministik pada handler job.
Jangan langsung pause semua worker jika:
- Masalah hanya di satu queue atau satu jenis job.
- Ada job kritikal yang bersifat pemulihan atau kompensasi.
- Backlog sensitif waktu dan penghentian total justru menimbulkan kehilangan SLA yang lebih besar.
Praktik yang lebih aman adalah pause selektif: per queue, per consumer group, atau per jenis job. Ini memberi ruang investigasi tanpa mematikan seluruh sistem asinkron.
Kapan flush cache dan kapan harus menahan diri
Flush cache terdengar sederhana, tetapi di produksi ia sering menjadi tombol “perbaiki satu masalah, buat dua masalah baru”. Setelah flush, semua request akan lari ke database atau service origin.
Flush cache boleh dipertimbangkan jika:
- Key stale tersebar luas dan tidak ada mekanisme invalidasi selektif yang memadai.
- Data salah berdampak bisnis tinggi dan harus dihentikan segera.
- Sistem origin sudah disiapkan menghadapi lonjakan miss.
Jangan flush cache jika:
- Masalah hanya pada satu namespace atau beberapa key.
- Database sedang lambat atau connection pool mepet.
- Anda tidak punya mitigasi stampede seperti locking, stale-while-revalidate, atau warmup bertahap.
Alternatif yang lebih aman:
- Delete by prefix/namespace jika didukung.
- Naikkan versi key.
- Warmup key kritikal sebelum traffic penuh diarahkan.
Pola desain pencegahan yang paling berguna
1. Idempotensi sebagai default, bukan fitur tambahan
Kalau suatu job bisa di-retry, anggap ia akan dijalankan lebih dari sekali. Idempotensi tidak harus berarti “tidak pernah ada duplikasi”, tetapi “hasil akhirnya tetap benar meski ada retry atau replay”.
2. Pisahkan enqueue dari side effect besar
Jangan taruh terlalu banyak langkah rapuh dalam satu job panjang. Memecah kerja menjadi beberapa tahap kecil memudahkan retry yang aman dan observability yang lebih jelas. Trade-off-nya adalah kompleksitas orkestrasi meningkat.
3. Klasifikasi error
Bedakan error transien, error permanen, dan error akibat bug. Retry hanya masuk akal untuk kelas pertama. Tanpa klasifikasi ini, queue menjadi tempat memutar ulang kegagalan yang sama.
4. Dead-letter queue
Job yang gagal berulang sebaiknya dipindahkan ke jalur khusus untuk inspeksi manual atau replay terkontrol. Ini mencegah queue utama dipenuhi sampah operasional.
5. Concurrency limit per resource
Sering lebih penting membatasi jumlah job per pelanggan, per dokumen, atau per endpoint eksternal daripada menambah total worker. Ini mengurangi kontensi lock dan ledakan retry lokal.
6. TTL dan jitter
Untuk cache dan lock, TTL tanpa jitter bisa menciptakan lonjakan bersamaan. Jitter kecil membuat beban lebih tersebar dan mengurangi stampede.
7. Observability per job type
Jangan gabungkan semua metrik queue menjadi satu angka global. Masalah pada satu handler berat akan tertutup oleh volume job ringan jika tidak dipisah per tipe.
Contoh alur insiden: backlog naik, job dobel, dan cache stale setelah deploy
Misalkan setelah deploy sore hari, tim melihat:
- Backlog queue pembayaran naik cepat.
- Beberapa pelanggan menerima notifikasi ganda.
- Halaman status pembayaran kadang masih menampilkan state lama.
Langkah penanganan yang tenang
- Identifikasi scope: hanya queue pembayaran yang terdampak, queue email biasa masih sehat.
- Pause selektif worker pembayaran untuk mencegah side effect ganda bertambah.
- Periksa log dan trace: ditemukan handler baru memanggil gateway pembayaran, lalu crash sebelum menyimpan status operasi sukses.
- Validasi cache: cache status pembayaran memakai cache-aside, tetapi invalidasi gagal karena event pasca-commit tidak terkirim saat proses crash.
- Mitigasi cepat: nonaktifkan retry otomatis untuk job pembayaran, tambahkan pengecekan idempotency key di storage utama, dan invalidasi selektif key status pembayaran yang terdampak.
- Recovery: replay job yang aman saja, berdasarkan daftar yang belum memiliki bukti side effect sukses.
- Pencegahan: ubah urutan commit, simpan bukti idempotensi secara atomik dengan perubahan status, serta tambahkan dashboard oldest job age dan retry rate per job type.
Pola seperti ini jauh lebih aman daripada “restart semua worker lalu flush semua cache”, karena tim mengendalikan radius dampak dan memahami state yang benar sebelum replay.
Anti-pattern operasional yang sering bikin keadaan lebih buruk
- Flush cache global tanpa menghitung beban origin.
- Menambah worker saat downstream sedang jenuh.
- Retry tanpa backoff dan tanpa jitter.
- Menganggap queue itu exactly-once. Dalam praktik, banyak sistem queue lebih dekat ke at-least-once, sehingga idempotensi menjadi kewajiban.
- Lock tanpa TTL atau unlock tanpa verifikasi kepemilikan.
- Job terlalu besar dan memegang resource terlalu lama.
- Tidak memisahkan queue prioritas tinggi dan rendah.
- Requeue massal tanpa menyaring job yang side effect-nya sudah terjadi.
- Runbook hanya ada di kepala satu orang.
Checklist runbook operasional yang sebaiknya benar-benar ditulis
Bagian ini idealnya hidup di dokumentasi internal dan ditautkan dari dashboard alert.
Informasi minimum per queue/job
- Nama queue dan tingkat prioritas.
- Pemilik teknis dan pemilik produk.
- Dependensi utama: database, Redis, API eksternal.
- Apakah job idempotent.
- Kebijakan retry dan dead-letter.
- Dampak bisnis jika pause dilakukan.
Langkah diagnosis
- Cek backlog, oldest job age, failure rate, retry rate.
- Cek health worker dan deployment terakhir.
- Cek dependency health.
- Ambil satu contoh job dan telusuri end-to-end.
- Tentukan apakah masalahnya transien, deterministik, atau data-specific.
Langkah mitigasi cepat
- Pause selektif worker jika side effect ganda atau retry storm terjadi.
- Kurangi concurrency jika downstream saturasi.
- Matikan retry otomatis untuk error deterministik.
- Invalidasi cache secara selektif.
- Lepas lock manual hanya dengan verifikasi kepemilikan dan status proses aktif.
Keputusan pemulihan
- Tentukan job mana yang aman di-replay.
- Pisahkan job yang perlu kompensasi manual.
- Monitor metrik pasca-perbaikan sebelum membuka worker penuh.
Penutup
Runbook anti-kacau untuk queue worker dan cache di produksi bukan dokumen formalitas. Ia adalah alat untuk membuat insiden lebih kecil, respons lebih tenang, dan hasil lebih dapat diprediksi. Dengan budaya kerja yang menolak heroics dan memilih kebiasaan yang bisa diulang, tim akan lebih sering menang dengan cara membosankan: metrik jelas, idempotensi kuat, retry masuk akal, invalidasi cache terukur, dan keputusan pause/flush yang disiplin.
Kalau Anda ingin mulai dari yang paling berdampak, kerjakan tiga hal lebih dulu: ukur oldest job age, pastikan job side effect bersifat idempotent, dan tulis aturan kapan worker boleh dipause serta kapan cache tidak boleh di-flush. Tiga hal ini saja sudah cukup untuk menurunkan kekacauan operasional secara nyata.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!