Semantik pengiriman menetapkan jaminan sebuah sistem pesan terhadap setiap kejadian, yaitu paling banyak sekali, paling sedikit sekali, atau tepat sekali. Topik ini membahas ketiganya beserta pengaturan klien yang menghasilkan masing-masing, menunjukkan asal mula duplikat lewat simulasi pengiriman ulang yang berjalan tanpa broker, dan menutupnya dengan penyaringan pesan ganda berbasis event_id di sisi konsumen. Setelah topik ini dibaca, kombinasi konfigurasi yang menghasilkan setiap semantik dapat disusun secara sadar, bukan sekadar mengikuti default.
Tiga semantik pengiriman#
Kafka menganggap sebuah pesan committed hanya setelah seluruh salinan di ISR menerapkannya ke log, dan pesan committed tidak hilang selama satu salinan tetap hidup. Jaminan kepada producer dan consumer dibangun di atas aturan itu. Bagian tersulitnya terletak pada kegagalan di tengah jalan, karena producer yang menerima error jaringan tidak dapat mengetahui apakah pesannya sudah committed atau belum.
| Semantik | Jaminan | Pengaturan klien utama |
|---|---|---|
| at-most-once | Pesan paling banyak sekali terkirim, boleh hilang | Producer: acks=0 atau pengiriman ulang dimatikan. Consumer: commit offset sebelum pemrosesan. |
| at-least-once | Pesan tidak hilang, boleh terkirim lebih dari sekali | Producer: acks=all dengan pengiriman ulang aktif. Consumer: commit offset setelah pemrosesan selesai. |
| exactly-once | Tepat sekali, tanpa duplikat dan tanpa hilang | Producer idempoten dan transaksi, consumer membaca dengan isolation.level=read_committed. |
Ketika producer gagal menerima konfirmasi, pilihannya sebelum Kafka 0.11 hanyalah mengirim ulang pesan yang sama. Pengiriman ulang menimbulkan kemungkinan pesan tercatat dua kali ketika pengiriman pertama ternyata berhasil, dan itulah semantik at-least-once: tidak ada yang hilang, tetapi duplikat mungkin muncul. Sejak 0.11, producer idempoten menghilangkan duplikat di log, sedangkan transaksi menaikkannya menjadi pemrosesan consume-transform-produce yang tepat sekali, seperti dibahas pada topik Transaksi di modul ini. Jaminan yang sampai ke sistem di luar Kafka, misalnya basis data tujuan, tetap membutuhkan langkah tambahan di sisi konsumen yang dibahas pada bagian akhir.
Asal mula duplikat pada pengiriman ulang#
Duplikat tidak menuntut kesalahan apa pun dari producer. Kasusnya muncul ketika pengiriman ternyata berhasil di broker, tetapi konfirmasinya hilang di jaringan, sehingga producer menganggap pengiriman gagal dan mengirim ulang pesan yang sama. Producer yang taat aturan pun tidak dapat menghindarinya, karena keputusan mengirim ulang diambil justru karena tidak menerima konfirmasi. Broker tidak memiliki cara membedakan ulangan itu dari pesan baru bila producer tidak membawa penanda urutan, lalu mencatatnya dua kali. Simulasi berikut memodelkan pengiriman tiga bacaan ke sebuah log, dengan konfirmasi bacaan kedua yang hilang, tanpa melibatkan broker Kafka sama sekali.
log_broker = [] # isi log partisi di sisi broker
def kirim(pesan, konfirmasi_hilang=False):
log_broker.append(pesan) # broker selalu mencatat pesan yang tiba
if konfirmasi_hilang:
return None # konfirmasi tidak pernah sampai ke producer
return "ok"
pengiriman = [
("bacaan-0", False),
("bacaan-1", True), # pengiriman berhasil, tetapi konfirmasinya hilang
("bacaan-2", False),
]
for pesan, konfirmasi_hilang in pengiriman:
hasil = kirim(pesan, konfirmasi_hilang)
if hasil is None:
kirim(pesan) # producer mengirim ulang karena tidak menerima konfirmasi
print(f"{pesan}: konfirmasi hilang, dikirim ulang")
else:
print(f"{pesan}: terkirim")
print(f"isi log: {log_broker}")
print(f"jumlah pesan di log: {len(log_broker)} dari 3 pesan yang dimaksudkan")bacaan-0: terkirim
bacaan-1: konfirmasi hilang, dikirim ulang
bacaan-2: terkirim
isi log: ['bacaan-0', 'bacaan-1', 'bacaan-1', 'bacaan-2']
jumlah pesan di log: 4 dari 3 pesan yang dimaksudkanLog berakhir dengan empat entri padahal producer hanya bermaksud mengirim tiga, karena bacaan-1 tercatat dua kali. Semua pihak bertindak sesuai aturan at-least-once: broker mencatat apa yang tiba, dan producer mengulang apa yang tidak dikonfirmasi. Aturan commit offset di sisi konsumen tidak mengubah keadaan ini, karena duplikat sudah menetap di log sebelum konsumen membacanya. Duplikat semacam ini lazim muncul di sistem produksi yang mengandalkan pengiriman ulang tanpa idempotence, dan jumlahnya bertambah seiring frekuensi kegagalan jaringan.
Menekan duplikat dengan pengiriman idempoten#
Mekanisme idempoten menggantikan asumsi bahwa setiap pesan adalah pesan baru. Producer membawa nomor urut yang terus berlanjut per partisi, dan broker hanya menerima nomor yang tepat satu di atas nomor terakhir yang sudah diterima. Ulangan dengan nomor yang sudah diterima ditolak tanpa dicatat, sehingga pengiriman ulang yang sama tidak menimbulkan salinan kedua. Karena penjagaannya ada di broker, aplikasi producer tidak perlu menulis logika pembilang sendiri. Simulasi berikut menggunakan skenario yang persis sama dengan sebelumnya, tetapi pesan kini membawa nomor urut.
log_broker = []
nomor_terakhir = -1 # nomor urut terakhir yang diterima broker dari producer ini
def kirim(nomor, pesan):
global nomor_terakhir
if nomor == nomor_terakhir + 1:
log_broker.append(pesan) # nomor urut berlanjut, pesan dicatat
nomor_terakhir = nomor
return "ok"
return "duplikat-ditolak" # nomor sudah pernah diterima, pesan ditolak
pengiriman = [
("bacaan-0", False),
("bacaan-1", True), # pengiriman berhasil, tetapi konfirmasinya hilang
("bacaan-2", False),
]
for nomor, (pesan, konfirmasi_hilang) in enumerate(pengiriman):
hasil = kirim(nomor, pesan)
if konfirmasi_hilang:
hasil = kirim(nomor, pesan) # producer mengirim ulang dengan nomor yang sama
print(f"{pesan}: konfirmasi hilang, dikirim ulang (hasil: {hasil})")
else:
print(f"{pesan}: terkirim (hasil: {hasil})")
print(f"isi log: {log_broker}")
print(f"jumlah pesan di log: {len(log_broker)} dari 3 pesan yang dimaksudkan")bacaan-0: terkirim (hasil: ok)
bacaan-1: konfirmasi hilang, dikirim ulang (hasil: duplikat-ditolak)
bacaan-2: terkirim (hasil: ok)
isi log: ['bacaan-0', 'bacaan-1', 'bacaan-2']
jumlah pesan di log: 3 dari 3 pesan yang dimaksudkanUlangan untuk bacaan-1 ditolak oleh penjaga nomor urut, sehingga log berakhir dengan tepat tiga entri. Nomor urut yang melompat ditolak dengan error urutan, sehingga pesan tidak pernah masuk ke log di luar urutan pengirimannya. Pada Kafka sungguhan, penjaga itu bekerja dengan pengenal producer dan nomor urut per partisi seperti dibahas pada topik Ketahanan producer, dan jaminannya berlaku di dalam log untuk satu producer. Pemrosesan yang tepat sekali sampai ke consumer membutuhkan lapisan berikutnya, yaitu transaksi yang membuat konsumsi dan produksi ulang terjadi secara atomik.
Dedupe di sisi konsumen dengan event_id#
Idempotence producer dan transaksi hanya menjamin isi log Kafka, sedangkan jaminan berakhir ketika hasil pemrosesan ditulis ke sistem lain seperti basis data atau mesin pencari. Konsumen yang commit offset setelah pemrosesan masih dapat memproses kejadian yang sama dua kali, misalnya ketika pemrosesan berhasil tetapi commit-nya gagal. Karena itu, pola yang paling tahan gangguan memadukan semantik at-least-once dengan dedupe, yaitu penyaringan kejadian yang sudah pernah diproses di sisi konsumen.
Setiap kejadian membawa event_id, yaitu pengenal unik yang ditetapkan di sumber sebelum pengiriman pertama dan tetap sama pada setiap pengiriman ulang. Konsumen menyimpan pengenal itu bersama hasil pemrosesannya dalam satu transaksi basis data, lalu memeriksa kehadirannya sebelum memproses. Kejadian yang sudah tercatat dilewati begitu saja, sehingga pemrosesan ulang yang dijamin at-least-once tidak menimbulkan efek ganda. Dengan begitu, at-least-once yang dipadukan dengan dedupe memberikan hasil yang setara dengan tepat sekali tanpa menuntut transaksi Kafka.
Pola ini membawa dua syarat yang sering terlewat. Pengenal wajib tetap sama antar pengiriman ulang, sehingga event_id harus dihasilkan sebelum pengiriman pertama dan tidak boleh diambil dari offset atau waktu penerimaan. Penyimpanan pengenal juga perlu direncanakan, karena tabelnya tumbuh seiring jumlah kejadian dan lazim diberi masa retensi yang selaras dengan masa pemrosesan ulang yang masih diperbolehkan.
Kesalahan umum#
- Mengambil
offsetpesan sebagaievent_id. Offset tidak stabil antar pengiriman ulang pada producer yang berbeda, sehingga pengenalnya berubah dan dedupe gagal. Dedupe yang mengandalkan pengenal yang berubah hanya menggeser duplikatnya, bukan menghilangkannya. Tetapkan pengenal di sumber kejadian sebelum pengiriman pertama. - Commit offset sebelum pemrosesan selesai demi kecepatan. Commit dini memang mempercepat consumer, tetapi pemrosesan yang gagal setelahnya tidak pernah mendapat kesempatan kedua, sehingga kejadian hilang selamanya. Commit hanya setelah hasil tersimpan.
- Menganggap
enable.idempotencesudah menyelesaikan masalah pemrosesan tepat sekali sampai ke tujuan akhir. Jaminannya berhenti di log Kafka untuk satu producer, sedangkan pengiriman ganda ke basis data tujuan tetap mungkin terjadi. Tambahkan transaksi untuk pemrosesan atomik, atau dedupeevent_iddi tujuan, dan keduanya lazim digunakan bersamaan. - Menyimpan pengenal kejadian tanpa masa retensi. Tabel pengenal tumbuh tanpa batas dan memperlambat pemeriksaan kehadirannya pada setiap kejadian baru. Beri masa simpan yang selaras dengan masa pemrosesan ulang yang masih diperbolehkan pada sistem tersebut.
Ringkasan#
- Semantik at-most-once mengorbankan pesan demi kecepatan, at-least-once menjamin keutuhan dengan risiko duplikat, dan exactly-once menutup keduanya dengan idempotence dan transaksi.
- Duplikat lahir dari pengiriman ulang yang konfirmasinya hilang, dan broker tanpa penanda urutan tidak dapat membedakannya dari pesan baru.
- Nomor urut idempoten membuat broker menolak ulangan, sehingga log tidak menyimpan salinan kedua.
- Dedupe berbasis
event_iddi sisi konsumen menyempurnakan jaminan sampai ke sistem di luar Kafka, dengan syarat pengenalnya stabil dan penyimpanannya direncanakan.
Referensi#
- Apache Kafka design: message delivery semantics — Apache Software Foundation — definisi resmi at-most-once, at-least-once, dan exactly-once beserta peran pesan committed.
- Apache Kafka design: using transactions — Apache Software Foundation — transaksi sebagai dasar pemrosesan consume-transform-produce yang tepat sekali.
- Producer configs — Apache Software Foundation
— konfigurasi
enable.idempotenceyang menjadi dasar pengiriman idempoten.