Log Compaction #

Ketika kita berbicara tentang retensi data di Apache Kafka, pikiran kita sering kali langsung tertuju pada penghapusan data secara absolut berdasarkan batas waktu atau batas ukuran file. Namun, ada banyak skenario bisnis di mana kita tidak ingin membuang data historis sama sekali, melainkan hanya ingin menyimpan status akhir (final state) dari suatu entitas. Bayangkan sistem saldo rekening bank, pembaruan profil pengguna, atau database stok barang. Kita tidak membutuhkan seluruh riwayat perubahan dari tahun lalu yang memakan kapasitas penyimpanan terabyte; kita hanya perlu tahu saldo terakhir nasabah saat ini atau alamat email aktif pengguna. Untuk memecahkan kebutuhan arsitektural ini, Kafka menyediakan fitur luar biasa bernama Log Compaction (Pemadatan Log).


Filosofi Dasar: Delete Policy vs Compact Policy #

Secara arsitektural, Kafka membagi kebijakan pembersihan commit log (cleanup.policy) menjadi dua jenis utama:

  1. cleanup.policy=delete (Default) Kafka menghapus seluruh berkas segmen log lama secara fisik begitu usianya melebihi batas waktu atau ukurannya melebihi batas byte. Pesan dihapus tanpa memedulikan kunci atau isinya.
  2. cleanup.policy=compact Kafka menjamin bahwa untuk setiap kunci pesan (message key), broker akan selalu mempertahankan minimal satu pesan terakhir dengan offset terbesar. Pesan-pesan lama dengan kunci yang sama akan dibersihkan secara bertahap untuk menghemat ruang disk.

Dengan menggunakan kebijakan pemadatan, kita dapat menyimpan data secara permanen (infinite retention) per kunci bisnis tanpa khawatir disk penyimpanan akan penuh oleh data duplikat yang usang.


Bagaimana Log Cleaner Bekerja? (Clean vs Dirty Portion) #

Proses pemadatan log tidak dijalankan secara real-time saat pesan ditulis oleh produser, melainkan dijalankan secara asinkron di latar belakang oleh thread khusus bernama Log Cleaner Thread.

Pembagian Struktur Log Segment #

Untuk memahami proses pemadatan, kita harus melihat bagaimana berkas log partisi dibagi menjadi dua area logis:

flowchart LR
    subgraph Clean ["Clean Portion (Terkompresi)"]
        direction LR
        C1["Key A: Off 10"] --- C2["Key B: Off 12"] --- C3["Key C: Off 15"]
    end
    
    subgraph Dirty ["Dirty Portion (Kotor)"]
        direction LR
        D1["Key A: Off 20"] --- D2["Key D: Off 21"]
    end
    
    Clean <--> |"First Dirty Offset"| Dirty
  • Clean Portion: Area log bagian awal yang sudah dipadatkan pada siklus sebelumnya. Area ini hanya berisi satu pesan unik per kunci.
  • Dirty Portion: Area log bagian akhir yang berisi pesan-pesan baru yang baru saja masuk dari produser. Di area ini, kunci yang sama bisa muncul berulang kali dengan nilai yang berbeda.
  • First Dirty Offset: Titik pembatas yang menandai batas akhir data bersih dan awal data kotor.

Tahapan Proses Pemadatan (Compaction Cycle) #

Ketika Log Cleaner Thread mendeteksi bahwa rasio data kotor telah melampaui ambang batas minimum yang ditentukan oleh parameter log.cleaner.min.cleanable.ratio (default: 0.5 atau 50% data kotor):

  1. Membangun Skimpy Offset Map: Log Cleaner memindai seluruh segmen tidak aktif di area Dirty Portion. Ia membangun sebuah tabel hash di memori broker yang disebut Skimpy Offset Map. Tabel ini memetakan hash 128-bit dari kunci pesan ke offset terbesarnya yang ditemukan di area kotor.
  2. Menyalin Pesan Secara Selektif: Log Cleaner mulai membaca ulang berkas log dari awal (dari area Clean Portion hingga akhir Dirty Portion).
    • Untuk setiap record pesan yang dibaca, broker mencocokkan kuncinya dengan data di Skimpy Offset Map.
    • Jika offset pesan yang sedang dibaca lebih kecil dari offset yang tercatat di map untuk kunci tersebut, maka pesan tersebut dinilai usang dan langsung diabaikan (dibuang).
    • Jika offset pesan sama dengan offset terbesar di map, pesan tersebut ditulis ke berkas segmen bersih baru.
  3. Penggabungan File (Segment Merge): Setelah penyalinan selesai, segmen-segmen lama yang kotor dan bersih digabungkan secara fisik menjadi segmen baru yang padat. Segmen-segmen lama dihapus dari disk OS, dan pointer First Dirty Offset digeser ke akhir segmen bersih yang baru terbentuk.

Analisis Memori: Skimpy Offset Map & Performance Tuning #

Karena Skimpy Offset Map dibangun langsung di dalam memori JVM Heap broker, kita harus melakukan perhitungan kapasitas memori secara teliti agar tidak menyebabkan broker kehabisan memori (OutOfMemoryError).

Struktur Memori Per-Entri #

Setiap entri di dalam Skimpy Offset Map memakan ruang memori sebesar 24 byte yang terbagi menjadi:

  • 16 byte: Untuk menyimpan hash MD5 dari kunci pesan.
  • 8 byte: Untuk menyimpan angka offset pesan (tipe data long 64-bit).

Perhitungan Kasus Nyata (Sizing RAM) #

Jika kita memiliki sebuah partisi kotor dengan jumlah kunci unik mencapai 10.000.000 (10 juta kunci unik), maka kebutuhan RAM minimal yang harus dialokasikan untuk memproses peta offset tersebut adalah:

$$\text{Memori} = 10.000.000 \times 24 \text{ byte} = 240.000.000 \text{ byte} \approx 240 \text{ MB}$$

Parameter Tuning Log Cleaner #

Untuk mengoptimalkan kinerja pembersih log, kita dapat melakukan penyetelan pada parameter berikut di file server.properties:

  • log.cleaner.dedupe.buffer.size: Ukuran memori total yang dialokasikan untuk menampung Skimpy Offset Map di seluruh thread pembersih (default: 134.217.728 / 128 MB). Untuk kluster dengan jutaan kunci unik, kita wajib menaikkan parameter ini (misal menjadi 1.073.741.824 / 1 GB).
  • log.cleaner.threads: Jumlah thread latar belakang yang bertugas melakukan pemadatan log (default: 1). Jika kita memiliki banyak partisi terkompresi, tingkatkan jumlah thread ini (misalnya 4 atau 8) untuk mempercepat siklus pemadatan.
  • log.cleaner.io.buffer.size: Ukuran buffer I/O disk yang digunakan untuk membaca/menulis file segmen selama proses pemadatan (default: 2.097.152 / 2 MB).

Menghindari I/O Spikes dengan Throttling Bandwidth #

Proses pemadatan yang membaca dan menulis ulang file segmen besar di disk secara konstan dapat menimbulkan kemacetan bus I/O disk. Hal ini dapat mengganggu performa produser yang sedang menulis pesan baru. Untuk membatasi bandwidth tulis Log Cleaner, kita dapat menyetel parameter:

# Membatasi kecepatan baca/tulis Log Cleaner maksimal 15 MB/detik per broker
log.cleaner.io.max.bytes.per.second=15728640

Penyetelan ini menjamin proses pemadatan berjalan stabil di latar belakang tanpa pernah merebut seluruh I/O throughput disk dari operasi transaksional utama klien.


Batasan & Konsekuensi Log Compaction bagi Aplikasi Konsumen #

Menerapkan kebijakan pemadatan log membawa beberapa implikasi arsitektural penting yang harus diantisipasi oleh aplikasi konsumen kita.

1. Offset yang Tidak Berurutan (Non-Contiguous Offsets) #

Setelah proses pemadatan berjalan, offset pesan di dalam berkas log tidak lagi berurutan secara linier (misalnya: dari 1, 2, 3, 4 berubah menjadi 1, 4, 12, 15 karena offset 2 dan 3 dihapus).

  • Perilaku Konsumen: Konsumen tetap dapat membaca data secara normal. Ketika konsumen melakukan seek ke offset 2, Kafka secara cerdas akan langsung mengarahkan pointer baca konsumen ke offset berikutnya yang tersedia, yaitu 4.

2. Larangan Kunci Null (Null Message Keys) #

Log Compaction hanya dapat bekerja pada pesan yang memiliki kunci (key).

  • Bahaya Kunci Null: Jika kita mengirimkan pesan dengan null key ke topik terkompresi, broker Kafka akan gagal memetakan pesan tersebut ke Skimpy Offset Map. Hal ini dapat menyebabkan error pada Log Cleaner Thread atau pesan tersebut terlewat dari proses pemadatan, menyumbat ruang penyimpanan secara permanen.

Alur Visual Pemadatan Log #

Berikut adalah diagram visual yang menunjukkan keadaan commit log sebelum dan sesudah proses pemadatan dijalankan oleh Log Cleaner Thread:

flowchart TD
    subgraph Sebelum_Pemadatan ["1. Log Sebelum Pemadatan (Banyak Duplikasi Kunci)"]
        direction LR
        K1_O1["Key: User_A <br> Offset: 10 <br> Val: Jakarta"]
        K2_O2["Key: User_B <br> Offset: 11 <br> Val: Bandung"]
        K1_O3["Key: User_A <br> Offset: 12 <br> Val: Surabaya"]
        K3_O4["Key: User_C <br> Offset: 13 <br> Val: Medan"]
        K2_O5["Key: User_B <br> Offset: 14 <br> Val: Bali"]
    end

    subgraph Sesudah_Pemadatan ["2. Log Setelah Pemadatan (Hanya Offset Terbesar Tersisa)"]
        direction LR
        K1_O3_A["Key: User_A <br> Offset: 12 <br> Val: Surabaya"]
        K3_O4_A["Key: User_C <br> Offset: 13 <br> Val: Medan"]
        K2_O5_A["Key: User_B <br> Offset: 14 <br> Val: Bali"]
    end

    K1_O1 -.->|"Dihapus (Ada Offset 12)"| Sesudah_Pemadatan
    K2_O2 -.->|"Dihapus (Ada Offset 14)"| Sesudah_Pemadatan
    K1_O3 -->|"Dipertahankan"| K1_O3_A
    K3_O4 -->|"Dipertahankan"| K3_O4_A
    K2_O5 -->|"Dipertahankan"| K2_O5_A

    style K1_O1 stroke:#c62828,stroke-width:2px
    style K2_O2 stroke:#c62828,stroke-width:2px
    style K1_O3 stroke:#2e7d32,stroke-width:2px
    style K3_O4 stroke:#2e7d32,stroke-width:2px
    style K2_O5 stroke:#2e7d32,stroke-width:2px

Mekanisme Penghapusan Kunci: Tombstone Marker #

Jika sistem pemadatan selalu menjamin minimal satu pesan terakhir per kunci dipertahankan selamanya, lalu bagaimana cara kita menghapus suatu kunci secara permanen dari topik terkompresi? Misalnya, ketika seorang pengguna menghapus akunnya (delete account), kita wajib menghapus seluruh data pribadinya untuk mematuhi regulasi privasi data (seperti GDPR).

Untuk mengatasi hal ini, Kafka memperkenalkan konsep Tombstone Marker (juga dikenal sebagai Delete Marker).

Bagaimana Cara Kerja Tombstone? #

  1. Pengiriman Nilai Null: Klien produser mengirimkan pesan baru dengan kunci yang ingin dihapus, tetapi dengan menyetel nilai pesan (value) menjadi null.
  2. Pencatatan sebagai Tombstone: Ketika broker menerima pesan bernilai null ini, pesan tersebut ditulis ke log dan secara resmi dinilai sebagai Tombstone Marker.
  3. Pembersihan Tahap Pertama: Saat siklus pemadatan log berikutnya berjalan, Log Cleaner Thread mendeteksi adanya tombstone marker. Broker akan menghapus seluruh pesan-pesan lama yang terkait dengan kunci tersebut, menyisakan hanya berkas tombstone marker itu sendiri di dalam log.
  4. Pembersihan Tahap Akhir: Tombstone marker tidak boleh langsung dihapus seketika, karena konsumen aktif yang sedang membaca data secara offline/lag perlu melihat tombstone tersebut untuk mengetahui bahwa kunci itu telah dihapus.
  5. Parameter Durasi Simpan: Tombstone marker akan disimpan di disk selama durasi waktu yang dikonfigurasi oleh parameter:

$$\text{log.cleaner.delete.retention.ms} = 86.400.000 \text{ ms (24 Jam)}$$

Setelah 24 jam terlewati, pada siklus pemadatan berikutnya, berkas tombstone marker tersebut akan dihapus secara total dari disk. Kunci tersebut kini benar-benar hilang dari kluster Kafka.


Kasus Penggunaan Utama: CDC & Kafka Streams State Stores #

Log Compaction adalah fondasi arsitektural di balik modul-modul data terdistribusi modern.

1. Change Data Capture (CDC) #

Dalam arsitektur microservices, kita sering menggunakan tool CDC (seperti Debezium) untuk menangkap perubahan tabel di database transaksional (seperti PostgreSQL atau MySQL) dan mereplikasikannya ke Kafka.

  • Topik CDC wajib dikonfigurasi dengan cleanup.policy=compact. Kita hanya butuh mencatat status baris terakhir database per primary key. Jika database melakukan update baris, data baru dengan offset terbesar di Kafka akan mewakili baris database saat ini, sedangkan data lama dapat dibuang dengan aman untuk menghemat ruang disk.

2. Changelog Topik di Kafka Streams (Estimasi Waktu Rekonstruksi) #

Aplikasi pemrosesan data real-time (seperti Kafka Streams atau Apache Flink) sering menyimpan state bisnis mereka di database lokal memori (seperti RocksDB) yang disebut State Store.

  • Untuk menjamin toleransi kegagalan, state store secara berkala mengirimkan log perubahannya ke topik tersembunyi di Kafka bernama Changelog Topic yang dikompresi.
  • Jika server yang menjalankan aplikasi Streams mati mendadak, server pengganti dapat memulihkan state memori lokalnya dengan sangat cepat dengan memutar kembali (replay) data dari awal changelog topik terkompresi tersebut.

Kalkulasi Peningkatan Kecepatan Recovery #

Mari kita hitung secara riil perbedaan waktu recovery untuk aplikasi Streams yang mengolah 1.000.000 transaksi harian (total data mentah uncompacted sebesar 10 GB):

  • Skenario A: Tanpa Log Compaction (Delete Policy) Aplikasi harus memutar kembali (replay) seluruh 10 GB log perubahan dari awal. Dengan kecepatan I/O jaringan klien rata-rata 16 MB/detik, waktu yang dibutuhkan untuk membangun kembali state store lokal adalah:

$$\text{Waktu Recovery} = \frac{10.240 \text{ MB}}{16 \text{ MB/s}} \approx 640 \text{ detik (10.6 Menit)}$$

  • Skenario B: Dengan Log Compaction Karena topik dipadatkan, data 10 GB yang berisi pembaruan berulang diperkecil menjadi hanya status terakhir per nasabah (misalnya tersisa hanya 100 MB data bersih). Dengan kecepatan I/O yang sama:

$$\text{Waktu Recovery} = \frac{100 \text{ MB}}{16 \text{ MB/s}} \approx 6.2 \text{ detik}$$

Hasil Akhir: Log Compaction memotong waktu downtime pemulihan aplikasi microservice kita sebesar 99% (dari 10 menit menjadi hanya 6 detik), yang sangat krusial untuk menjaga Service Level Agreement (SLA) operasional.


Panduan CLI Operasional #

Berikut adalah perintah command line praktis untuk mengonfigurasi dan mengelola pemadatan log.

1. Membuat Topik Terkompresi Baru #

# Membuat topik profil pengguna dengan kebijakan pemadatan log
kafka-topics.sh --bootstrap-server localhost:9092 \
  --create \
  --topic user-profiles \
  --partitions 3 \
  --replication-factor 3 \
  --config cleanup.policy=compact \
  --config log.cleaner.min.cleanable.ratio=0.3 \
  --config min.insync.replicas=2

2. Mengirimkan Pesan Tombstone via Console Producer #

Untuk menghapus profil pengguna dengan kunci user_id_99 secara permanen, kita harus mengirimkan pesan bernilai null. Di console producer, kita bisa menggunakan flag properti khusus untuk mendefinisikan null:

# Mengaktifkan console producer dengan parse key dan null marker kustom
kafka-console-producer.sh --bootstrap-server localhost:9092 \
  --topic user-profiles \
  --property parse.key=true \
  --property key.separator=: \
  --property null.marker=TOMBS

# Ketik perintah berikut di terminal (Key: user_id_99, Value: null)
user_id_99:TOMBS

3. Mengonfigurasi Minimum Compaction Lag #

Secara default, Kafka memadatkan data kotor sesegera mungkin setelah batas rasio terlampaui. Namun, jika aplikasi konsumen kita membutuhkan waktu untuk membaca seluruh riwayat update real-time (bukan hanya status akhir saja), kita dapat menangguhkan pemadatan untuk pesan baru selama jangka waktu tertentu menggunakan log.cleaner.min.compaction.lag.ms:

# Menahan pesan agar tidak dipadatkan selama minimal 1 jam (3.600.000 ms)
# agar konsumen real-time sempat membaca seluruh rentetan perubahan historis.
kafka-configs.sh --bootstrap-server localhost:9092 \
  --entity-type topics \
  --entity-name user-profiles \
  --alter \
  --add-config log.cleaner.min.compaction.lag.ms=3600000

Ringkasan #

  • Log Compaction: Kebijakan pembersihan log yang mempertahankan minimal satu data terbaru dengan offset terbesar untuk setiap kunci pesan.
  • Rasio Kotor: Pemadatan dipicu secara asinkron ketika rasio data kotor (dirty portion) melampaui parameter log.cleaner.min.cleanable.ratio (default: 0.5).
  • Skimpy Offset Map: Struktur data memori broker yang memakan 24 byte per entri untuk mencatat offset kunci terbesar. Sesuaikan log.cleaner.dedupe.buffer.size sesuai kebutuhan.
  • Cleaner Throttling: Gunakan parameter log.cleaner.io.max.bytes.per.second untuk membatasi pemakaian I/O disk agar tidak mengganggu transaksi produser.
  • Tombstone Marker: Pesan bernilai null yang dikirim produser untuk memicu penghapusan kunci secara permanen pada topik terkompresi.
  • Offset Seeks: Pasca-kompresi, offset log tidak lagi berurutan secara linier, namun API pencarian konsumen tetap berjalan normal dengan melompati offset kosong.
  • Use Case: Log Compaction adalah fondasi vital untuk sistem sinkronisasi database CDC dan changelog state store pada Kafka Streams.

← Sebelumnya: Size-Based   Berikutnya: Producer Workflow →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact