Key vs No-Key Message #
Saat kita mengirimkan data ke Apache Kafka menggunakan klien produser, setiap pesan dibungkus dalam bentuk objek ProducerRecord. Di dalam objek ini, kita dapat menentukan apakah pesan tersebut akan dikirimkan dengan menyertakan kunci (key) atau tanpa kunci (no-key). Keputusan arsitektural yang tampak sederhana ini memiliki dampak yang luar biasa besar terhadap bagaimana data didistribusikan di seluruh partisi kluster, efisiensi penulisan batch di memori produser, kinerja jaringan, hingga jaminan urutan pemrosesan pesan (message ordering guarantees) di sisi konsumen. Memilih strategi pengiriman yang tidak tepat dapat menyebabkan penurunan throughput yang masif atau menyebabkan ketidakseimbangan beban kerja (load imbalance) yang ekstrem pada broker. Oleh karena itu, kita harus memahami secara mendalam perbedaan mekanisme internal, kelebihan, serta kekurangan dari kedua pendekatan ini.
Pengiriman Pesan dengan Key (Hashed Key Partitioning) #
Ketika kita membuat objek ProducerRecord dengan menyertakan kunci, misalnya:
ProducerRecord<String, String> record = new ProducerRecord<>("orders-topic", "User_A", "Order_1001");
Kita memberi tahu Kafka bahwa pesan "Order_1001" memiliki relasi logis dengan identitas "User_A".
Mekanisme Kerja: Algoritma Hashing Murmur2 #
Secara default, jika suatu pesan memiliki kunci, Partitioner bawaan Kafka akan memproses kunci tersebut menggunakan algoritma hash Murmur2. Hasil hash biner tersebut kemudian diubah menjadi angka positif dan dimodulo dengan jumlah total partisi aktif pada topik tersebut untuk menentukan nomor partisi tujuan:
$$\text{Nomor Partisi} = \text{Utils.toPositive}(\text{Utils.murmur2}(\text{keyBytes})) \pmod{\text{Total Partisi}}$$
Karakteristik penting dari algoritma hashing adalah konsistensi deterministik. Selama jumlah partisi pada topik tersebut tidak berubah, kunci yang sama akan selalu menghasilkan nomor partisi yang sama. Jika kunci "User_A" hari ini diarahkan ke Partisi 2, maka esok hari atau kapan pun pesan dengan kunci "User_A" dikirim, ia akan selalu masuk ke Partisi 2.
Jaminan Urutan Data (Ordering Guarantee) #
Mengapa kita membutuhkan jaminan bahwa kunci yang sama selalu masuk ke partisi yang sama? Jawabannya adalah urutan pemrosesan. Kafka hanya menjamin urutan pesan yang dikirimkan pada tingkat partisi spesifik, bukan tingkat topik secara keseluruhan.
Dengan mengarahkan semua aktivitas "User_A" (misalnya: OrderCreated, OrderPaid, OrderShipped) ke partisi yang sama (misalnya Partisi 2), konsumen yang membaca dari Partisi 2 dijamin akan memproses event-event tersebut secara berurutan sesuai kronologi kejadian. Jika kita menyebarkan event tersebut ke partisi yang berbeda, ada kemungkinan event OrderPaid dibaca oleh konsumen lain mendahului event OrderCreated, yang akan merusak integritas data bisnis kita.
Pengiriman Pesan Tanpa Key (No-Key Message) #
Jika pesan dikirim tanpa menyertakan kunci, misalnya:
ProducerRecord<String, String> record = new ProducerRecord<>("logs-topic", "Log_Message_Content");
Tujuan utama kita biasanya adalah distribusi data yang merata tanpa mempedulikan urutan pemrosesan antar pesan. Namun, mekanisme internal bagaimana Kafka menangani pesan tanpa kunci telah mengalami evolusi besar demi efisiensi performa.
Mekanisme Klasik: Round-Robin Partitioner (Sebelum Kafka 2.4) #
Di versi lawas Kafka, pesan tanpa kunci didistribusikan menggunakan metode Round-Robin (bergantian secara berurutan). Pesan pertama masuk ke Partisi 0, pesan kedua ke Partisi 1, pesan ketiga ke Partisi 2, dan seterusnya secara melingkar.
- Kelemahan Kritis: Pendekatan ini memicu inefisiensi memori yang parah di dalam
RecordAccumulatorproduser. Karena pesan dikirim bergantian satu per satu ke setiap partisi, batch memori untuk masing-masing partisi terisi sangat lambat. Akibatnya, produser sering kali terpaksa mengirimkan batch jaringan yang berukuran sangat kecil (misalnya baru terisi 1 KB dari kapasitasbatch.size16 KB) karena batas waktulinger.mstelah habis. Ini memboroskan bandwidth jaringan dan membebani I/O broker secara berlebihan.
Revolusi Efisiensi: Sticky Partitioner (Kafka 2.4+) #
Untuk mengatasi inefisiensi Round-Robin, Kafka memperkenalkan Sticky Partitioner mulai versi 2.4. Ini adalah standar baru yang sangat cerdas untuk mendistribusikan data tanpa kunci.
Cara Kerja Sticky Partitioner #
Alih-alih menyebarkan pesan tipis-tipis ke semua partisi secara bergantian, Sticky Partitioner akan memilih satu partisi secara acak dan menempel (sticky) pada partisi tersebut untuk semua pesan tanpa kunci berikutnya.
- Produser memilih Partisi
0sebagai target aktif. - Semua pesan non-key baru akan dimasukkan ke dalam batch Partisi
0hingga batch tersebut penuh mencapai kapasitasbatch.size(misal 16 KB). - Setelah batch Partisi
0penuh dan ditutup untuk dikirim oleh Sender Thread, Sticky Partitioner akan memilih partisi baru secara acak (misalnya Partisi2) untuk menjadi partisi aktif berikutnya. - Proses ini diulangi terus-menerus.
flowchart TD
subgraph Hashed ["1. Hashed Key Partitioner (Dengan Key)"]
direction TB
K1["Pesan Kunci 'User_A'"] --> H1["Murmur2 Hash"]
K2["Pesan Kunci 'User_B'"] --> H2["Murmur2 Hash"]
H1 -->|"Partisi 0"| B0["Batch Partisi 0 (User_A)"]
H2 -->|"Partisi 1"| B1["Batch Partisi 1 (User_B)"]
end
subgraph RoundRobin ["2. Round-Robin Partitioner (Tanpa Key - Lama)"]
direction TB
R1["Pesan 1"] --> P0["Partisi 0 (Batch 1 KB)"]
R2["Pesan 2"] --> P1["Partisi 1 (Batch 1 KB)"]
R3["Pesan 3"] --> P2["Partisi 2 (Batch 1 KB)"]
NoteRR["Overhead: Batch tidak pernah penuh, langsung dikirim karena timeout"]
end
subgraph Sticky ["3. Sticky Partitioner (Tanpa Key - Kafka 2.4+)"]
direction TB
S1["Pesan 1"] --> SP0["Partisi 0 (Batch Aktif)"]
S2["Pesan 2"] --> SP0
S3["Pesan 3"] --> SP0
SP0 -->|"Batch Penuh (16 KB)"| Send["Kirim Batch Partisi 0"]
S4["Pesan 4 (Pindah ke Partisi Baru)"] --> SP1["Partisi 1 (Batch Aktif Baru)"]
end
Keunggulan Sticky Partitioner #
- Throughput Lebih Tinggi: Dengan memaksimalkan pengisian batch hingga kapasitas penuh sebelum dikirim, payload per permintaan jaringan menjadi jauh lebih efisien.
- Pengurangan Latensi: Mengurangi jumlah permintaan tulis (produce requests) yang harus ditangani oleh broker, sehingga memangkas beban CPU broker.
- Distribusi Merata dalam Skala Besar: Meskipun menempel sementara pada satu partisi, dalam hitungan detik data tetap terdistribusi secara seimbang ke seluruh partisi karena perpindahan target partisi yang terjadi secara berkala.
Masalah Distribusi Data: Key Skewness #
Ketika kita memutuskan menggunakan pesan dengan kunci (key), kita harus menyadari adanya risiko terjadinya ketidakseimbangan distribusi data yang parah di dalam kluster, yang dikenal sebagai Key Skewness (kemencengan kunci).
Mengapa Key Skewness Terjadi? #
Mekanisme hashing Murmur2 mengasumsikan bahwa variasi nilai kunci terdistribusi secara acak dan merata. Namun, realitas data bisnis sering kali tidak demikian.
Bayangkan kita memiliki topik orders dengan kunci berupa customerId.
- Pelanggan reguler (misal perorangan) hanya melakukan 1–2 transaksi per hari.
- Namun, ada satu pelanggan korporat besar (misal akun B2B/reseller) yang melakukan 1.000.000 transaksi per hari.
- Karena semua transaksi pelanggan korporat tersebut menggunakan kunci yang sama (misal kunci
"CUST_B2B_MEGA"), maka berdasarkan rumus hash Murmur2, seluruh satu juta transaksi tersebut akan masuk ke satu partisi yang sama (misalnya Partisi1). - Akibatnya, Partisi
1akan membengkak luar biasa besar (menyebabkan konsumsi disk penuh), broker leader dari Partisi1akan bekerja ekstra keras (CPU spike), dan konsumen yang ditugaskan membaca Partisi1akan mengalami hambatan pemrosesan yang parah (consumer lag), sementara konsumen di partisi lain menganggur (idle).
Solusi Key Skewness: Teknik Key Salting #
Untuk mengatasi masalah Key Skewness tanpa kehilangan manfaat pengiriman pesan dengan kunci, kita dapat menerapkan teknik yang disebut Key Salting (penggaraman kunci).
Apa itu Key Salting? #
Key Salting adalah teknik menambahkan nilai acak atau sufiks berurutan (salt) ke ujung kunci utama kita sebelum dikirim ke Kafka. Hal ini memaksa algoritma hashing Murmur2 memecah kunci tunggal tersebut menjadi beberapa variasi kunci yang berbeda, sehingga pesan disebarkan ke beberapa partisi yang berbeda pula.
Misalnya, kita menetapkan rentang salt dari 1 hingga 5. Kunci "CUST_B2B_MEGA" akan ditransisikan menjadi:
"CUST_B2B_MEGA_1"-> masuk ke Partisi 0"CUST_B2B_MEGA_2"-> masuk ke Partisi 1"CUST_B2B_MEGA_3"-> masuk ke Partisi 2- …dan seterusnya.
Implementasi Key Salting di Java #
Berikut adalah contoh implementasi perbandingan tanpa salting (anti-pattern) dengan implementasi salting yang benar:
// ANTI-PATTERN: Mengirim key besar tanpa salting, memicu ketidakseimbangan partisi (key skewness)
public class NaiveOrderProducer {
public void sendOrders(KafkaProducer<String, String> producer, Order order) {
// Jika customerId adalah akun B2B besar, jutaan data masuk ke satu partisi yang sama
String key = order.getCustomerId();
ProducerRecord<String, String> record = new ProducerRecord<>("orders-topic", key, order.toJson());
producer.send(record);
}
}
// BENAR: Menerapkan Key Salting untuk memecah beban pengiriman ke beberapa partisi secara seimbang
import java.util.concurrent.ThreadLocalRandom;
public class SaltingOrderProducer {
private static final int SALT_RANGE = 5; // Membagi data ke maksimal 5 partisi berbeda
public void sendOrders(KafkaProducer<String, String> producer, Order order) {
String key = order.getCustomerId();
// Cek apakah pelanggan ini merupakan akun B2B raksasa yang membutuhkan salting
if (order.isMegaCorporate()) {
// ✓ Tambahkan salt acak di akhir key untuk memicu hash biner yang berbeda
int randomSalt = ThreadLocalRandom.current().nextInt(1, SALT_RANGE + 1);
key = key + "_" + randomSalt; // Hasil: "CUST_B2B_MEGA_3"
}
// Kirim record dengan key yang telah digarami
ProducerRecord<String, String> record = new ProducerRecord<>("orders-topic", key, order.toJson());
producer.send(record);
}
}
Konsekuensi Key Salting yang Harus Diperhatikan: #
- Kehilangan Urutan Mutlak: Karena kunci dipecah menjadi beberapa variasi, urutan pesan antar transaksi pelanggan korporat tersebut tidak lagi dijamin secara global. Pemrosesan data B2B hanya dijamin runtut di dalam sub-partisi (misalnya semua data berakhiran
_3tetap runtut di Partisi2). - Kompleksitas Deserialisasi: Aplikasi konsumen kita harus menyadari adanya teknik salting ini dan harus membuang sufiks
_salt(misal_3) saat melakukan pemrosesan data di database hilir.
Karakteristik Algoritma Murmur2 dan Alasan Pemilihannya #
Mengapa Kafka memilih algoritma hash Murmur2 sebagai landasan pembagian partisi berbasis kunci? Ada beberapa alasan teknis yang mendasari keputusan arsitektural ini:
- Non-Cryptographic Hash: Algoritma kriptografi seperti MD5, SHA-1, atau SHA-256 dirancang untuk keamanan tingkat tinggi dan ketahanan terhadap serangan tabrakan (collision attacks). Namun, proses komputasi yang rumit ini membutuhkan daya CPU yang sangat besar. Sebaliknya, Murmur2 adalah algoritma non-kriptografi yang dioptimalkan sepenuhnya untuk kecepatan pembacaan tabel hash cepat (hash table lookups). Kecepatan eksekusi komputasinya bisa mencapai beberapa Gigabyte data per detik pada satu core CPU. Hal ini mencegah serializer/partitioner menjadi bottleneck pada utas aplikasi utama kita.
- Efek Longsoran yang Baik (Excellent Avalanche Effect): Properti krusial dari fungsi hash yang baik adalah efek longsoran. Jika kita mengubah satu bit saja pada input kunci (misal mengubah
"User_A"menjadi"User_B"), hasil hash biner keluarannya akan berubah secara dramatis dan acak (hampir 50% bit output berfluktuasi). Properti ini memastikan bahwa kunci-kunci yang memiliki kemiripan penamaan string tetap akan disebarkan secara merata ke seluruh partisi yang berbeda. - Tingkat Tabrakan yang Rendah (Low Collision Rate): Meskipun sangat cepat, Murmur2 memiliki probabilitas tabrakan kunci yang sangat rendah. Distribusi output biner 32-bit yang dihasilkannya tergolong sangat seragam untuk berbagai jenis data string maupun angka.
Dampak Penambahan Partisi Terhadap Kepatuhan Kunci (Re-hashing Problem) #
Jaminan urutan data berbasis kunci di Apache Kafka memiliki satu keterbatasan fisik yang krusial: Jumlah partisi pada topik tujuan harus tetap konstan.
Apabila volume lalu lintas data kita meningkat tajam dan kita memutuskan untuk meningkatkan jumlah partisi pada topik (misalnya melakukan penskalaan topik orders-topic dari 3 partisi menjadi 6 partisi), rumus pembagian modulo kita akan berubah secara instan.
Ilustrasi Masalah Re-hashing #
Misalkan hasil hash Murmur2 dari kunci "User_A" bernilai integer positif 10:
- Sebelum Penskalaan (3 Partisi):
$$\text{Target Partisi} = 10 \pmod 3 = \text{Partisi } 1$$
Semua pesan
"User_A"lama tersimpan di Partisi1. - Setelah Penskalaan (6 Partisi):
$$\text{Target Partisi} = 10 \pmod 6 = \text{Partisi } 4$$
Pesan
"User_A"baru yang dikirim setelah penambahan partisi akan diarahkan ke Partisi4.
Dampak Buruk #
Karena pesan "User_A" sekarang terpecah ke dalam dua partisi fisik yang berbeda (Partisi 1 untuk data lama, dan Partisi 4 untuk data baru), konsumen yang ditugaskan membaca topik tersebut akan mendeserialisasi dan memproses data dari kedua partisi secara konkuren dan asinkron. Akibatnya, urutan kronologis transaksi "User_A" akan rusak seketika.
Berikut adalah perbandingan skenario penanganan penskalaan yang salah (anti-pattern) terhadap solusi yang direkomendasikan:
// ANTI-PATTERN: Mengubah jumlah partisi langsung pada topik transaksional aktif
// ✗ Mengabaikan fakta bahwa penambahan partisi merusak urutan penulisan data kunci yang ada
public class NaiveScaleUp {
public void scaleTopicPartitions() {
// Menggunakan AdminClient untuk menaikkan partisi orders-topic secara mendadak di produksi
// Memicu masalah re-hashing instan pada semua produser aktif
}
}
// BENAR: Membuat topik baru dengan partisi yang diinginkan dan melakukan migrasi data terstruktur
// ✓ Menjaga urutan data di topik orders-v1 tetap selesai diproses sebelum mengalihkan ke orders-v2
public class SafeScaleUp {
// Prosedur Operasional Standar (SOP):
// 1. Buat topik baru 'orders-v2' dengan jumlah partisi yang lebih besar (misal 12 partisi).
// 2. Deploy aplikasi konsumen baru yang membaca dari 'orders-v2'.
// 3. Arahkan produser aplikasi untuk mulai menulis ke topik baru 'orders-v2'.
// 4. Biarkan konsumen lama menyelesaikan sisa antrean (drain) di 'orders-v1' hingga habis.
// 5. Matikan konsumen lama dan decommission 'orders-v1'.
}
Solusi Alternatif: Consistent Hashing Partitioner #
Jika kita mengantisipasi perubahan jumlah partisi yang sering terjadi di masa depan, kita dapat menulis Custom Partitioner yang mengimplementasikan konsep Consistent Hashing (seperti ring hashing menggunakan algoritma Ketama). Dengan consistent hashing, saat terjadi penambahan partisi, hanya sebagian kecil kunci saja yang akan dipindahkan ke partisi baru, sementara mayoritas kunci lainnya tetap berkomitmen pada partisi lamanya.
Panduan Memilih: Kapan Menggunakan Key vs Tanpa Key #
Memilih antara menggunakan kunci atau tanpa kunci harus diselaraskan dengan kebutuhan fungsional dan non-fungsional aplikasi kita. Berikut adalah matriks keputusan sebagai acuan:
| Aspek Pembanding | Menggunakan Key (Standard Hashing) | Tanpa Key (Sticky Partitioner) | Menggunakan Key + Salting |
|---|---|---|---|
| Jaminan Urutan | Ya, Kuat (pada tingkat kunci di partisi yang sama). | Tidak ada jaminan urutan sama sekali. | Terbatas (urutan hanya terjamin pada tingkat sub-kunci/salt). |
| Distribusi Beban | Bergantung pada keragaman kunci (rentan skewness). | Sangat Merata (keseimbangan dinamis). | Merata (memecah titik kemacetan kunci utama). |
| Throughput Jaringan | Sedang (karena batching dipisah per partisi tujuan kunci). | Sangat Tinggi (batching selalu optimal maksimal). | Tinggi (meningkatkan efisiensi batch dibandingkan tanpa salting). |
| Use Case Utama | Integrasi database CDC, riwayat transaksi per akun, status pembaruan order. | Pengumpulan log sistem (log aggregation), pelacakan klik (clickstream), metrik IoT tanpa urutan. | Penanganan akun korporat super besar di sistem transaksi perbankan/e-commerce. |
Ringkasan #
- Jaminan Urutan: Pengiriman pesan dengan kunci memastikan semua data yang memiliki kunci sama masuk ke partisi yang sama, menjamin urutan kronologis di sisi konsumen.
- Murmur2 Hashing: Penentuan partisi berbasis kunci menggunakan algoritma hash Murmur2 secara deterministik selama jumlah partisi topik tetap stabil.
- Round-Robin vs Sticky: Sticky Partitioner (Kafka 2.4+) merevolusi efisiensi pesan non-key dengan memusatkan batching pada satu partisi hingga penuh, melipatgandakan throughput dibandingkan round-robin klasik.
- Key Skewness: Risiko ketidakseimbangan beban kerja broker akibat adanya satu kunci dominan yang menguras memori dan CPU pada satu partisi tunggal.
- Key Salting: Solusi memecah kemacetan partisi dengan menyematkan sufiks acak pada kunci dominan, menyebarkan beban secara proporsional ke beberapa partisi.
← Sebelumnya: Serialization Berikutnya: Acks, Retries, & Linger.ms →