Failure mode queue dan cache pada sistem yang menua jarang muncul sebagai satu bug tunggal. Gejalanya biasanya tersebar: job diproses dua kali, cache berisi data lama, dead letter queue terus bertambah, lock tidak pernah lepas, retry mendadak meledak, atau dua node memberi jawaban berbeda untuk request yang sama. Pada sistem yang telah lama hidup, masalah ini sering lahir dari interaksi antar komponen, bukan dari satu service yang jelas-jelas rusak.
Jika Anda pernah melihat sistem yang tampak “masih jalan” tetapi perilakunya makin sulit diprediksi, Anda sedang berhadapan dengan kompleksitas ala Jurassic Park computers: lapisan lama, integrasi parsial, asumsi historis yang tidak lagi valid, dan komponen yang saling bergantung tanpa kontrak yang tegas. Artikel ini mengubah konteks itu menjadi panduan operasional modern: bagaimana mengenali failure mode queue, cache, worker, locking, dan consistency; metrik apa yang harus dipantau; serta bagaimana merespons incident tanpa memperburuk keadaan.
Kenapa sistem yang menua lebih rentan terhadap failure mode ini
Sistem legacy tidak selalu berarti teknologi lama. Yang lebih berbahaya adalah statefulness yang tersebar: ada state di queue, cache, database, worker memory, scheduler, dan kadang di side effect eksternal seperti email gateway atau payment processor. Ketika asumsi awal desain tidak lagi cocok dengan beban, pola deploy, atau topologi cluster saat ini, failure mode mulai muncul.
- Kontrak idempotency tidak konsisten: producer menganggap job aman di-retry, consumer tidak.
- TTL dan timeout tidak disejajarkan: visibility timeout queue lebih pendek dari waktu proses nyata, lock TTL terlalu singkat, cache TTL terlalu panjang.
- Komponen observability tidak lengkap: antrean terlihat sehat dari sisi throughput, padahal duplicate rate tinggi.
- Multi-node tanpa sumber kebenaran yang jelas: cache lokal per node, clock drift, atau read replica lag memunculkan state yang berbeda.
- Perubahan bertahap: patch kecil selama bertahun-tahun menambah cabang logika tanpa merapikan model konsistensi.
Akibatnya, sistem tidak gagal secara dramatis di awal. Ia gagal secara ambigu: sebagian request sukses, sebagian timeout, sebagian lagi sukses dua kali.
Failure mode queue dan cache yang paling sering muncul
1. Job dobel atau diproses lebih dari sekali
Ini adalah failure mode paling umum pada sistem berbasis queue. Banyak queue modern memberi jaminan at-least-once delivery, bukan exactly once. Artinya, duplicate delivery bukan anomali, melainkan perilaku yang harus diantisipasi.
Penyebab umum:
- Worker crash setelah side effect terjadi tetapi sebelum acknowledgement terkirim.
- Visibility timeout lebih pendek daripada durasi kerja aktual.
- Consumer lambat akibat downstream timeout, lalu message muncul kembali dan diambil worker lain.
- Retry manual dari operator tanpa pengecekan status job sebelumnya.
Gejalanya sering tampak sebagai email terkirim dua kali, stok berkurang ganda, invoice dibuat lebih dari sekali, atau webhook keluar berulang.
Mitigasi utama adalah idempotency. Jangan bergantung pada “queue kami biasanya tidak dobel”. Simpan kunci idempotency yang mewakili operasi bisnis, bukan hanya ID pesan teknis.
// Pseudocode consumer yang idempotent
function handleOrderPaid(event) {
key = "order-paid:" + event.orderId
if (idempotencyStore.exists(key)) {
return
}
beginTransaction()
try {
if (paymentAlreadyApplied(event.orderId)) {
idempotencyStore.put(key, ttl=86400)
commit()
return
}
applyPayment(event.orderId, event.amount)
markOrderAsPaid(event.orderId)
idempotencyStore.put(key, ttl=86400)
commit()
} catch (err) {
rollback()
throw err
}
}Poin pentingnya: pemeriksaan idempotency harus terkait dengan state bisnis yang tahan crash, idealnya di penyimpanan yang konsisten dengan side effect utama. Jika idempotency hanya disimpan di cache volatile tanpa relasi ke transaksi inti, duplicate tetap mungkin lolos saat restart atau failover.
2. Cache stale atau data lama terus muncul
Cache stale muncul ketika data sumber sudah berubah tetapi cache belum diperbarui atau tidak pernah di-invalidasi dengan benar. Pada sistem yang menua, penyebabnya sering bukan sekadar TTL terlalu panjang, melainkan adanya banyak jalur tulis yang tidak semua tahu cara menghapus cache.
Pola umum:
- Update data lewat aplikasi A menghapus cache, tetapi update dari batch job legacy tidak.
- Cache key dibangun dari parameter yang tidak seragam antar service.
- Node memiliki local in-memory cache sendiri, sementara invalidation hanya dikirim ke sebagian node.
- TTL terlalu agresif sehingga stale data hidup lebih lama daripada toleransi bisnis.
Mitigasi praktis:
- Pilih model cache yang eksplisit: cache-aside, write-through, atau write-behind. Jangan campur pola tanpa alasan.
- Definisikan siapa yang bertanggung jawab atas invalidation.
- Bedakan data yang boleh stale selama beberapa detik dari data yang harus konsisten saat transaksi selesai.
- Gunakan versi atau namespace pada key jika skema invalidation sulit dibersihkan satu per satu.
Kesalahan umum adalah menganggap cache sebagai lapisan performa murni. Pada kenyataannya, cache adalah lapisan konsistensi tambahan. Begitu dipakai, Anda harus merancang kapan data boleh salah dan selama berapa lama.
3. Dead letter queue menumpuk
Dead letter queue (DLQ) yang terus bertambah menandakan sistem tidak hanya gagal, tetapi juga gagal berulang untuk jenis input yang sama. DLQ bukan tempat sampah akhir; ia adalah alat diagnosis.
Penyebab umum:
- Payload rusak atau tidak kompatibel dengan format baru.
- Consumer menganggap error transient padahal sebenarnya error permanen.
- Dependensi eksternal berubah kontrak atau mengembalikan data tak terduga.
- Migration data parsial membuat sebagian record tidak valid.
Yang perlu dilakukan:
- Kelompokkan pesan DLQ berdasarkan root cause, bukan hanya error string mentah.
- Bedakan mana yang aman di-replay dan mana yang akan memicu side effect ulang.
- Buat jalur reprocessing terkontrol, bukan sekadar “push balik semua pesan ke queue utama”.
Jika operator hanya mengosongkan DLQ dengan replay massal tanpa klasifikasi, Anda berisiko menciptakan retry storm dan duplicate side effect sekaligus.
4. Lock orphan atau lock yatim
Lock orphan terjadi ketika lock tetap ada walaupun pemiliknya sudah mati, hang, atau kehilangan koneksi. Pada sistem yang menua, lock sering ditambahkan sebagai tambalan cepat untuk mencegah race condition, tetapi tidak disertai desain expiry dan ownership yang kuat.
Gejalanya:
- Job tertentu berhenti diproses padahal queue masih berisi item.
- Worker terus melaporkan “resource busy”.
- Lock key bertahan jauh lebih lama dari durasi normal proses.
Mitigasi minimum:
- Lock harus memiliki TTL.
- Nilai lock harus mengandung token pemilik acak, bukan sekadar boolean.
- Release lock hanya boleh dilakukan oleh pemilik yang memegang token yang sama.
# Pola umum lock aman secara dasar di Redis
SET lock:order:123 random-token NX EX 30
# Saat melepas lock, verifikasi token pemilik terlebih dahulu
if GET lock:order:123 == random-token then
DEL lock:order:123
endMenghapus lock tanpa memeriksa token pemilik dapat menyebabkan worker A melepas lock milik worker B setelah lock lama kedaluwarsa dan diambil ulang.
5. Retry storm
Retry storm terjadi ketika banyak worker serentak mengulang pekerjaan yang sama atau jenis error yang sama, sehingga sistem makin berat dan peluang pulih makin kecil. Ini sering dipicu oleh timeout downstream, rate limit eksternal, atau konfigurasi retry yang terlalu agresif.
Gejala khas:
- Lonjakan trafik internal tanpa kenaikan trafik pengguna.
- Queue depth naik, CPU worker naik, tetapi success rate turun.
- Database atau API downstream penuh request identik.
Pencegahan:
- Gunakan exponential backoff dan jitter.
- Bedakan retryable error dan permanent error.
- Tambahkan circuit breaker atau pembatas concurrency pada consumer.
- Batasi total retry per job.
Retry tanpa jitter membuat worker bangun bersamaan dan kembali menghantam dependency pada saat yang sama. Ini salah satu pola klasik yang membuat outage kecil berubah menjadi outage besar.
6. Perbedaan state antar node
Jika node A menganggap order sudah lunas sementara node B belum, sumber masalah biasanya ada di cache lokal, replikasi data, event yang terlambat, atau lock yang tidak benar-benar terdistribusi. Pada sistem multi-node, konsistensi bukan asumsi default.
Penyebab umum:
- Read replica lag sehingga node membaca state lama.
- Cache per node tanpa invalidation broadcast yang andal.
- Event processing tidak berurutan atau terlambat.
- Clock drift memengaruhi TTL, lease, atau penentuan timeout.
Pertanyaan operasional yang penting bukan “kenapa satu node salah”, tetapi “apa sumber kebenaran final, dan kapan node lain dianggap boleh berbeda?”.
Pola diagnosis: mulai dari gejala, bukan dari asumsi
Pada sistem tua, asumsi dokumentasi sering tertinggal dari implementasi aktual. Karena itu diagnosis harus dimulai dari gejala yang terukur.
Langkah 1: petakan lifecycle job
Tulis alur minimum:
- Producer membuat job.
- Job masuk queue.
- Worker mengambil job.
- Worker melakukan side effect.
- Worker menyimpan state hasil.
- Worker mengirim acknowledgement.
- Cache di-update atau di-invalidasi.
Pada setiap langkah, tanyakan: jika proses mati di sini, apa yang terjadi? Teknik ini sering langsung mengungkap area duplicate, stale cache, atau lock orphan.
Langkah 2: korelasikan ID lintas komponen
Minimal, setiap job sebaiknya punya:
- message ID dari broker,
- business ID seperti orderId atau invoiceId,
- attempt number,
- worker ID atau hostname,
- trace atau correlation ID jika ada.
Tanpa korelasi ini, Anda hanya akan melihat log acak yang sulit disatukan saat incident.
Langkah 3: ukur durasi nyata, bukan asumsi timeout
Banyak masalah queue berasal dari timeout yang ditetapkan berdasarkan dugaan lama proses, bukan distribusi nyata. Kumpulkan:
- p50/p95/p99 waktu proses job,
- waktu tunggu di queue,
- durasi lock dipegang,
- waktu respons dependency eksternal.
Jika p99 processing time sering melebihi visibility timeout, duplicate delivery hampir pasti terjadi cepat atau lambat.
Langkah 4: bedakan transient failure dan deterministic failure
Jika job yang sama gagal 10 kali dengan error identik pada payload identik, itu bukan kandidat retry tanpa batas. Itu sinyal bahwa payload, code path, atau data sumber harus diperbaiki dulu.
Metrik yang wajib dipantau
Banyak tim hanya memantau panjang queue. Itu belum cukup. Queue depth tinggi bisa berarti producer sehat dan consumer lambat, tetapi juga bisa berarti consumer macet oleh lock atau menunggu dependency.
Metrik queue dan worker
- Queue depth: jumlah item menunggu diproses.
- Queue age / oldest message age: umur pesan tertua, sangat penting untuk mendeteksi backlog nyata.
- Throughput: job masuk per menit dan job selesai per menit.
- Success rate vs failure rate: per jenis job, bukan agregat total saja.
- Retry rate: lonjakan kecil sering lebih informatif daripada lonjakan failure total.
- Duplicate processing rate: jika bisa diukur dari idempotency hit atau constraint conflict.
- Worker concurrency: berapa job aktif per worker dan total cluster.
- Worker restart/crash count: restart sering dapat menyebabkan duplicate dan lock orphan.
Metrik lock
- Jumlah lock aktif per resource type.
- Lock wait time: waktu menunggu lock.
- Lock acquisition failure rate.
- Lock duration: distribusi waktu lock dipegang.
- Expired lock vs explicit release: jika terlalu banyak expired lock, mungkin worker tidak sehat.
Metrik cache
- Hit ratio, tetapi jangan hanya mengejar angka tinggi.
- Stale read indicators: mismatch antara cache dan sumber data saat sampling.
- Eviction rate: eviction tinggi bisa membuat pola performa tidak stabil.
- Key cardinality: lonjakan mendadak bisa menandakan key design buruk.
- Invalidation lag: jika memakai event invalidation.
Metrik consistency
- Replication lag jika memakai replica database.
- Event lag: selisih waktu event dibuat dan diproses.
- Read-after-write failure rate untuk endpoint penting.
- State divergence rate: sampel resource yang berbeda antar node atau antar storage.
Trade-off Redis lock vs database lock
Tidak ada lock yang “paling benar” untuk semua kasus. Pilihan harus mengikuti apa yang ingin dilindungi, seberapa kuat konsistensi yang dibutuhkan, dan apa failure mode yang masih bisa diterima.
Kapan Redis lock cocok
- Butuh lock cepat dengan latensi rendah.
- Tujuannya mengurangi kerja ganda, bukan menjamin serialisasi mutlak.
- Resource yang dilindungi berada di luar database utama atau tersebar di banyak worker.
- Anda siap menerima bahwa lock bersifat lease dengan TTL, bukan kepemilikan abadi.
Kelebihan Redis lock adalah sederhana dan cepat. Kekurangannya, ia bergantung pada expiry, kondisi jaringan, dan implementasi ownership yang benar. Jika dipakai ceroboh, lock bisa hilang terlalu cepat atau justru tertinggal.
Kapan database lock lebih cocok
- State yang dilindungi memang berada di database yang sama.
- Anda butuh konsistensi kuat pada transaksi inti.
- Operasi dapat diringkas dalam transaksi pendek dan jelas.
- Anda ingin mengandalkan constraint, row lock, atau unique index sebagai pagar terakhir.
Kelebihan database lock adalah kedekatannya dengan sumber kebenaran data. Kekurangannya, lock ini bisa membebani database, memicu blocking, dan tidak cocok untuk pekerjaan panjang di luar transaksi.
Aturan praktis memilih
- Jika Anda hanya ingin mencegah worker memproses resource yang sama secara bersamaan selama beberapa detik, Redis lock sering cukup.
- Jika Anda ingin menjamin satu perubahan state bisnis terjadi tepat satu kali pada data inti, utamakan constraint database + idempotency, bukan lock terdistribusi semata.
- Jangan menahan database transaction lock sambil memanggil API eksternal. Itu resep klasik untuk contention.
Visibility timeout, TTL, dan hubungan yang sering salah konfigurasi
Banyak insiden duplicate dan lock orphan berakar pada tiga timer yang tidak diselaraskan:
- Visibility timeout queue: berapa lama pesan dianggap “sedang diproses” sebelum terlihat lagi.
- Lock TTL: berapa lama lock dianggap valid.
- Cache TTL: berapa lama data boleh dianggap segar.
Visibility timeout
Atur berdasarkan durasi proses nyata, terutama p99, plus ruang aman untuk variasi jaringan dan dependency. Jika terlalu pendek, job yang belum selesai akan diambil worker lain. Jika terlalu panjang, recovery dari worker yang benar-benar mati menjadi lambat.
Lock TTL
TTL harus lebih panjang dari durasi kerja normal yang dilindungi, tetapi tidak terlalu panjang hingga lock orphan menghambat sistem lama sekali. Jika job bisa berjalan lebih lama dari perkiraan, pertimbangkan mekanisme heartbeat atau lease renewal yang terkontrol.
Cache TTL
TTL cache harus mengikuti toleransi stale data bisnis, bukan sekadar target hit ratio. Data profil pengguna mungkin aman stale beberapa menit; status pembayaran biasanya tidak.
Kesalahan yang sering terjadi: visibility timeout 30 detik, lock TTL 15 detik, dan job rata-rata butuh 45 detik saat downstream lambat. Kombinasi ini hampir pasti melahirkan duplicate processing dan race condition.
Idempotency sebagai pagar utama, bukan fitur tambahan
Pada sistem queue, idempotency sering lebih penting daripada lock. Lock mencegah kerja paralel pada waktu yang sama. Idempotency mencegah side effect ganda meski pesan diproses ulang pada waktu berbeda.
Tempat menerapkan idempotency
- Consumer job: sebelum menulis state atau memanggil dependency eksternal.
- API eksternal: gunakan idempotency key jika penyedia mendukung.
- Database: unique constraint untuk operasi yang seharusnya unik.
- Outbox/inbox table: simpan event yang sudah diproses.
Pola yang kuat biasanya memadukan beberapa lapis. Misalnya, consumer memeriksa tabel processed_messages dan database juga memiliki unique constraint pada kombinasi field bisnis penting.
-- Contoh sederhana pagar idempotency di database
CREATE TABLE processed_messages (
consumer_name VARCHAR(100) NOT NULL,
message_id VARCHAR(200) NOT NULL,
processed_at TIMESTAMP NOT NULL,
PRIMARY KEY (consumer_name, message_id)
);Pendekatan ini tidak otomatis menyelesaikan semua masalah, tetapi sangat membantu membedakan mana duplicate delivery yang aman dan mana duplicate side effect yang berbahaya.
Kapan memilih eventual consistency
Tidak semua perbedaan state harus dihapus dengan konsistensi kuat. Pada sistem yang menua, memaksakan strong consistency di semua jalur sering menambah coupling, latency, dan risiko lock contention.
Eventual consistency masuk akal jika:
- Perbedaan sementara antar node atau antar tampilan data masih dapat diterima bisnis.
- Ada mekanisme rekonsiliasi yang jelas.
- Operasi bersifat asynchronous secara alami, misalnya notifikasi, indexing, analytics, atau cache refresh.
- Anda dapat menjelaskan SLA konsistensi, misalnya “status bisa terlambat hingga beberapa detik”.
Jangan pilih eventual consistency untuk menutupi proses yang sebenarnya memerlukan atomicity kuat, seperti pembukuan inti, pemotongan saldo kritis, atau perubahan legal state yang tidak boleh ambigu.
Prinsip praktisnya: pilih strong consistency untuk source of truth bisnis, dan eventual consistency untuk proyeksi, cache, pencarian, notifikasi, serta sinkronisasi turunan.
Checklist incident response saat queue atau cache mulai berperilaku aneh
Saat terjadi job dobel atau retry storm
- Periksa apakah failure rate naik karena satu dependency tertentu.
- Bandingkan visibility timeout dengan durasi proses p95/p99 aktual.
- Turunkan concurrency consumer jika downstream sedang kolaps.
- Nonaktifkan replay massal otomatis dari DLQ.
- Aktifkan atau perketat idempotency guard pada jalur paling kritis.
- Pastikan retry memakai backoff dan jitter.
Saat cache stale menyebabkan hasil berbeda
- Identifikasi sumber kebenaran final untuk data tersebut.
- Uji apakah semua jalur tulis melakukan invalidation yang sama.
- Periksa apakah ada cache lokal per node yang belum dibersihkan.
- Kurangi TTL sementara hanya jika Anda siap dengan beban tambahan ke sumber data.
- Lakukan sampling mismatch antara cache dan database untuk mengukur luas masalah.
Saat lock orphan menghambat pekerjaan
- Inventarisasi key lock yang umurnya melebihi durasi normal proses.
- Periksa worker pemilik lock: masih hidup, hang, atau restart?
- Validasi apakah release lock memeriksa token pemilik.
- Jika perlu hapus lock manual, pastikan tidak ada worker aktif yang masih sah memegangnya.
- Tinjau ulang TTL dan pertimbangkan heartbeat jika durasi kerja sangat bervariasi.
Saat DLQ menumpuk
- Kelompokkan berdasarkan tipe error, payload shape, dan versi producer/consumer.
- Pisahkan permanent failure dari transient failure.
- Replay bertahap dengan sampling, jangan sekaligus.
- Jika side effect eksternal terlibat, verifikasi idempotency sebelum replay.
Pola implementasi yang biasanya paling tahan di sistem nyata
- Constraint database + idempotency key untuk operasi bisnis penting.
- Queue at-least-once + consumer idempotent alih-alih mengejar exactly-once yang rapuh.
- Cache-aside dengan invalidation eksplisit untuk data baca tinggi.
- Redis lock berbasis token + TTL hanya untuk mengurangi contention, bukan sebagai satu-satunya jaminan integritas data.
- DLQ dengan klasifikasi penyebab, bukan hanya penampungan pesan gagal.
- Observability berbasis correlation ID agar lintas queue, worker, cache, dan database bisa ditelusuri cepat.
Kesalahan desain yang paling sering memperburuk sistem tua
- Menambah retry tanpa batas untuk semua error.
- Menggunakan lock untuk menutupi operasi yang sebenarnya butuh unique constraint atau transaksi.
- Menganggap cache invalidation dapat diserahkan pada “nanti dibersihkan cron”.
- Menghapus lock secara paksa tanpa mengecek ownership.
- Menganggap duplicate delivery sebagai bug broker, bukan sifat sistem distributed.
- Memakai eventual consistency tanpa mendefinisikan batas keterlambatan yang bisa diterima.
Penutup
Failure mode queue dan cache pada sistem yang menua hampir selalu terkait dengan state yang tersebar, timer yang tidak selaras, dan asumsi lama yang tak lagi benar. Solusi yang paling efektif biasanya bukan menambah satu komponen baru, melainkan memperjelas kontrak operasional: kapan job boleh diulang, di mana idempotency ditegakkan, apa sumber kebenaran final, berapa TTL yang realistis, dan kapan sistem boleh berbeda state untuk sementara.
Jika harus memilih prioritas, mulailah dari empat hal: ukur lifecycle job secara nyata, pasang idempotency pada operasi bisnis penting, selaraskan visibility timeout dengan durasi proses aktual, dan audit semua jalur invalidation cache. Pada sistem kompleks, disiplin pada empat area ini sering memberi dampak lebih besar daripada migrasi besar yang mahal tetapi tidak menyentuh failure mode inti.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!