Event #
Dalam arsitektur event streaming modern, event merupakan unit data terkecil dan fakta fundamental yang mewakili suatu kejadian nyata di dunia bisnis kita. Berbeda dengan sistem antrean pesan tradisional yang memperlakukan pesan sebagai instruksi sementara untuk segera dihapus, Apache Kafka memperlakukan event sebagai catatan sejarah yang tidak dapat diubah (immutable) dan disimpan secara permanen. Memahami struktur anatomi, mekanisme distribusi, dan pengelolaan skema event secara mendalam adalah pondasi krusial sebelum kita merancang sistem terdistribusi yang tangguh dan berskala besar.
Konsep Dasar: Apa itu Event? #
Di dalam sistem Apache Kafka, sebuah event (sering juga disebut sebagai record atau message) mencatat fakta bahwa sesuatu telah terjadi di dalam sistem atau bisnis kita. Fakta ini bersifat immutable, yang berarti sekali sebuah event ditulis ke dalam log Kafka, event tersebut tidak dapat diubah, dihapus, atau dimodifikasi oleh siapapun. Karakteristik kekekalan ini sangat penting karena menjamin integritas data historis yang dapat kita audit kapan saja tanpa khawatir terjadi manipulasi di tengah jalan.
Sebagai contoh konkret, dalam aplikasi perdagangan elektronik (e-commerce), kejadian nyata seperti “Pelanggan A menambahkan barang X ke keranjang belanja pada pukul 10:00” atau “Pembayaran transaksi Y berhasil diverifikasi pada pukul 10:05” ditulis sebagai event individual. Setiap event membawa konteks waktu dan payload data yang merekam keadaan tepat pada saat peristiwa itu terjadi.
Perbedaan fundamental antara Kafka dengan broker pesan tradisional seperti RabbitMQ terletak pada cara memperlakukan event setelah dikonsumsi. Pada RabbitMQ, setelah konsumen menerima dan mengonfirmasi pesan, pesan tersebut akan segera dihapus dari memori broker untuk menghemat ruang penyimpanan. Di Kafka, event tetap tersimpan di dalam disk broker sesuai dengan kebijakan retensi yang kita tetapkan, memungkinkan konsumen lain untuk membaca kembali (replay) seluruh peristiwa historis tersebut dari titik awal mana pun.
Anatomi Fisik dan Struktur Event #
Secara fisik di tingkat biner, sebuah event yang dikirimkan oleh aplikasi produsen (producer) dan disimpan oleh broker Kafka bukanlah sekadar teks JSON atau XML biasa. Kafka membungkus data tersebut ke dalam struktur biner teratur yang terdiri dari empat komponen utama: Key, Value, Timestamp, dan Headers.
Mari kita visualisasikan struktur biner logis dari sebuah event di bawah ini:
flowchart TD
subgraph EventBiner ["Struktur Biner Event Kafka"]
direction TB
Headers["Headers <br/> (Metadata opsional: Trace ID, Schema ID, Token)"]
Timestamp["Timestamp <br/> (8 byte: CreateTime vs LogAppendTime)"]
Key["Key <br/> (Byte Array: Identitas & Rute Partisi)"]
Value["Value <br/> (Byte Array: Payload Bisnis Utama)"]
end
1. Key (Kunci) #
Key adalah data opsional berbentuk larik biner (byte array) yang dikirim bersama payload utama. Meskipun opsional (bisa bernilai null), key memegang peran yang sangat krusial dalam arsitektur Kafka. Fungsi utamanya adalah sebagai penunjuk identitas logis dari data tersebut (misalnya ID Pengguna, ID Transaksi, atau Nomor Registrasi Kendaraan) serta digunakan oleh algoritma internal produsen untuk menentukan ke partisi mana event tersebut harus dikirimkan.
2. Value (Nilai/Payload) #
Value adalah payload data utama yang ingin kita kirimkan, juga disimpan sebagai larik biner (byte array). Di sinilah informasi bisnis utama kita berada. Karena Kafka memperlakukan value hanya sebagai biner mentah, kita bebas menggunakan format data apa pun untuk merepresentasikan informasi ini, seperti JSON, Apache Avro, Protocol Buffers (Protobuf), XML, atau teks biasa. Broker Kafka tidak pernah membaca atau memedulikan isi dari value ini; ia hanya menerima, menyimpan, dan menyalurkannya kembali ke konsumen secara efisien.
3. Timestamp (Stempel Waktu) #
Setiap event di Kafka wajib memiliki stempel waktu (timestamp) berukuran 8 byte. Timestamp ini mencatat kapan peristiwa tersebut terjadi atau kapan peristiwa tersebut ditulis ke dalam broker. Ada dua jenis timestamp utama yang didukung secara bawaan oleh Kafka: CreateTime (stempel waktu saat produsen membuat event di sisi klien) dan LogAppendTime (stempel waktu saat broker menerima dan menulis event ke dalam penyimpanan disk lokal).
4. Headers (Metadata Tambahan) #
Ditambahkan sejak Kafka versi 0.11, Headers memungkinkan kita menyisipkan metadata tambahan berupa pasangan kunci-nilai (key-value pairs) tanpa harus mengotori atau mengubah struktur data bisnis utama yang ada di dalam Value. Pasangan kunci ini bertipe string, sedangkan nilainya disimpan sebagai larik biner. Fitur ini sangat berguna untuk keperluan utilitas sistem seperti pelacakan terdistribusi (distributed tracing), pengelolaan versi skema, enkripsi, dan otentikasi.
Peran Kunci (Key) dalam Partisi Data #
Salah satu tantangan terbesar dalam mengelola sistem terdistribusi adalah bagaimana cara membagi beban kerja secara merata ke beberapa server sekaligus tanpa kehilangan jaminan urutan data (ordering guarantee). Kafka memecahkan masalah ini dengan membagi topik menjadi beberapa partisi, dan di sinilah Key pada event memainkan peran sentral.
Ketika aplikasi produsen mengirimkan sebuah event, produsen akan memeriksa apakah event tersebut memiliki Key atau tidak. Alur keputusan ini menentukan rute fisik penyimpanan data kita:
flowchart TD
Start(["Producer Mengirim Event"]) --> CheckKey{"Apakah Key bernilai null?"}
CheckKey -- Ya --> RoundRobin["Sticky Partitioning / Round-Robin <br/> (Event didistribusikan merata ke seluruh partisi)"]
CheckKey -- Tidak --> Hashing["Hitung Hash dari Key <br/> (Formula: MurmurHash2(Key) % Jumlah Partisi)"]
Hashing --> FixedPartition["Kirim ke Partisi Hasil Hash <br/> (Jaminan: Key yang sama masuk ke partisi yang sama)"]
Algoritma Hashing Murmur2 #
Jika Key tidak bernilai null, produsen secara bawaan (default) akan menggunakan algoritma hashing MurmurHash2 untuk mengubah nilai kunci tersebut menjadi representasi angka integer 32-bit. Setelah nilai hash didapatkan, produsen melakukan operasi matematika modulo terhadap total jumlah partisi aktif yang tersedia pada topik tersebut:
$$\text{Partisi} = \text{MurmurHash2}(\text{Key}) \pmod{\text{Jumlah Partisi}}$$
Melalui rumus matematika di atas, selama jumlah partisi pada topik tersebut tidak berubah, nilai Key yang sama akan selalu menghasilkan angka indeks partisi yang sama persis. Hal ini memberikan jaminan mutlak bahwa semua event yang berkaitan dengan satu entitas bisnis tertentu (misalnya, semua riwayat transaksi dari User_123) akan selalu ditulis ke partisi yang sama dan dibaca oleh konsumen yang sama secara berurutan.
Mekanisme Sticky Partitioning untuk Null Key #
Apabila kita mengirim event dengan Key bernilai null, produsen tidak dapat menggunakan metode hashing. Pada versi awal Kafka, produsen akan membagi data ke seluruh partisi secara round-robin satu per satu. Namun, metode ini kurang efisien karena menghasilkan banyak paket jaringan berukuran kecil yang dikirim terus-menerus ke berbagai broker berbeda.
Sejak Kafka 2.4, diperkenalkan algoritma Sticky Partitioner. Ketika Key bernilai null, produsen akan memilih satu partisi secara acak dan mengirimkan seluruh event berikutnya ke partisi yang sama tersebut hingga ukuran batch akumulator (batch.size) terpenuhi atau batas waktu tunggu (linger.ms) habis. Setelah batch dikirim ke broker, produsen akan memilih partisi baru secara acak untuk batch berikutnya. Strategi ini sangat meningkatkan efisiensi penggunaan jaringan dan meminimalkan latensi tanpa mengorbankan distribusi beban kerja secara jangka panjang.
Bahaya Key Skewness (Partisi Panas) #
Meskipun penggunaan Key sangat membantu dalam menjaga urutan pesan, kita harus berhati-hati terhadap fenomena Key Skewness atau partisi timpang. Ini terjadi ketika kita memilih kunci yang distribusinya tidak merata di dunia nyata.
Sebagai contoh, jika kita memilih kunci berdasarkan Negara_Asal transaksi e-commerce, dan 90% pelanggan kita berasal dari Indonesia (ID), maka partisi hasil hash dari kunci ID akan menerima beban 90% dari total data situs kita, sedangkan partisi lainnya akan menganggur. Akibatnya, server broker yang mengelola partisi panas tersebut akan mengalami kelebihan beban CPU dan I/O disk, memicu kemacetan sistem secara keseluruhan. Untuk mencegah hal ini, kita harus selalu memilih Key yang memiliki entropi tinggi dan terdistribusi secara acak, seperti ID Pengguna unik (user_id) atau UUID transaksi.
Stempel Waktu (Timestamp) di Kafka #
Stempel waktu (timestamp) berukuran 8 byte di dalam setiap event Kafka memegang peran penting dalam operasional broker, pemrosesan aliran data (stream processing), dan manajemen ruang penyimpanan disk. Kafka menyediakan konfigurasi tingkat topik (message.timestamp.type) untuk menentukan bagaimana stempel waktu ini dikelola di kluster:
1. CreateTime (Sisi Produsen) #
Secara bawaan, stempel waktu bertipe CreateTime. Ketika aplikasi produsen kita membuat objek event, produsen akan menyuntikkan waktu lokal server saat itu ke dalam metadata event. Jika server produsen dan broker memiliki sinkronisasi waktu yang buruk melalui protokol NTP (Network Time Protocol), stempel waktu ini mungkin tidak mencerminkan waktu sebenarnya saat broker menerima data.
2. LogAppendTime (Sisi Broker) #
Jika konfigurasi topik diatur ke LogAppendTime, broker Kafka akan mengabaikan stempel waktu apa pun yang dikirim oleh produsen. Sesaat setelah broker menerima event dan sebelum menuliskannya ke dalam segmen log lokal di disk, broker akan menyuntikkan waktu server broker saat itu sebagai stempel waktu resmi. Metode ini menjamin bahwa seluruh stempel waktu di dalam partisi bersifat monoton naik (monotonically increasing).
Dampak Terhadap Kebijakan Retensi dan Windowing #
Pemilihan tipe stempel waktu ini memiliki konsekuensi praktis yang signifikan pada arsitektur sistem kita:
- Retensi Data: Kebijakan retensi berbasis waktu di Kafka (misalnya menghapus data setelah 7 hari) dihitung berdasarkan stempel waktu yang tertera di dalam event. Jika kita menggunakan
CreateTimedan produsen tidak sengaja mengirimkan data dengan stempel waktu tahun lalu akibat bug sistem, Kafka akan langsung menghapus data tersebut sesaat setelah ditulis karena dianggap sudah kedaluwarsa. - Windowing di Kafka Streams: Saat kita melakukan pemrosesan event streaming secara real-time (misalnya menghitung total transaksi per 5 menit), aplikasi konsumen akan mengelompokkan data berdasarkan stempel waktu event. Penggunaan
CreateTimemencerminkan waktu kejadian nyata bisnis (event time), sedangkanLogAppendTimemencerminkan waktu sistem mencatat data (processing time).
Metadata Headers untuk Tracing dan Audit #
Headers di dalam Kafka menyediakan struktur metadata tingkat rendah yang sangat fleksibel. Fitur ini dirancang mirip dengan header pada protokol HTTP, di mana metadata dikirimkan sebagai bagian dari amplop event terpisah dari konten payload utama.
Mari kita pelajari contoh kasus riil penggunaan Headers di industri:
1. Distributed Tracing (Pelacakan Terdistribusi) #
Dalam arsitektur microservices, satu alur bisnis pengguna (misalnya, menekan tombol beli) dapat memicu puluhan panggilan API asinkron antar-layanan yang dihubungkan oleh Kafka. Untuk melacak alur ini, kita membutuhkan ID Pelacakan unik (Trace ID dan Span ID) berbasis standar OpenTelemetry.
Dengan menyisipkan Trace ID di dalam Headers Kafka, setiap microservice yang membaca dan menulis kembali ke Kafka dapat meneruskan konteks pelacakan tersebut. Kita dapat melihat visualisasi grafik alur panggilan layanan secara utuh di sistem monitoring seperti Jaeger atau Zipkin tanpa harus memaksa setiap tim pengembang microservice memodifikasi struktur JSON payload bisnis mereka.
2. Pengelolaan Versi Skema (Schema Registry Interaction) #
Ketika produsen mengirimkan data berformat biner seperti Apache Avro, produsen perlu memberi tahu konsumen skema versi berapa yang digunakan untuk mengodekan data tersebut. Produsen menyisipkan kode identifikasi skema (Schema ID unik berukuran 4 byte) ke dalam header event. Saat konsumen membaca data biner dari Kafka, konsumen pertama-tama akan membaca Schema ID dari header, mengunduh skema yang cocok dari server Schema Registry, lalu mendekodekan payload utama secara aman dan cepat.
Pentingnya Schema Registry dan Kontrak Data #
Salah satu kesalahan fatal yang sering dilakukan oleh pengembang yang baru menggunakan Kafka adalah mengirimkan payload data dalam format JSON mentah tanpa aturan skema yang ketat. Karena Kafka memperlakukan payload sebagai biner mentah, produsen bebas mengirim data apa saja. Tanpa kontrak data yang jelas, perubahan kecil pada sisi produsen dapat merusak seluruh sistem konsumen di hilir (downstream).
// ANTI-PATTERN: Produsen mengirim data JSON bebas tanpa kontrol skema
{
"id_transaksi": 9012,
"nominal": 150000 // Konsumen bergantung pada field "nominal" bertipe integer
}
// Produsen melakukan update kode tanpa koordinasi:
{
"transaction_id": 9012, // Nama field diubah
"nominal": "150,000" // Tipe data diubah menjadi string dengan pemisah ribuan
}
// AKIBATNYA: Seluruh aplikasi konsumen crash seketika karena gagal mem-parse data!
Untuk mengatasi masalah koordinasi skala besar ini, kita wajib menggunakan Confluent Schema Registry sebagai pengelola kontrak data antara produsen dan konsumen. Format data yang sangat direkomendasikan untuk ekosistem Kafka adalah Apache Avro atau Protocol Buffers (Protobuf).
Di bawah ini adalah ilustrasi bagaimana Schema Registry bertindak sebagai polisi lalu lintas data untuk mencegah data rusak masuk ke dalam Kafka cluster kita:
sequenceDiagram
participant P as Producer
participant SR as Schema Registry
participant K as Kafka Broker
participant C as Consumer
P->>SR: 1. Daftarkan / Validasi Skema Baru
alt Skema Kompatibel
SR-->>P: return Schema ID (e.g. ID 4)
P->>K: 2. Kirim Payload Biner + Schema ID di Header
K->>C: 3. Tarik Event Biner + Schema ID
C->>SR: 4. Ambil Skema Detail untuk ID 4
SR-->>C: return Skema Avro/Protobuf
Note over C: Dekode biner ke objek data sukses!
else Skema Melanggar Aturan Kompatibilitas
SR-->>P: Throw Exception (Registrasi Ditolak!)
Note over P: Pengiriman ke Kafka dibatalkan secara otomatis
end
Jenis-Jenis Aturan Kompatibilitas Skema #
Dengan menggunakan Schema Registry, kita dapat menetapkan aturan perubahan skema (schema evolution) yang aman bagi konsumen kita:
- BACKWARD Compatibility (Rekomendasi Bawaan): Konsumen dengan skema baru dapat membaca data lama yang ditulis oleh produsen dengan skema lama. Ini memungkinkan kita memperbarui aplikasi konsumen terlebih dahulu secara aman.
- FORWARD Compatibility: Konsumen dengan skema lama dapat membaca data baru yang ditulis oleh produsen dengan skema baru. Ini berguna jika kita ingin memperbarui aplikasi produsen terlebih dahulu tanpa memutus fungsionalitas konsumen lama.
- FULL Compatibility: Perubahan skema bersifat kompatibel baik secara backward maupun forward. Kita bebas memperbarui produsen atau konsumen dalam urutan apa pun tanpa risiko kegagalan parsing.
Mari kita bandingkan tiga format skema populer yang umum digunakan di ekosistem Apache Kafka:
| Parameter Evaluasi | Apache Avro | Protocol Buffers (Protobuf) | JSON Schema |
|---|---|---|---|
| Format Serialisasi | Biner (Sangat Ringkas) | Biner (Sangat Ringkas) | Teks Biasa (Lebih Besar) |
| Kebutuhan Skema | Wajib ada di sisi produsen/konsumen | Wajib dideklarasikan dalam file .proto |
Opsional / Berdiri sendiri |
| Kecepatan Parsing | Sangat Cepat (O(1) CPU cycles) | Sangat Cepat | Lambat (Butuh parsing string DOM) |
| Dukungan Industri | Standar de-facto ekosistem Hadoop/Kafka | Standar Google / Arsitektur gRPC | Standar Web umum |
| Kemudahan Debugging | Sulit dibaca langsung tanpa decoder | Sulit dibaca langsung tanpa decoder | Sangat mudah dibaca manusia |
Skenario Penggunaan Null Key vs Keyed Event #
Dalam merancang sistem berbasis peristiwa, keputusan apakah kita harus menyertakan kunci (Key) atau membiarkannya bernilai null berdampak langsung pada urutan data dan performa distribusi beban kerja. Gunakan tabel panduan keputusan di bawah ini untuk menentukan strategi yang tepat bagi sistem kita:
PILIH Keyed Event (Key TIDAK Null) jika:
✓ Urutan logis data berdasarkan entitas tertentu wajib dijaga (e.g., urutan transaksi per ID Rekening).
✓ Kita mengadopsi pola arsitektur Event Sourcing atau CQRS.
✓ Kita ingin melakukan operasi join data atau agregasi stateful menggunakan Kafka Streams (KTable).
✓ Kita menggunakan strategi Log Compaction untuk mempertahankan event terakhir dari setiap kunci unik.
PILIH Non-Keyed Event (Key = Null) jika:
✗ Urutan global antar entitas tidak penting bagi bisnis kita (e.g., pengumpulan log sistem server acak).
✗ Throughput pengiriman data yang setinggi-tingginya adalah prioritas utama kita.
✗ Kita ingin distribusi beban data tersebar secara mutlak 100% rata ke seluruh partisi dan broker.
Kode Implementasi: Mengirim Event dengan Benar dan Aman #
Mari kita pelajari contoh penulisan kode produsen di bawah ini menggunakan bahasa pemrograman Python untuk menunjukkan penanganan pengiriman event dengan skema dan kunci yang tepat:
# ANTI-PATTERN: Mengirim pesan JSON mentah tanpa key dan tanpa penanganan error
# Tindakan ini memicu hilangnya jaminan urutan data entitas dan mempersulit pelacakan kegagalan.
def kirim_transaksi_salah(producer, data_transaksi):
import json
payload = json.dumps(data_transaksi).encode('utf-8')
# Mengirim tanpa key dan tanpa callback (fire-and-forget)
producer.send('topik-transaksi', value=payload)
# BENAR: Mengirim event dengan Key yang tepat untuk menjamin urutan data per entitas
# Dilengkapi dengan penanganan callback asinkron untuk memastikan pengiriman sukses ke broker.
def kirim_transaksi_benar(producer, data_transaksi):
import json
# ID Akun dijadikan Key untuk menjamin semua transaksi akun ini masuk ke partisi yang sama
key_biner = str(data_transaksi['account_id']).encode('utf-8')
payload_biner = json.dumps(data_transaksi).encode('utf-8')
# Callback penanganan hasil pengiriman (asinkron)
def on_send_success(record_metadata):
# Menyertakan informasi partisi dan offset untuk keperluan audit log aplikasi kita
print(f"✓ Event terkirim ke topik {record_metadata.topic} "
f"partisi [{record_metadata.partition}] dengan offset {record_metadata.offset}")
def on_send_error(ex):
# Logger error untuk memicu sistem alert atau pengiriman ulang data
print(f"✗ Gagal mengirim event ke Kafka cluster: {ex}")
# JANGAN: mengabaikan error ini di produksi, lakukan logging/retry
simpan_ke_antrean_lokal_retry(data_transaksi)
# Mengirim dengan menyertakan Key dan callback penanganan
producer.send(
topic='topik-transaksi',
key=key_biner,
value=payload_biner
).add_callback(on_send_success).add_errback(on_send_error)
Ringkasan #
- Definisi Event — Event adalah rekaman fakta bisnis yang bersifat kekal (immutable), disimpan secara persisten di disk broker Kafka, dan dapat diputar ulang kapan saja oleh berbagai konsumen independen.
- Komponen Event — Struktur logis sebuah event biner terdiri dari Key (opsional untuk rute partisi), Value (payload bisnis utama), Timestamp (stempel waktu 8 byte), dan Headers (metadata pelacakan terdistribusi).
- Algoritma Partisi — Event dengan Key tidak null dipetakan secara konsisten ke partisi yang sama menggunakan rumus hash modulo
MurmurHash2(Key) % Jumlah Partisi. Event tanpa Key didistribusikan secara efisien menggunakan strategiSticky Partitioning.- Bahaya Key Skewness — Hindari penggunaan Key dengan keragaman nilai yang rendah (seperti negara atau jenis kelamin) karena dapat memicu penumpukan data di satu partisi saja, yang memperlambat performa sistem secara keseluruhan.
- Stempel Waktu — Kafka mendukung stempel waktu berbasis
CreateTime(waktu buatan produsen) danLogAppendTime(waktu pencatatan broker) yang memengaruhi kebijakan retensi data dan logika jendela waktu pemrosesan (windowing).- Kontrak Data Skema — Jangan pernah mengirimkan data JSON bebas tanpa skema di lingkungan produksi skala besar. Gunakan Schema Registry bersama format Apache Avro atau Protobuf untuk memastikan kompatibilitas skema aman antar versi aplikasi.
← Sebelumnya: Kapan Harus & Tidak Harus Menggunakan Kafka Berikutnya: Topic →