Failover queue worker saat sistem komunikasi internal terputus bukan sekadar soal memindahkan proses ke node lain. Masalah utamanya adalah bagaimana menjaga eksekusi job tetap aman ketika layanan masih hidup, tetapi kanal koordinasi seperti service discovery, Redis, message broker, cache, atau jaringan internal hanya gagal sebagian. Pada kondisi ini, sistem bisa terlihat "normal" dari luar, namun di dalam terjadi job menumpuk, worker zombie, lock tidak lepas, cache stale, hingga duplikasi eksekusi.
Konteks ini mirip dengan pelajaran dari insiden gangguan komunikasi kereta di Jerman: ketika kanal komunikasi internal terganggu, operasi inti bisa berhenti walau komponen fisik atau proses utama belum benar-benar rusak. Untuk sistem terdistribusi, pelajarannya jelas: internal communication adalah dependency kritis. Artikel ini membahas bagaimana merancang queue worker yang tetap aman saat koordinasi antar komponen putus sebagian, bukan dengan asumsi jaringan selalu sehat.
Sumber konteks insiden: AP News melaporkan gangguan komunikasi radio internal yang menyebabkan perjalanan kereta di Jerman terganggu. Di dunia perangkat lunak terdistribusi, kegagalan kanal koordinasi yang serupa dapat melumpuhkan orkestrasi job walau compute node masih aktif.
Masalah yang Sebenarnya Terjadi Saat Komunikasi Internal Putus Sebagian
Kegagalan parsial lebih berbahaya daripada mati total karena sistem masuk ke area abu-abu. Sebagian worker masih bisa membaca queue, sebagian gagal memperbarui lock, sebagian lagi masih bisa menulis hasil tetapi tidak bisa mengirim heartbeat. Akibatnya, beberapa gejala operasional muncul bersamaan.
1. Job menumpuk
Queue depth naik karena worker tidak bisa mengambil job baru, terlalu lama menunggu dependency, atau terus melakukan retry pada operasi yang sebenarnya tidak mungkin berhasil selama gangguan berlangsung.
2. Worker zombie
Worker masih berjalan di tingkat proses atau container, tetapi kehilangan kemampuan koordinasi. Dari sisi supervisor, proses terlihat hidup. Dari sisi sistem, worker tidak lagi aman untuk dipercaya karena tidak dapat memperbarui lease, mengakui job, atau menyinkronkan status.
3. Lock tidak lepas
Jika locking bergantung pada cache/distributed store dan worker mati setelah mengambil lock, lock bisa tertinggal hingga timeout. Jika timeout terlalu panjang, throughput turun. Jika terlalu pendek, lock bisa direbut worker lain saat job lama belum selesai.
4. Cache stale
Cache yang dipakai sebagai status koordinasi dapat tetap berisi data lama. Worker kemudian membuat keputusan dari state yang tidak lagi valid, misalnya menganggap node lain sudah berhenti padahal masih memproses job.
5. Duplikasi eksekusi
Saat acknowledgment ke broker gagal, worker mungkin sudah menyelesaikan job tetapi broker menganggap job belum selesai. Job pun dikirim ulang ke worker lain. Tanpa idempotensi, efek samping seperti pembayaran ganda, email ganda, atau update inventori ganda bisa terjadi.
6. Split-brain ringan
Ini bukan split-brain penuh seperti pada cluster database, tetapi cukup untuk berbahaya: dua worker atau dua grup worker sama-sama yakin berhak memproses kelas job yang sama karena view terhadap lock, membership, atau health status tidak konsisten.
Prinsip Desain: Jangan Mengandalkan Koordinasi Sempurna
Desain queue worker yang tahan gangguan berangkat dari asumsi berikut:
- Jaringan internal bisa lambat, putus sebagian, atau asimetris.
- Queue, cache, lock store, dan database bisa punya pandangan state yang tidak sama untuk sementara waktu.
- Worker bisa selesai mengerjakan job tetapi gagal melaporkan hasil.
- Retry pasti terjadi, termasuk retry yang tidak Anda inginkan.
Karena itu, targetnya bukan membuat sistem "tidak pernah gagal", melainkan membuat kegagalan tetap terbatas, terdeteksi, dan dapat dipulihkan.
Pisahkan data-plane dan control-plane
Dalam praktik queue worker, data-plane adalah pemrosesan job itu sendiri, sedangkan control-plane mencakup lease, heartbeat, health check, rate control, dan koordinasi failover. Saat control-plane terganggu, worker sebaiknya masuk ke mode aman: berhenti mengambil job baru, menyelesaikan job yang masih aman, lalu melepaskan kepemilikan jika tidak bisa memperbarui lease.
Gunakan at-least-once dengan idempotensi, bukan berharap exactly-once
Pada sistem terdistribusi umum, exactly-once end-to-end sulit dicapai tanpa biaya dan kompleksitas tinggi. Pendekatan yang lebih realistis adalah menerima kemungkinan duplikasi, lalu memastikan handler job bersifat idempotent.
Lease lebih aman daripada lock permanen
Lock yang tidak punya masa berlaku sangat rentan saat terjadi crash atau partisi jaringan. Lease memberi batas waktu eksplisit: worker harus memperbarui kepemilikannya secara periodik. Jika gagal, kepemilikan dianggap hangus.
Arsitektur Queue Worker yang Lebih Tahan Gangguan
1. Queue sebagai sumber distribusi kerja, bukan sumber kebenaran bisnis
Queue mendistribusikan unit kerja, tetapi status final bisnis sebaiknya disimpan di penyimpanan yang lebih kuat dan transaksional, misalnya database utama. Ini penting karena broker bisa melakukan redelivery, reorder, atau delay.
Contoh: job kirim invoice boleh masuk queue berkali-kali, tetapi tabel invoice_delivery harus menyimpan apakah invoice tertentu sudah benar-benar dikirim atau belum.
2. Worker dengan state machine sederhana
Alih-alih worker yang hanya loop ambil-job-lalu-proses, lebih aman memakai state eksplisit:
- READY: boleh mengambil job baru.
- PROCESSING: sedang menjalankan job.
- DRAINING: berhenti mengambil job baru, menyelesaikan yang sedang berjalan jika masih aman.
- FENCED: kehilangan lease atau status kepemilikan; tidak boleh commit hasil yang memerlukan kepemilikan aktif.
- STOPPED: keluar atau menunggu restart.
State ini membantu mencegah worker zombie tetap memproses setelah kehilangan hak koordinasi.
3. Lock berbasis lease dengan fencing token
Lease saja belum cukup. Jika worker A lambat lalu lease-nya kedaluwarsa dan worker B mengambil alih, worker A mungkin masih lanjut menulis hasil. Untuk mencegah ini, tambahkan fencing token: setiap lease baru mendapat nomor monoton meningkat. Operasi ke storage hilir harus menolak token yang lebih tua.
Ini sangat berguna pada skenario split-brain ringan.
4. Timeout bertingkat
Jangan gunakan satu timeout besar untuk semua hal. Pisahkan:
- Queue poll timeout: seberapa lama worker menunggu job.
- Dependency timeout: timeout untuk database, API internal, cache, atau lock store.
- Lease renew interval: seberapa sering lease diperbarui.
- Hard job timeout: batas maksimum total durasi job.
- Shutdown drain timeout: batas waktu sebelum worker dipaksa berhenti saat draining.
Timeout bertingkat membantu sistem lebih cepat mendeteksi gangguan tanpa membuat false positive berlebihan.
5. Retry dengan klasifikasi error
Tidak semua error pantas di-retry dengan cara yang sama.
- Transient error: timeout jaringan, koneksi terputus, 5xx internal. Cocok untuk retry dengan backoff dan jitter.
- Persistent error: data invalid, referensi tidak ditemukan, pelanggaran aturan bisnis. Jangan di-retry berkali-kali.
- Coordination error: lease hilang, lock store tak terjangkau, heartbeat gagal. Umumnya worker harus berhenti mengambil job baru atau masuk mode draining.
6. Backpressure agar antrian tidak menghancurkan dependency
Saat komunikasi internal putus sebagian, refleks umum adalah menambah jumlah worker. Ini sering memperburuk situasi karena semua worker melakukan retry bersamaan ke dependency yang sama. Backpressure diperlukan agar sistem melambat secara terkontrol, bukan kolaps.
Bentuk backpressure yang lazim:
- Mengurangi concurrency secara dinamis.
- Menghentikan konsumsi queue sementara saat error rate melewati ambang.
- Mengaktifkan circuit breaker pada dependency yang gagal.
- Memindahkan job ke delayed queue atau dead-letter queue setelah sejumlah percobaan.
Pseudocode: Failover Worker yang Aman
Contoh berikut menunjukkan alur worker dengan lease, heartbeat, dan mode draining. Ini pseudocode generik agar mudah diadaptasi ke berbagai stack.
state = READY
worker_id = random_id()
lease = null
loop:
if state == READY:
lease = acquire_or_renew_worker_lease(worker_id)
if lease == null:
sleep(short_backoff())
continue
job = queue.poll(timeout=poll_timeout)
if job == null:
continue
state = PROCESSING
start_heartbeat_thread(lease)
result = process_job_with_guard(job, lease)
stop_heartbeat_thread()
if result == SUCCESS:
queue.ack(job)
state = READY
elif result == RETRYABLE_ERROR:
queue.retry_later(job, backoff_with_jitter(job.attempts))
state = READY
elif result == LOST_LEASE:
queue.do_not_ack(job)
state = DRAINING
else:
queue.move_to_dead_letter(job)
state = READY
elif state == DRAINING:
release_best_effort(lease)
if coordination_is_healthy() == false:
sleep(longer_backoff())
else:
state = READYPoin penting dari alur di atas:
- Worker tidak menganggap dirinya sehat hanya karena proses masih hidup.
- Jika lease hilang saat memproses job, worker tidak boleh sembarang mengakui job selesai.
- Mode draining mencegah worker zombie mengambil kerja tambahan saat kontrol sistem tidak jelas.
Pseudocode: Lease-Based Locking dengan Fencing Token
Di bawah ini contoh sederhana untuk lease dengan token monoton. Implementasi nyatanya bisa memakai store yang mendukung operasi atomik.
function acquire_lease(resource_key, owner_id, ttl):
current = store.get(resource_key)
if current == null or current.expires_at < now():
next_token = store.increment(resource_key + ":token")
new_lease = {
owner_id: owner_id,
token: next_token,
expires_at: now() + ttl
}
if store.compare_and_set(resource_key, current, new_lease):
return new_lease
return null
function renew_lease(resource_key, owner_id, token, ttl):
current = store.get(resource_key)
if current.owner_id != owner_id:
return LOST_LEASE
if current.token != token:
return LOST_LEASE
current.expires_at = now() + ttl
if store.compare_and_set(resource_key, current, current):
return OK
return LOST_LEASEKetika worker menulis ke database atau storage hilir, sertakan token tersebut. Storage atau lapisan aplikasi harus menolak penulisan dari token lama. Itu yang membuat failover lebih aman daripada lock biasa.
Idempotensi: Pertahanan Utama terhadap Duplikasi
Jika ada satu hal yang wajib ada pada sistem queue modern, itu adalah idempotensi. Tanpanya, gangguan komunikasi internal hampir pasti berubah menjadi korupsi state bisnis.
Teknik implementasi idempotensi
- Idempotency key: setiap job punya kunci unik berdasarkan identitas bisnis, bukan hanya ID pesan broker.
- Status table: simpan status pemrosesan per key, misalnya
pending,completed,failed. - Unique constraint: gunakan batasan unik di database untuk mencegah efek samping ganda.
- Outbox/inbox pattern: simpan event keluar atau masuk secara transaksional agar publish/consume lebih dapat dipulihkan.
Kesalahan umum
- Menganggap
ackbroker berarti efek samping sudah persis sekali terjadi. - Menyimpan idempotency key hanya di cache volatile.
- Menggunakan UUID acak per percobaan, bukan per operasi bisnis.
Contoh yang lebih aman: untuk job pembuatan invoice, key-nya bisa berupa invoice:{order_id}, bukan job_run_uuid.
Cache dan Lock Store: Gunakan Secukupnya, Jangan Jadi Sumber Kebenaran Tunggal
Cache cepat, tetapi bukan tempat terbaik untuk menyimpan kebenaran akhir status bisnis. Saat gangguan komunikasi internal terjadi, cache sering menjadi titik kegagalan yang paling membingungkan: worker melihat state lama, lock belum hilang, atau node tertentu tampak sehat padahal tidak.
Kapan cache cocok dipakai
- Lease jangka pendek.
- Rate limiting dan counter sementara.
- Deduplication window pendek.
- Metadata non-kritis yang bisa dibangun ulang.
Kapan jangan bergantung pada cache saja
- Status akhir job yang memengaruhi bisnis.
- Riwayat audit.
- Keputusan final apakah efek samping sudah terjadi.
Mitigasi cache stale
- Pakai TTL yang eksplisit dan realistis.
- Jangan menyamakan cache hit dengan truth.
- Untuk operasi kritis, verifikasi ke storage yang lebih kuat.
- Monitor rasio stale read jika bisa diukur dari versi state atau token.
Backpressure dan Degradasi Terkendali
Saat koordinasi antar layanan putus sebagian, tujuan pertama bukan throughput maksimum, melainkan mencegah eskalasi insiden. Sistem yang baik tahu kapan harus melambat.
Pola mitigasi yang efektif
- Pause consumption: hentikan konsumsi sementara jika lease store atau dependency utama tidak sehat.
- Adaptive concurrency: turunkan jumlah job paralel ketika timeout atau retry rate meningkat.
- Exponential backoff dengan jitter: hindari retry serempak yang memicu thundering herd.
- Circuit breaker: untuk dependency internal yang gagal berulang, buka circuit dan arahkan job ke retry tertunda.
- Priority queue: pastikan job kritis tidak tenggelam oleh backlog job non-kritis.
Trade-off yang perlu dipahami
Backpressure mengorbankan latensi jangka pendek demi kestabilan sistem. Ini keputusan yang tepat untuk kebanyakan sistem internal. Memaksa semua worker tetap agresif hanya mempercepat penumpukan error dan memperbesar backlog pemulihan.
Konsistensi Saat Kanal Koordinasi Putus Sebagian
Pada gangguan parsial, Anda jarang mendapat konsistensi kuat di semua komponen sekaligus. Yang lebih realistis adalah menentukan batas konsistensi minimum agar sistem tetap aman.
Strategi praktis
- Single writer per resource menggunakan lease + fencing token.
- Idempotent side effect untuk operasi yang bisa terulang.
- Reconciliation job berkala untuk menyamakan queue, cache, dan database.
- Monotonic state transition: status job tidak boleh mundur semaunya, misalnya dari
completedkembali keprocessing.
Reconciliation penting setelah gangguan
Setelah komunikasi pulih, backlog mungkin sudah diproses sebagian, diulang sebagian, dan gagal sebagian. Job rekonsiliasi diperlukan untuk mencari:
- job yang selesai tapi belum di-ack,
- job yang di-ack tapi efek samping belum lengkap,
- lock yatim,
- cache state yang tidak sesuai dengan database.
Metrik dan Alert yang Harus Dipasang
Banyak tim hanya memantau CPU, memori, dan queue depth. Untuk skenario failover queue worker saat sistem komunikasi internal terputus, itu tidak cukup. Anda perlu observability yang membedakan masalah compute dari masalah koordinasi.
Metrik inti
- Queue depth per jenis job.
- Job age: umur job tertua di queue.
- Processing latency dan end-to-end latency.
- Retry rate dan dead-letter rate.
- Lease renewal failure rate.
- Heartbeat gap per worker.
- Duplicate execution count berdasarkan idempotency key.
- Lock contention dan orphan lock count.
- Cache stale ratio atau mismatch rate dengan source of truth.
- Dependency timeout rate per layanan internal.
Contoh alert yang berguna
- Queue depth naik tajam dan lease renewal failure ikut naik.
- Job age melewati SLA walau jumlah worker terlihat sehat.
- Satu resource diproses oleh lebih dari satu worker dalam jendela waktu pendek.
- Retry rate meningkat tetapi ack rate turun.
- Worker heartbeat ada, tetapi tidak ada progress job selama periode tertentu.
Kombinasi sinyal ini lebih berguna daripada alert tunggal yang hanya berbasis backlog.
Checklist Incident Response
Saat insiden terjadi, tim sering langsung menambah replika worker atau merestart semua pod. Itu bisa benar, tetapi juga bisa memperburuk duplikasi dan lock contention. Gunakan checklist yang lebih disiplin.
Langkah respons awal
- Identifikasi apakah masalah ada di queue, lock store, cache, database, atau jaringan internal.
- Bekukan autoscaling agresif jika justru menambah retry storm.
- Aktifkan mode pause/drain untuk worker yang kehilangan lease atau heartbeat stabil.
- Cek apakah ada dead-letter spike, lease renewal failure, atau duplicate execution.
- Verifikasi dependency yang lambat: DNS internal, service discovery, cache, broker, database.
Jika ditemukan worker zombie
- Pastikan worker tidak lagi bisa commit hasil tanpa fencing token valid.
- Hentikan konsumsi job baru dari instance tersebut.
- Lepaskan lock secara best effort, tetapi jangan mengandalkan unlock manual sebagai satu-satunya pemulihan.
Jika lock tidak lepas
- Periksa TTL lease dan pola renew.
- Jangan langsung menghapus semua lock tanpa tahu owner dan token.
- Pastikan proses pengambil alihan memakai token yang lebih baru.
Setelah sistem stabil
- Jalankan reconciliation terhadap job yang sedang diproses saat insiden.
- Audit duplikasi berdasarkan idempotency key.
- Ukur backlog drain time dan cek job starvation pada queue prioritas rendah.
- Evaluasi timeout, TTL, dan kebijakan retry yang ternyata terlalu agresif atau terlalu longgar.
Kesalahan Desain yang Sering Menyebabkan Insiden Memburuk
- Menyamakan proses hidup dengan worker sehat.
- Menggunakan lock tanpa expiry atau tanpa fencing token.
- Menyimpan status akhir job hanya di broker atau cache.
- Retry tanpa batas pada error koordinasi.
- Menambah worker saat dependency utama sedang timeout.
- Tidak memiliki idempotency key berbasis operasi bisnis.
- Tidak memisahkan timeout untuk queue, database, lock store, dan shutdown.
Kapan Memilih Pendekatan yang Lebih Sederhana
Tidak semua sistem perlu lease canggih atau token fencing di semua tempat. Jika job Anda hanya membaca data, tidak punya efek samping penting, dan duplikasi tidak berbahaya, desain bisa lebih sederhana. Tetapi jika job memodifikasi state penting, mengirim instruksi ke sistem lain, atau memicu transaksi finansial, pendekatan defensif di atas hampir selalu layak.
Pilih kompleksitas secara proporsional:
- Sistem sederhana: retry terbatas, idempotensi dasar, timeout jelas.
- Sistem menengah: lease lock, worker draining, metrics koordinasi.
- Sistem kritis: fencing token, reconciliation terjadwal, adaptive backpressure, circuit breaker, audit lengkap.
Penutup
Pelajaran utama dari gangguan komunikasi internal, seperti yang terlihat pada konteks insiden kereta di Jerman, adalah bahwa operasi dapat berhenti bukan karena komputasi gagal total, melainkan karena koordinasi runtuh. Pada sistem backend modern, queue worker harus dirancang dengan asumsi bahwa komunikasi antar layanan bisa putus sebagian dan pulih secara tidak serempak.
Desain yang paling membantu bukan yang paling rumit, melainkan yang menjaga tiga hal: job aman untuk diulang, kepemilikan kerja punya batas waktu yang jelas, dan sistem tahu kapan harus melambat. Dengan lease-based locking, fencing token, idempotensi, backpressure, timeout yang tepat, serta observability yang fokus pada koordinasi, Anda bisa membuat failover queue worker yang jauh lebih tahan terhadap gangguan internal.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!