Kodeloka
  1. Kodeloka
  2. Kelas Apache Kafka
  3. Materi

6 · Pola Produksi · Modul 13 dari 13

Outbox, Dead Letter Queue & Konsumen Idempoten

Tiga pola ini menyelesaikan hampir semua masalah nyata yang muncul saat Kafka dipakai di antara layanan dan basis data.

Masalah Dual Write

Layanan menyimpan pesanan ke basis data, lalu menerbitkan peristiwa ke Kafka. Dua sistem, dua penulisan, tanpa transaksi bersama.

simpanKeBasisData(pesanan)   ← berhasil
terbitKeKafka(peristiwa)     ← proses mati di sini

Hasil: pesanan ada di basis data, tetapi tidak ada yang pernah tahu.
Layanan pengiriman tidak pernah menerima peristiwanya.
🚫 Membalik urutannya tidak menyelesaikan apa pun

Menerbitkan ke Kafka lebih dulu menghasilkan kegagalan yang berlawanan tetapi sama buruknya: peristiwa tersebar dan diproses hilir, sementara transaksi basis data gagal — seluruh sistem kini yakin akan pesanan yang tidak pernah ada. Tidak ada urutan yang benar; masalahnya struktural.

Pola Outbox

Tulis peristiwa ke tabel outbox di dalam transaksi basis data yang sama dengan perubahan data. Proses terpisah kemudian mengirimkannya ke Kafka.

BEGIN;
  INSERT INTO pesanan (id, total, status) VALUES (123, 250000, 'dibuat');
  INSERT INTO outbox (id_peristiwa, agregat, kunci, payload)
  VALUES (gen_random_uuid(), 'pesanan', '123', '{...}');
COMMIT;          ← atomik: keduanya jadi, atau keduanya batal

Relay (Debezium CDC pada tabel outbox, atau poller terjadwal)
  ↳ baca baris outbox → kirim ke Kafka → tandai terkirim
Cara relayKelebihanKekurangan
Debezium CDC pada tabel outboxLatensi rendah, tanpa pollingPerlu menjalankan Connect dan Debezium
Poller terjadwalSederhana, tanpa infrastruktur baruLatensi sebesar interval polling; beban kueri berkala
💡 Outbox memberi at-least-once, bukan exactly-once

Relay bisa mati setelah mengirim ke Kafka tetapi sebelum menandai baris terkirim, sehingga peristiwa itu dikirim ulang. Itu sebabnya id_peristiwa disertakan sejak awal — konsumen memakainya untuk deduplikasi.

Konsumen Idempoten

Karena Kafka pada praktiknya memberi at-least-once, pemrosesan harus aman diulang. Ada tiga cara, dari yang paling disukai:

1. Operasi yang secara alami idempoten
   UPDATE pesanan SET status='dibayar' WHERE id=123;
   → dijalankan sepuluh kali, hasilnya sama

2. Upsert dengan kunci alami
   INSERT INTO ringkasan (id, total) VALUES (123, 250000)
   ON CONFLICT (id) DO UPDATE SET total = EXCLUDED.total;

3. Tabel deduplikasi eksplisit
   INSERT INTO peristiwa_terproses (id_peristiwa) VALUES ('evt-abc')
   ON CONFLICT DO NOTHING RETURNING id_peristiwa;
   → tidak ada baris kembali berarti sudah pernah diproses, lewati
⚠️ Tabel deduplikasi butuh kebijakan pembersihan

Tabel peristiwa_terproses tumbuh selamanya bila dibiarkan. Simpan hanya jendela waktu yang lebih panjang daripada retensi topik ditambah kemungkinan replay — misalnya 30 hari — dan hapus sisanya secara terjadwal. Beri indeks pada kolom waktu agar pembersihan tidak menjadi kueri berat.

🚫 Operasi penambahan tidak idempoten

UPDATE saldo SET jumlah = jumlah + 50000 menghasilkan hasil berbeda setiap kali diulang. Operasi akumulatif seperti ini wajib dilindungi kunci deduplikasi — inilah kategori bug yang berakhir sebagai selisih uang, dan biasanya baru ditemukan saat rekonsiliasi.

Retry dan Dead Letter Queue

Kafka tidak punya redelivery per pesan. Bila satu pesan selalu gagal, konsumen yang mengulanginya terus akan memblokir seluruh partisi.

pesan-1 ✓   pesan-2 ✗ gagal terus   pesan-3 …  pesan-4 …
                    ▲
        seluruh partisi berhenti di sini selamanya

Bedakan dua jenis kegagalan:

JenisContohPenanganan
SementaraBasis data timeout, layanan hilir 503Retry dengan backoff — pesan berikutnya memang harus menunggu
PermanenPesan rusak, skema tidak valid, id tidak adaKirim ke DLQ dan lanjut — retry tidak akan pernah berhasil
Pola topik retry berjenjang:

  pesanan  →  gagal  →  pesanan.retry.5m
                     →  pesanan.retry.30m
                     →  pesanan.dlq   ← perlu campur tangan manusia

Tiap topik retry dikonsumsi oleh worker yang menunda sesuai jenjangnya,
sehingga topik utama tidak pernah tertahan.
⚠️ DLQ yang tidak dipantau sama saja dengan menghapus pesan

Pesan yang masuk DLQ butuh manusia yang melihatnya. Pasang peringatan pada jumlah pesan masuk DLQ, sertakan header diagnostik — pesan galat, jumlah percobaan, topik dan offset asal — dan sediakan cara memasukkan kembali pesan yang sudah diperbaiki.

Daftar Periksa Sebelum Produksi

  • acks=all, enable.idempotence=true, dan callback galat pada semua produser
  • replication.factor=3 dengan min.insync.replicas=2 pada semua topik penting
  • Kunci pesan dipilih sesuai entitas yang urutannya wajib terjaga
  • Auto-commit dimatikan; offset di-commit setelah pemrosesan selesai
  • Pemrosesan idempoten atau dilindungi kunci deduplikasi
  • DLQ tersedia, terpantau, dan bisa diputar ulang
  • Skema terdaftar dengan pemeriksaan kompatibilitas di CI
  • Peringatan untuk lag, under-replicated partitions, dan pemakaian disk
  • auto.create.topics.enable=false, topik dikelola sebagai kode
  • Prosedur shutdown yang memanggil flush pada produser dan menutup konsumen dengan rapi