EOS Limitation #
Jaminan pemrosesan data Exactly-Once Semantics (EOS) di Apache Kafka sering kali dianggap sebagai solusi mutlak untuk seluruh masalah konsistensi data di arsitektur sistem terdistribusi. Fitur ini memang sangat kuat, namun sebagai insinyur perangkat lunak, kita harus memahami bahwa EOS bukanlah keajaiban tanpa batas (silver bullet). Ada batasan-batasan teknis dan struktural penting yang membatasi ruang lingkup jaminannya. Yang paling mendasar adalah: EOS Kafka hanya berlaku untuk aliran data yang berada sepenuhnya di dalam ekosistem Kafka (Kafka-to-Kafka). Ketika aliran data kita berinteraksi dengan sistem eksternal — seperti menulis ke basis data SQL/NoSQL, mengirim panggilan REST API ke payment gateway, atau memperbarui cache di Redis — jaminan Exactly-Once Kafka tidak lagi mencakup sistem-sistem tersebut. Artikel ini akan membedah secara mendalam batas-batas ruang lingkup EOS Kafka, tantangan integrasi sistem eksternal, solusi arsitektur penyelamatan seperti Outbox Pattern, overhead performa, dan checklist konfigurasi produksi yang wajib dipenuhi.
Batas Ruang Lingkup EOS: Hanya Berlaku untuk Aliran Kafka-to-Kafka #
Batasan paling mendasar dari EOS Kafka adalah lingkup batas ekosistem internal (boundary of guarantee). Protokol transaksi Kafka dirancang untuk mengoordinasikan status internal broker, topik, partisi, dan offset konsumen yang disimpan di topik internal __consumer_offsets.
JAMINAN KAFKA EOS BOUNDARY:
[Topic Input] ──> [Kafka Consumer] ──> [Logika Aplikasi] ──> [Kafka Producer] ──> [Topic Output]
└────────────────────────────────── JAMINAN EOS KAFKA ─────────────────────────────────────────┘
BATASAN INTEGRASI EKSTERNAL (TIDAK Dijamin EOS secara Otomatis):
[Topic Input] ──> [Kafka Consumer] ──> [Logika Aplikasi] ──> [Database / REST API Eksternal]
└─────────────── JAMINAN KAFKA ────────────────┘ └────────── TIDAK DIJAMIN EOS ───────────────┘
Jika aplikasi kita mengonsumsi pesan dari Kafka dan menuliskan hasilnya ke database (misalnya PostgreSQL) atau mengirimkan data ke web service eksternal (misalnya Stripe API), transaksi Kafka tidak dapat menjamin atomisitas transaksi database tersebut.
- Mengapa demikian? Karena Kafka tidak mendukung protokol transaksi terdistribusi global yang memblokir seperti Two-Phase Commit lintas sistem eksternal (misalnya XA Transactions). Kafka tidak bisa mengirimkan perintah rollback ke database PostgreSQL atau membatalkan panggilan API HTTP Stripe yang terlanjur diproses di server pihak ketiga jika transaksi Kafka kemudian mengalami kegagalan.
Oleh karena itu, pola pemrosesan transaksional exactly-once bawaan Kafka hanya terjamin 100% jika kita membaca dari topik Kafka dan menulis kembali ke topik Kafka.
2-Phase Commit (2PC) vs Protokol Transaksi Kafka #
Untuk menghargai batasan EOS Kafka, kita harus memahami mengapa Kafka tidak menggunakan protokol Two-Phase Commit (2PC) terdistribusi klasik (seperti spesifikasi XA/Open JTA) yang biasa digunakan untuk mengoordinasikan transaksi antara sistem antrean dengan database relasional (misalnya JMS ActiveMQ dengan Oracle Database).
Dalam 2PC tradisional:
- Fase Persiapan (Prepare Phase): Koordinator transaksi eksternal mengirimkan pesan ke database dan sistem antrean untuk mengunci sumber daya (blocking locks) dan menanyakan apakah mereka siap untuk menulis data.
- Fase Komit (Commit Phase): Jika semua sistem menjawab “ya”, koordinator mengirimkan perintah komit. Jika salah satu menjawab “tidak” atau tidak merespons akibat gangguan jaringan, koordinator mengirimkan perintah rollback ke semua sistem.
Mengapa ini dilarang di ekosistem Kafka?
- Hambatan Kunci (Resource Locking): 2PC tradisional menuntut database untuk mengunci baris data (row locks) selama fase persiapan hingga komit selesai. Jika jaringan lambat, kunci ini akan bertahan lama, menghancurkan throughput database kita secara instan.
- Titik Kegagalan Tunggal (Single Point of Failure): Jika koordinator transaksi eksternal mati setelah fase persiapan tetapi sebelum fase komit, seluruh database akan terkunci dalam status menggantung (in-doubt state), membutuhkan intervensi manual administrator untuk melepaskan kunci.
- Skala Raksasa: Kafka dirancang untuk memproses jutaan pesan per detik. Menggunakan mekanisme penguncian XA terdistribusi akan membuat throughput Kafka terjun bebas mendekati throughput database relasional yang sangat lambat.
Sebaliknya, Kafka menggunakan variasi 2PC internal yang tidak memblokir (non-blocking). Broker coordinator mencatat perubahan status secara asinkron ke topik __transaction_state dan menyebarkan penanda (markers) ke log partisi data biasa tanpa pernah mengunci partisi atau melarang konsumen membaca data non-transaksional. Jaminan ini sangat cepat, namun konsekuensinya ia tidak bisa menjangkau dan mengunci baris data di database eksternal Anda.
Penanganan Detail Outbox Pattern #
Ketika aplikasi microservices kita harus memperbarui status database lokal (RDBMS) sekaligus memublikasikan event ke Kafka, pola Transactional Outbox Pattern adalah solusi arsitektur paling andal untuk menjamin semantik pemrosesan Exactly-Once di tingkat sistem keseluruhan.
1. Struktur Skema Tabel Outbox #
Aplikasi kita menulis ke tabel bisnis (misalnya orders) dan tabel outbox (misalnya outbox_events) di bawah transaksi database lokal yang sama. Berikut adalah contoh struktur skema tabel outbox yang direkomendasikan:
CREATE TABLE outbox_events (
event_id UUID PRIMARY KEY,
aggregate_type VARCHAR(255) NOT NULL,
aggregate_id VARCHAR(255) NOT NULL,
event_type VARCHAR(255) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
2. Alur Kerja Engine CDC (Change Data Capture) #
Setelah transaksi database lokal dikomit secara ACID:
- Engine CDC (seperti Debezium yang terpasang di Kafka Connect) memantau log biner database secara non-blocking (misalnya Write-Ahead Log / WAL pada PostgreSQL atau Binlog pada MySQL). CDC tidak melakukan kueri
SELECTberulang yang membebani CPU database. - CDC secara otomatis mengekstrak baris baru yang masuk ke tabel
outbox_events. - CDC mengubah baris tersebut menjadi pesan Kafka, menyematkan
event_idpada header pesan, dan mengirimkannya ke topik Kafka tujuan (misalnya topikorders-event-stream). - Setelah pesan sukses terkirim ke Kafka, sebuah proses latar belakang opsional dapat menghapus entri lama di tabel
outbox_eventsagar ukuran database tidak membengkak.
3. Jaminan Pengiriman CDC-to-Kafka #
Karena engine CDC membaca log transaksi fisik database, ia menjamin bahwa setiap pesan yang sukses dikomit di database pasti akan dikirim ke Kafka (At-Least-Once).
- Untuk mencegah duplikasi di broker Kafka akibat pengiriman ulang CDC pasca-crash konektor, konektor Kafka Connect dikonfigurasi dengan
producer.enable.idempotence=true. - Konsumen hilir yang membaca dari topik Kafka kemudian menggunakan salah satu taktik deduplikasi (unique constraints atau tabel Redis) untuk menyaring duplikasi jika terjadi kegagalan commit offset di sisi konsumen itu sendiri.
Solusi Integrasi Sistem Eksternal #
Meskipun EOS Kafka tidak mencakup sistem eksternal secara otomatis, kita dapat menggunakan beberapa pola arsitektur untuk mencapai jaminan pemrosesan Exactly-Once di tingkat aplikasi:
Taktik 1: Deduplikasi Idempoten di Sisi Database #
Taktik ini memanfaatkan kapabilitas idempotency dari database target kita.
- Cara Kerja: Pastikan setiap pesan yang masuk dari Kafka memiliki pengenal unik yang permanen (misalnya
transaction_id). Di sisi database target, buat pengenal unik tersebut sebagai Primary Key atau buat indeks kolom berstatus Unique Constraint. - Operasi Tulis: Gunakan perintah penulisan bersyarat seperti UPSERT (Insert or Update /
INSERT ... ON CONFLICT DO UPDATEdi PostgreSQL, atauINSERT ... ON DUPLICATE KEY UPDATEdi MySQL). Jika pesan duplikat tiba akibat kegagalan commit offset di sisi konsumen, database hanya akan memperbarui baris data lama alih-alih membuat baris baru yang ganda.
Taktik 2: Pola Outbox Pattern dengan CDC (Change Data Capture) #
Jika kita harus melakukan penulisan database lokal dan pengiriman pesan ke Kafka secara bersamaan dari satu microservice, cara terbaik adalah menggunakan Outbox Pattern:
- Aplikasi kita hanya menulis data bisnis ke tabel utama dan menulis pesan keluar ke tabel khusus bernama Outbox Table di dalam satu transaksi database lokal (RDBMS ACID). Ini menjamin penulisan data dan pendaftaran pesan keluar bersifat atomik 100%.
- Komponen Change Data Capture (seperti Debezium atau Kafka Connect Source Connector) memantau log transaksi database kita.
- Begitu transaksi database lokal sukses dikomit, CDC akan membaca data dari Outbox Table secara asinkron dan mengirimkannya ke topik Kafka.
- Karena CDC menggunakan jaminan At-Least-Once, pesan dikirim ke Kafka minimal sekali. Kita kemudian mengaktifkan Idempotent Producer di konektor untuk memastikan tidak ada duplikasi di broker Kafka.
flowchart TD
App["Aplikasi Klien"] -->|"1. Transaksi Lokal (ACID)"| DB[(Database RDBMS)]
subgraph DB_Boundary ["Batas Database"]
DB -->|"Tulis Data Bisnis"| BizTable["Tabel Bisnis"]
DB -->|"Tulis Payload Event"| OutboxTable["Tabel Outbox"]
end
CDC["Debezium (CDC) / Kafka Connect"] -->|"2. Pantau Log Transaksi"| OutboxTable
CDC -->|"3. Kirim Event secara Idempotent"| Kafka["Kafka Topic"]
style DB_Boundary stroke:#333,stroke-dasharray:5,5
style DB stroke:#0288d1,stroke-width:2px
style Kafka stroke:#2e7d32,stroke-width:2px
Overhead Performa dan Dampak terhadap Throughput #
Mengaktifkan fitur transaksi dan idempotensi di Apache Kafka memberikan dampak langsung terhadap performa dan throughput kluster. Kita harus membayar harga durabilitas ini dalam bentuk:
1. Peningkatan Latensi Jaringan dan CPU #
- ACKS=ALL: Idempotensi dan transaksi secara otomatis memaksa produser menggunakan properti
acks=all. Ini berarti produser harus menunggu pesan disalin ke seluruh daftar replika ISR sebelum mendapatkan konfirmasi sukses. Latensi jaringan per pengiriman akan meningkat sebanding dengan jumlah replika dan delay jaringan antar rack. - Logging Status Transaksi: Setiap transaksi aktif menuntut koordinator untuk menulis catatan transaksi (Ongoing, PrepareCommit, CompleteCommit) ke topik internal
__transaction_state. Ini melipatgandakan jumlah I/O tulis yang harus ditangani oleh broker koordinator.
2. Hambatan Latensi di Sisi Konsumen (LSO Blockage) #
Seperti yang telah dibahas sebelumnya, konsumen dengan konfigurasi isolation.level=read_committed tidak dapat membaca pesan baru melewati batas Last Stable Offset (LSO).
- Jika ada sebuah produser transaksional yang mengalami jeda atau kesalahan logika bisnis di tengah jalan dan membiarkan status transaksinya tetap menggantung (hanging transaction) tanpa memanggil
commitTransaction()atauabortTransaction(), LSO partisi tersebut akan membeku. - Konsumen
read_committedhilir akan tertahan (block) dan tidak dapat memproses data baru dari partisi tersebut, memicu peningkatan consumer lag secara drastis meskipun data non-transaksional baru sudah menumpuk di broker.
Bahaya Transaksi Berumur Panjang (Long-Running Transactions) #
Kesalahan fatal yang sering dilakukan oleh pengembang saat menggunakan Transaction API adalah membiarkan transaksi berjalan terlalu lama.
- Penyebab: Membuka transaksi dengan
beginTransaction(), melakukan pemrosesan data yang rumit (seperti memproses file besar atau menunggu panggilan API eksternal yang lambat), baru kemudian memanggilcommitTransaction(). - Risiko:
- LSO Blockage pada konsumen hilir akan berlangsung lama, menahan pemrosesan data sistem lain secara global.
- Transaction Timeout: Broker Kafka memiliki parameter keamanan
transaction.max.timeout.ms(secara default15menit). Jika durasi transaksi produser melebihi batas waktutransaction.timeout.msyang disetel (default1menit), koordinator transaksi di broker secara sepihak akan membatalkan (abort) transaksi tersebut. Produser yang mencoba melakukan komit setelah batas waktu terlewati akan menerimaTransactionTimeoutExceptiondan seluruh data batch tersebut akan dibuang.
Aturan Emas: Buka transaksi sesingkat mungkin. Hanya panggil beginTransaction() tepat sebelum memanggil fungsi send() ke Kafka, dan langsung panggil commitTransaction() segera setelah data batch selesai terkirim. Jangan pernah menaruh pemrosesan logika bisnis yang lambat atau panggilan I/O non-Kafka di dalam blok transaksi aktif.
Checklist Kesiapan Produksi Exactly-Once (EOS) #
Sebelum merilis aplikasi yang menggunakan semantik Exactly-Once ke lingkungan produksi, pastikan seluruh parameter konfigurasi berikut telah diverifikasi dan diselaraskan:
1. Konfigurasi Klien Produser (Producer Settings) #
□ enable.idempotence = true (Mengaktifkan nomor urut per pesan)
□ transactional.id = [NAMA_UNIK_STATIS] (Wajib unik untuk setiap replika aplikasi aktif)
□ transaction.timeout.ms = 10000 s.d. 30000 (Setel durasi transaksi sesingkat mungkin)
□ acks = all (Dipaksa otomatis oleh idempotensi)
□ max.in.flight.requests.per.connection = 1 s.d. 5 (Menjamin urutan pengiriman)
2. Konfigurasi Klien Konsumen (Consumer Settings) #
□ isolation.level = read_committed (Mencegah dirty reads data aborted)
□ enable.auto.commit = false (Offset harus dikomit via sendOffsetsToTransaction)
3. Konfigurasi Kluster Broker (Broker Settings) #
□ transaction.state.log.replication.factor = 3 (Ketahanan data status transaksi)
□ transaction.state.log.min.isr = 2 (Batas minimal broker replika transaksi aktif)
□ transaction.max.timeout.ms = 900000 (Toleransi batas waktu maksimal transaksi di broker)
Ringkasan #
- Kafka-only Boundary — Jaminan semantik Exactly-Once (EOS) di Kafka hanya dijamin 100% berlaku untuk aliran data internal dari topik Kafka ke topik Kafka (Kafka-to-Kafka).
- Integrasi Eksternal — Transaksi Kafka tidak secara otomatis mencakup penyimpanan database SQL/NoSQL atau panggilan REST API pihak ketiga, memerlukan taktik penanganan tambahan di tingkat aplikasi.
- Deduplikasi Idempoten — Taktik penanganan data ganda di sisi database target menggunakan pemanfaatan Primary Key/Unique Constraints dan operasi penulisan bersyarat (UPSERT).
- Outbox Pattern — Pola arsitektur yang memastikan atomisitas penulisan data bisnis dan pendaftaran pesan keluar secara lokal di RDBMS sebelum dialirkan secara idempotent ke Kafka oleh CDC (Debezium).
- Overhead Performa EOS — Transaksi menyebabkan penurunan throughput kluster dan peningkatan latensi akibat penulisan log status ganda ke
__transaction_statedan kebutuhan replikasi penuh (acks=all).- LSO Blockage (Hambatan LSO) — Konsumen
read_committedhilir akan tertahan tidak dapat membaca pesan baru jika ada transaksi ongoing di partisi yang berjalan terlalu lama atau menggantung.- Aturan Emas Transaksi — Blok transaksi harus dijaga agar sesingkat mungkin; hindari menaruh pemrosesan data lambat atau panggilan I/O non-Kafka di antara
beginTransaction()dancommitTransaction().
← Sebelumnya: Producer Consumer Transaction Berikutnya: Apa Itu Kafka Connect? →