Tiga istilah pada chapter sebelumnya — topic, partition, offset — menentukan hampir semua keputusan desain di Kafka: seberapa cepat sistem membaca, urutan apa yang terjaga, dan bagaimana sistem pulih dari kegagalan. Topic adalah kategori event, partition adalah unit paralelisme sekaligus jaminan urutan, dan offset adalah alamat setiap pesan di dalam partition. Memahami ketiganya berarti memahami cara Kafka menskalakan pembacaan ke banyak mesin tanpa saling mengganggu. Chapter ini membedah ketiganya satu per satu, lalu mensimulasikan distribusi kunci, deret offset, dan rebalance consumer group dengan Python murni.

Topic: Kategori Event dengan Nama Terstruktur #

Topic adalah tempat event sejenis dikumpulkan, dan namanya biasanya mengikuti pola <organisasi>.<domain>.<peristiwa> — misalnya tokokita.orders atau tokokita.payments. Nama yang terstruktur membuat siapa pemilik topic terbaca sekali pandang, penting ketika pulasan topic berjalan di satu cluster yang sama. Topic membawa konfigurasi penyimpanannya sendiri:

KonfigurasiPeranContoh nilai
num.partitionsjumlah partition topic3
retention.mslama pesan disimpan sebelum dibersihkan604800000 (7 hari)
cleanup.policycara pesan lama dibersihkandelete atau compact

Kebijakan delete membuang pesan yang lebih tua dari retensi; kebijakan compact menyimpan satu nilai terakhir per kunci — pola yang dipakai untuk data berbentuk “keadaan terkini” seperti profil pelanggan. Daftar lengkap konfigurasi topic beserta nilai bawaannya terdokumentasi di dokumentasi Apache Kafka .

Partition sebagai Append-Only Log #

Setiap topic dipecah menjadi satu atau lebih partition, dan setiap partition adalah append-only log tersendiri: pesan baru selalu menempel di ekor, pesan lama tidak pernah diubah. Inilah unit paralelisme Kafka — jumlah partition menjadi batas atas banyaknya consumer yang bisa membaca satu topic secara bersamaan, karena satu partition hanya boleh dimiliki satu consumer dalam sebuah consumer group.

Producer yang mengirim pesan berkunci akan menghitung hash dari kunci tersebut lalu memetakan hasilnya ke satu partition. Konsekuensinya dua arah: seluruh pesanan Budi pasti menempel pada partition yang sama sehingga urutan per pelanggan terjaga, tetapi kunci yang kurang beragam membuat sebagian partition panas dan sebagian nganggur — masalah hot key yang sudah kita ukur pada Chapter Partitioning dan Sharding di section distributed systems.

Offset: Alamat Pesan dan Penanda Posisi Baca #

Offset adalah nomor urut pesan di dalam satu partition, dimulai dari 0 dan naik monoton tanpa pernah dipakai ulang. Offset bekerja seperti alamat rumah: tokokita.orders[partition 0] offset 41 menunjuk tepat satu pesan. Consumer menyimpan committed offset — posisi baca terakhir yang dinyatakan selesai — sehingga consumer yang direstart melanjutkan dari pesan berikutnya, bukan membaca ulang dari awal.

Committed offset juga menjadi pintu replay: menggeser offset sebuah consumer group ke angka yang lebih kecil membuat group membaca ulang histori pesan — sesuatu yang mustahil pada antrean klasik yang menghapus pesan setelah dikirim.

Simulasi: Kunci, Offset, dan Rebalance #

Simulasi berikut memodelkan topic tokokita.orders tiga partition. Dua belas pesanan berkunci pelanggan diproduksi, offset dihitung per partition, lalu consumer group tiga anggota mengalami kegagalan satu anggota:

python
"""Simulasi topic Kafka: kunci pelanggan -> partition (hash MD5), offset
per partition, lalu rebalance consumer group saat satu consumer berhenti."""

import hashlib

JUMLAH_PARTISI = 3
CONSUMER = ["analitik-1", "analitik-2", "analitik-3"]


def partisi_dari_kunci(kunci):
    """Kafka menghitung hash kunci pesan; MD5 dipakai agar stabil antar proses."""
    return int(hashlib.md5(kunci.encode()).hexdigest(), 16) % JUMLAH_PARTISI


def tampilkan_assignment(judul, pemilik):
    print(judul)
    for consumer in sorted(pemilik):
        partisi = ", ".join(f"p{p}" for p in pemilik[consumer]) or "-"
        print(f"  {consumer}: {partisi}")


def main():
    pesanan = [
        ("ORD-1001", "budi"), ("ORD-1002", "siti"), ("ORD-1003", "dewi"),
        ("ORD-1004", "budi"), ("ORD-1005", "siti"), ("ORD-1006", "anton"),
        ("ORD-1007", "dewi"), ("ORD-1008", "rina"), ("ORD-1009", "budi"),
        ("ORD-1010", "siti"), ("ORD-1011", "dewi"), ("ORD-1012", "budi"),
    ]

    # 1) Produksi: kunci pelanggan menentukan partition, offset bertambah monoton
    log = {p: [] for p in range(JUMLAH_PARTISI)}  # p -> [(offset, kunci, order)]
    for order_id, pelanggan in pesanan:
        p = partisi_dari_kunci(pelanggan)
        offset = len(log[p])
        log[p].append((offset, pelanggan, order_id))

    print(f"Topic tokokita.orders, {JUMLAH_PARTISI} partition, 12 pesanan:")
    for p in range(JUMLAH_PARTISI):
        if log[p]:
            isi = ", ".join(f"{off}:{kunci}" for off, kunci, _ in log[p])
            print(f"  p{p} offset 0..{len(log[p]) - 1}: {isi}")
        else:
            print(f"  p{p} (kosong, belum ada pesanan)")

    pesanan_budi = [order for isi in log.values() for off, kunci, order in isi
                    if kunci == "budi"]
    partisi_budi = partisi_dari_kunci("budi")
    print(f"  semua pesanan budi ({' '.join(pesanan_budi)}) berada di p{partisi_budi}")

    # 2) Assignment awal: satu partition untuk tiap consumer (round-robin)
    pemilik = {c: [] for c in CONSUMER}
    for p in range(JUMLAH_PARTISI):
        pemilik[CONSUMER[p % len(CONSUMER)]].append(p)
    tampilkan_assignment("\nAssignment awal consumer group analitik:", pemilik)

    # 3) analitik-3 berhenti -> rebalance: seluruh partition dibagi ulang
    aktif = [c for c in CONSUMER if c != "analitik-3"]
    pemilik = {c: [] for c in aktif}
    for p in range(JUMLAH_PARTISI):
        pemilik[aktif[p % len(aktif)]].append(p)
    tampilkan_assignment("Setelah analitik-3 berhenti (rebalance):", pemilik)

    # 4) Lanjut baca dari committed offset — pesan lama tidak dibaca ulang
    committed = {0: 4, 1: 1, 2: 0}  # posisi baca terakhir tiap partition
    print("\nPosisi lanjut baca setelah rebalance (committed offset):")
    for p in range(JUMLAH_PARTISI):
        penerima = next(c for c, daftar in pemilik.items() if p in daftar)
        print(f"  p{p} -> {penerima} melanjutkan dari offset {committed[p]}")


if __name__ == "__main__":
    main()

Output yang Dihasilkan:

text
Topic tokokita.orders, 3 partition, 12 pesanan:
  p0 offset 0..10: 0:budi, 1:siti, 2:dewi, 3:budi, 4:siti, 5:dewi, 6:rina, 7:budi, 8:siti, 9:dewi, 10:budi
  p1 offset 0..0: 0:anton
  p2 (kosong, belum ada pesanan)
  semua pesanan budi (ORD-1001 ORD-1004 ORD-1009 ORD-1012) berada di p0

Assignment awal consumer group analitik:
  analitik-1: p0
  analitik-2: p1
  analitik-3: p2
Setelah analitik-3 berhenti (rebalance):
  analitik-1: p0, p2
  analitik-2: p1

Posisi lanjut baca setelah rebalance (committed offset):
  p0 -> analitik-1 melanjutkan dari offset 4
  p1 -> analitik-2 melanjutkan dari offset 1
  p2 -> analitik-1 melanjutkan dari offset 0

Mari kita telaah baris-baris kode di atas:

  1. Baris 10-12 (partisi_dari_kunci): model sederhana dari partitioner Kafka — Kafka aslinya menghitung hash murmur2 dari kunci; simulasi memakai MD5 agar hasil identik di setiap eksekusi.
  2. Baris 30-35 (produksi): kunci dipetakan ke partition, lalu offset diambil dari panjang log saat itu — offset karenanya naik monoton per partition dan tidak pernah dipakai ulang.
  3. Baris 50-54 (assignment awal): pembagian round-robin memberi setiap consumer satu partition; aturan satu-partition-satu-pemilik inilah yang mencegah pesan diproses ganda dalam satu group.
  4. Baris 56-61 (rebalance): keanggotaan group berubah, seluruh partition dibagi ulang ke consumer yang masih hidup — proses yang pada Kafka sungguhan disertai jeda singkat seluruh group.
  5. Baris 63-68 (committed offset): consumer baru pemilik partition membaca committed offset dan melanjutkan dari sana; pesan yang sudah dikommit tidak dibaca ulang.

Membaca Hasil Simulasi #

Distribusi pesanan timpang secara jujur: p0 menampung 11 pesanan, p1 hanya satu, dan p2 kosong — lima kunci pelanggan terlalu sedikit untuk tiga partition, dan kebetulan hash budi, siti, dewi, serta rina mendarat di p0. Jaminan urutan tetap terpenuhi (keempat pesanan Budi berurutan di p0), tetapi consumer p0 bekerja sebelas kali lebih berat daripada consumer p1 — kunci yang kurang beragam menghadirkan kembali masalah hot key dari section distributed systems. Di produksi, TokoKita menambah kardinalitas kunci (misalnya order_id saat urutan per pelanggan tidak diperlukan) atau mengurangi jumlah partition agar sepadan dengan jumlah kunci.

Bagian rebalance memperlihatkan sifat penting berikutnya: pemilikan partition bukan permanen. Ketika analitik-3 berhenti, p2 berpindah ke analitik-1 dan seluruh assignment dihitung ulang — consumer yang tersisa kini menanggung partition tambahan, sementara committed offset menjaga tidak ada pesan yang terlewat ataupun dibaca dua kali.

Model penyimpanan ini menjawab pertanyaan “data tersimpan di mana dan bagaimana dibagi”. Pertanyaan berikutnya berpindah ke dua aktor yang bekerja di atasnya: bagaimana producer memilih partition dan ack, serta bagaimana consumer mengelola polling dan commit — pembahasan Chapter Producers dan Consumers.