Queue Comparison #
Dalam arsitektur sistem terdistribusi modern, RabbitMQ dan Apache Kafka sering kali ditempatkan dalam satu kategori yang sama sebagai platform penyedia pesan (message broker). Keduanya mampu menerima data dari produsen, menampungnya untuk sementara waktu, dan menyalurkannya kepada konsumen yang terhubung. Namun, jika kita menelaah hingga ke level paling fundamental dari cara kerja mesin penyimpanan keduanya, kita akan menemukan perbedaan arsitektural yang sangat radikal. Cara RabbitMQ dan Kafka mengimplementasikan konsep “antrean” (queue) sangat bertolak belakang, dan perbedaan inilah yang menentukan seluruh karakteristik performa, batas limitasi, serta kecocokan use case masing-masing di lingkungan produksi.
Mencoba membandingkan RabbitMQ dan Kafka tanpa memahami perbedaan model penyimpanan internal ini adalah sebuah kesalahan desain yang sangat fatal. Kita berisiko memilih teknologi yang salah hanya karena termakan rumor performa throughput tanpa menyadari kompleksitas operasional yang harus dibayar. Artikel ini akan membedah secara mendalam perbandingan konseptual antara model antrean tradisional milik RabbitMQ dan model log terdistribusi milik Kafka.
Model Antrean RabbitMQ: Destructive FIFO Queue #
Model dasar yang digunakan oleh RabbitMQ berakar pada spesifikasi protokol AMQP 0-9-1 tradisional, yaitu Destructive FIFO Queue (Antrean FIFO yang Merusak/Menghapus). Di dalam RabbitMQ, sebuah antrean dirancang sebagai struktur data linier yang bersifat sementara untuk pesan-pesan yang sedang mengalir.
flowchart LR
Producer["Producer"] --> Exchange["Exchange"] --> Queue["Antrean RabbitMQ"] --> Consumer["Consumer"] --> ACK["ACK (Hapus)"]Alur hidup pesan di RabbitMQ berjalan sebagai berikut:
- Produsen menerbitkan pesan ke Exchange, yang kemudian merutekannya ke antrean utama.
- Pesan disimpan di RAM (atau di-page ke disk jika memori penuh) menunggu giliran dikonsumsi.
- Konsumen mengambil pesan dari antrean. Status pesan berubah dari
ReadymenjadiUnacknowledged. - Setelah konsumen sukses memproses pesan dan mengirimkan konfirmasi penerimaan kembali ke broker (
basic.ack), broker RabbitMQ secara aktif akan menghapus fisik pesan tersebut dari penyimpanan disk dan memori RAM.
Karakteristik utama dari model ini adalah Destructive Read. Pesan hanya ada di dalam antrean selama pesan tersebut belum diproses. Setelah sukses dibaca dan di-ACK, pesan tersebut lenyap selamanya dari broker. RabbitMQ dirancang sebagai sistem pengantrean murni di mana antrean yang ideal di lingkungan produksi adalah antrean yang selalu kosong (atau mendekati nol), menandakan seluruh pekerjaan sukses diproses dengan cepat.
Model Log Kafka: Append-Only Immutable Log #
Apache Kafka membuang konsep antrean tradisional seutuhnya. Kafka mendesain topologi penyimpanannya berbasis konsep Append-Only Immutable Log (Log Terdistribusi yang Hanya Bisa Ditambah dan Tidak Dapat Diubah). Di dalam Kafka, wadah penampung pesan tidak disebut sebagai antrean, melainkan Topic Partition (Partisi Topik).
flowchart TD
Producer["Producer"] --> Log["\"Log Partisi Kafka (Append-Only)<br>[Pesan 0"][Pesan 1][Pesan 2][Pesan 3]"]
Log --> Consumer["Consumer"]
Consumer -. "Membaca via Offset" .-> LogAlur hidup pesan di Kafka berjalan sebagai berikut:
- Produsen menerbitkan event ke broker Kafka. Pesan baru akan selalu ditambahkan di bagian paling akhir berkas log partisi (append-only).
- Pesan ditulis ke disk secara berurutan (sequential write) dan bersifat kekal (immutable). Pesan tidak dapat diubah ataupun dihapus di tengah jalan.
- Konsumen terhubung ke partisi topik dan membaca pesan secara linear dari kiri ke kanan.
- Ketika konsumen selesai memproses pesan, Kafka tidak menghapus pesan tersebut dari disk. Pesan tetap tersimpan dengan aman di dalam log. Konsumen hanya perlu memperbarui catatan posisinya sendiri yang disebut Offset (indeks penunjuk urutan pesan yang sedang dibaca).
Mekanisme ini menerapkan prinsip Non-Destructive Read. Pesan yang telah dibaca oleh konsumen A tetap tersedia secara utuh untuk dibaca oleh konsumen B, konsumen C, atau bahkan dibaca ulang oleh konsumen A dari awal. Penghapusan data di Kafka tidak diatur berdasarkan status ACK konsumen, melainkan diatur secara global melalui Retention Policy (Kebijakan Retensi), misalnya data otomatis dihapus jika sudah berumur 7 hari atau jika ukuran file log partisi telah melebihi batas 100 GB.
Perbandingan Arsitektur: Smart Broker vs Smart Consumer #
Perbedaan model penyimpanan di atas secara langsung melahirkan perbedaan filosofi tanggung jawab sistem. Kita membedakannya sebagai arsitektur Smart Broker / Dumb Consumer pada RabbitMQ dan Dumb Broker / Smart Consumer pada Kafka.
Smart Broker / Dumb Consumer (RabbitMQ) #
Di RabbitMQ, broker memegang kendali penuh atas kecerdasan sistem (Smart Broker). Broker bertanggung jawab memantau seluruh status pesan secara real-time. Broker harus melacak:
- Pesan mana saja yang sedang dipegang oleh konsumen mana (status
Unacknowledged). - Mengelola timeout detak jantung koneksi konsumen untuk melakukan requeue otomatis jika konsumen mati.
- Mengelola prioritas antrean, perutean dinamis via exchange, dan pengalihan ke Dead Letter Exchange (DLX).
Sebaliknya, aplikasi konsumen kita bertindak secara pasif (Dumb Consumer). Konsumen tidak perlu tahu di mana pesan disimpan di disk, berapa jumlah total pesan di antrean, atau pesan mana yang harus diambil berikutnya. Konsumen hanya perlu membuka koneksi, mendengarkan channel, menerima pesan yang didorong (pushed) oleh broker, memprosesnya, dan mengirimkan ACK balik.
Konsekuensi Performa: Karena broker RabbitMQ harus mengelola state transaksional yang sangat granular untuk setiap pesan individu di seluruh antrean, beban kerja memori RAM dan CPU broker meningkat secara linier seiring dengan bertambahnya jumlah pesan yang menumpuk. Menimbun jutaan pesan di antrean aktif RabbitMQ akan menurunkan performa throughput karena broker sibuk mengelola metadata state pesan tersebut.
Dumb Broker / Smart Consumer (Kafka) #
Di Kafka, broker dirancang sesederhana mungkin (Dumb Broker). Broker Kafka hanya bertindak sebagai sistem penyimpanan log linear berkinerja tinggi yang bertugas menerima data dari produsen dan melayani request pembacaan byte dari konsumen pada offset tertentu. Broker tidak melacak pesan mana yang sudah dibaca oleh siapa secara individual.
Seluruh kecerdasan dipindahkan ke sisi aplikasi konsumen (Smart Consumer). Konsumen harus secara aktif melacak posisi membacanya sendiri (offset tracking), mendeteksi jika terjadi kegagalan, mengelola proses rebalancing partisi saat ada instance konsumen baru yang bergabung ke dalam grup, dan secara berkala men-commit posisi offset terbarunya ke broker.
Konsekuensi Performa: Karena broker Kafka tidak perlu mengelola state ACK per pesan (hanya perlu mencatat satu angka integer offset per consumer group per partisi), beban kerja broker sangatlah ringan. Performa broker Kafka tidak akan menurun sedikit pun meskipun kita menimbun miliaran pesan historis selama berbulan-bulan di dalam log disk. Kafka dioptimalkan untuk performa throughput ekstrem.
Perbandingan Fitur Komparatif #
Untuk mempermudah pemetaan arsitektur, berikut adalah tabel perbandingan fitur komparatif antara RabbitMQ dan Kafka:
| Dimensi Arsitektur | RabbitMQ | Apache Kafka |
|---|---|---|
| Model Dasar | Antrean FIFO tradisional. | Distributed append-only log. |
| Mekanisme Baca | Destructive Read (Pesan dihapus setelah di-ACK). | Non-destructive Read (Pesan dipertahankan di log). |
| Penyebaran Beban | Smart Broker melacak status per pesan. | Smart Consumer melacak offset membacanya sendiri. |
| Pola Konsumsi | Push-based (Broker mendorong pesan ke konsumen). | Pull-based (Konsumen menarik pesan dari broker). |
| Replay Historis | Tidak didukung secara native (harus pakai tipe Stream). | Didukung secara native dengan melakukan offset rewind. |
| Performa Backlog | Menurun jika terjadi penumpukan pesan yang besar. | Konsisten stabil tanpa memedulikan ukuran file log. |
| Routing Pesan | Sangat dinamis dan kompleks (Topic, Headers, AE). | Terbatas pada perutean statis berbasis partisi topik. |
Perilaku Multi-Konsumen: Competing Consumers vs Consumer Group #
Cara kedua platform mendistribusikan pesan kepada beberapa konsumen paralel sangatlah berbeda dan memengaruhi bagaimana kita melakukan scaling aplikasi di lingkungan produksi.
RabbitMQ: Competing Consumers Pattern #
RabbitMQ mendukung pola persaingan konsumen (Competing Consumers) secara dinamis pada satu antrean tunggal. Kita dapat menghubungkan 1, 10, atau 100 instance konsumen secara paralel ke satu antrean yang sama.
┌───> [ Konsumen A ]
│
[ Antrean Utama ] ├───> [ Konsumen B ] (Round-Robin)
│
└───> [ Konsumen C ]
Broker RabbitMQ secara otomatis akan mendistribusikan pesan-pesan yang masuk secara bergiliran (round-robin) kepada konsumen-konsumen yang sedang aktif. Jika Konsumen A sedang sibuk memproses pesan berat, broker akan mengalihkan pesan berikutnya ke Konsumen B atau C yang sedang menganggur (idle), sesuai dengan batas kapasitas prefetch QoS yang kita tetapkan.
Pola ini sangat fleksibel karena kita dapat melakukan scaling horizontal (menambah atau mengurangi instance konsumen) secara instan kapan saja tanpa perlu mengubah konfigurasi antrean di sisi broker.
Kafka: Consumer Group & Partition Read Pattern #
Kafka menerapkan aturan pembagian kerja yang jauh lebih ketat menggunakan konsep Consumer Group dan Partitioning. Beban kerja di dalam Kafka tidak dibagi di tingkat pesan individual, melainkan di tingkat partisi topik.
Aturan baku di Kafka menetapkan bahwa satu partisi log di dalam topik hanya boleh dibaca oleh maksimal satu konsumen aktif di dalam satu Consumer Group pada satu waktu.
[ Topik Kafka ]
┌──────────────┐
│ Partisi 0 │ ───────────> [ Konsumen A ]
├──────────────┤
│ Partisi 1 │ ───────────> [ Konsumen B ]
├──────────────┤
│ Partisi 2 │ ──┐
├──────────────┤ └────────> [ Konsumen C ]
│ Partisi 3 │ ──┘
└──────────────┘
Jika kita memiliki Topik dengan 4 partisi, kita dapat menghubungkan hingga maksimal 4 konsumen paralel di dalam satu grup untuk membagi beban kerja secara merata (1 konsumen per partisi).
- Jika kita menambah konsumen ke-5, konsumen baru tersebut akan menganggur (idle) dan tidak mendapatkan data sama sekali karena seluruh partisi sudah terkunci oleh konsumen yang ada.
- Jika kita hanya memiliki 2 konsumen untuk 4 partisi, maka masing-masing konsumen akan bertanggung jawab membaca dari 2 partisi sekaligus.
Skalabilitas horizontal di Kafka dibatasi secara keras oleh jumlah partisi topik yang kita deklarasikan di awal. Kita tidak bisa melakukan scaling konsumen secara dinamis melebihi jumlah partisi tanpa melakukan perubahan konfigurasi partisi topik di broker yang memicu proses rebalancing data yang cukup berat.
Dampak Terhadap Jaminan Urutan (Ordering Guarantees) #
Jaminan urutan pesan (ordering) adalah aspek kritis lainnya yang membedakan kedua teknologi ini.
Urutan Pesan di RabbitMQ #
Di RabbitMQ, jaminan urutan FIFO (First-In, First-Out) hanya berlaku mutlak jika dan hanya jika antrean kita hanya memiliki satu konsumen aktif tunggal dan tidak ada kesalahan pemrosesan. Begitu kita menggunakan competing consumers (banyak konsumen paralel), jaminan urutan FIFO di tingkat pemrosesan akan langsung pecah karena variasi kecepatan eksekusi thread masing-masing konsumen.
Selain itu, jika terjadi kegagalan pemrosesan sementara dan konsumen mengirimkan sinyal requeue = true, pesan yang gagal tersebut akan diletakkan kembali di kepala antrean, mengakibatkan pesan tersebut diproses setelah pesan-pesan berikutnya yang sudah terlanjur dikonsumsi oleh konsumen lain.
Urutan Pesan di Kafka #
Kafka menjamin urutan pesan secara mutlak di tingkat partisi log. Selama produsen mengirimkan pesan dengan menyertakan Partition Key yang sama (misalnya ID pengguna user_123), broker Kafka menjamin pesan-pesan tersebut akan selalu ditulis ke partisi yang sama dan dalam urutan kronologis yang presisi sesuai waktu kedatangan.
Karena partisi tersebut dikunci hanya untuk dibaca oleh satu instansi konsumen di dalam Consumer Group, konsumen dijamin akan memproses pesan-pesan user_123 tersebut secara serial dan berurutan dari pesan pertama hingga terakhir tanpa ada risiko tumpang tindih urutan (out-of-order), bahkan ketika proses retry terjadi di sisi konsumen.
Implementasi Kode Go (Golang) #
Untuk memperjelas perbedaan penanganan state pada kode aplikasi, berikut adalah perbandingan implementasi penulisan kode konsumen di bahasa Go untuk RabbitMQ (menggunakan amqp091-go) dan Kafka (menggunakan library github.com/segmentio/kafka-go). Perhatikan bagaimana masing-masing kode mengelola siklus hidup pesan.
1. Konsumer RabbitMQ (Destructive ACK Lifecycle) #
Pada konsumen RabbitMQ, kita harus mengirimkan sinyal ACK secara manual agar broker menghapus pesan dari antreannya.
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
amqp "github.com/rabbitmq/amqp091-go"
)
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
if err != nil {
log.Fatalf("Koneksi RabbitMQ gagal: %v", err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
log.Fatalf("Gagal membuka channel: %v", err)
}
defer ch.Close()
// Mendaftarkan konsumen dengan manual ACK aktif (autoAck = false)
msgs, err := ch.Consume(
"order.main.queue", // nama antrean
"rabbitmq-worker", // consumer tag
false, // autoAck: false (manual ACK wajib)
false, // exclusive
false,
false,
nil,
)
if err != nil {
log.Fatalf("Gagal consume: %v", err)
}
go func() {
for d := range msgs {
log.Printf("[RABBITMQ] Memproses data: %s", string(d.Body))
// Simulasi proses bisnis sukses
// Setelah Ack dipanggil, broker RabbitMQ langsung menghapus pesan ini dari disk/RAM
err := d.Ack(false)
if err != nil {
log.Printf("Gagal mengirimkan ACK: %v", err)
}
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
}
2. Konsumer Kafka (Non-Destructive Offset Commit Lifecycle) #
Pada konsumen Kafka, pesan dibaca secara linear dari log disk. Setelah sukses memproses pesan, kita tidak menghapus pesan tersebut dari broker, melainkan kita memperbarui catatan index offset pembacaan kita di broker.
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"github.com/segmentio/kafka-go"
)
func main() {
// Inisialisasi pembaca log Kafka (Reader)
r := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{"localhost:9092"},
GroupID: "order-consumer-group", // Identitas Consumer Group
Topic: "order-events-topic",
MinBytes: 10e3, // 10KB
MaxBytes: 10e6, // 10MB
QueueCapacity: 100,
})
defer r.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
for {
// 1. FetchMessage membaca data byte dari offset saat ini tanpa melakukan commit offset
msg, err := r.FetchMessage(ctx)
if err != nil {
log.Printf("Gagal membaca event Kafka: %v", err)
break
}
log.Printf("[KAFKA] Membaca Offset %d pada Partisi %d, Payload: %s",
msg.Offset, msg.Partition, string(msg.Value))
// Simulasi proses bisnis sukses
// 2. CommitMessages memperbarui catatan offset kita di broker secara aman.
// Event asli di log disk Kafka tetap tersimpan secara kekal dan tidak terhapus.
err = r.CommitMessages(ctx, msg)
if err != nil {
log.Printf("Gagal commit offset Kafka: %v", err)
}
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
}
Anti-Pattern vs Solusi Praktis #
Kesalahan dalam memahami model penyimpanan ini sering kali berujung pada kegagalan operasional sistem. Berikut adalah salah satu anti-pattern yang paling sering dijumpai:
Anti-Pattern: Memilih Kafka untuk Kebutuhan Task Queue / Work Distribution Hanya karena Klaim Performa #
Menggunakan Kafka sebagai platform antrean tugas di mana sistem membutuhkan fitur seperti:
- Membatalkan pemrosesan tugas individu secara dinamis.
- Mengalihkan satu pesan gagal ke Dead Letter Queue (DLQ) secara instan tanpa menghentikan pemrosesan pesan di belakangnya.
- Menggunakan prioritas pesan (message priority) untuk mempercepat tugas penting di tengah antrean.
Mengapa ini salah? #
Karena Kafka menggunakan model append-only log yang diakses secara linear, Kafka tidak mendukung fitur-fitur di atas secara native.
- Di Kafka, kita tidak bisa menghapus atau melompati satu pesan gagal di tengah partisi secara dinamis tanpa memajukan seluruh offset konsumen group, yang berarti seluruh pesan di belakang pesan gagal tersebut akan terpaksa ikut terhambat (Head-of-Line Blocking di level konsumen).
- Kafka juga tidak memiliki konsep prioritas pesan bawaan karena data ditulis secara linear di disk sekuensial.
- Memaksakan fitur-fitur ini di Kafka akan memaksa kita menulis kode aplikasi yang sangat kompleks di tingkat konsumen (misalnya membuat topik-topik delay buatan), yang rentan memicu bug dan menurunkan keandalan sistem.
Solusi Praktis #
Gunakan RabbitMQ jika domain masalah kita adalah Work Distribution atau Task Processing, di mana pesan adalah instruksi tugas mandiri yang harus diproses sekali secara fleksibel, memiliki prioritas bervariasi, dan segera dibuang setelah selesai. Gunakan Kafka hanya jika domain masalah kita adalah Event Streaming, di mana kita perlu menganalisis jutaan event log sekuensial, membutuhkan retensi jangka panjang, dan memerlukan pemrosesan ulang data historis (replay).
Ringkasan #
- Model Antrean RabbitMQ — Menggunakan model Destructive FIFO Queue, di mana pesan secara aktif dihapus dari broker segera setelah dikonfirmasi sukses (
basic.ack) oleh konsumen.- Model Antrean Kafka — Menggunakan model Append-Only Immutable Log, di mana pesan disimpan secara kekal di disk berdasarkan batas retensi waktu/ukuran, dan tidak dihapus pasca dibaca oleh konsumen.
- Pembagian Tanggung Jawab State — RabbitMQ bertindak sebagai Smart Broker yang melacak status ACK per pesan. Kafka bertindak sebagai Dumb Broker yang hanya melayani pembacaan byte data, memindahkan kecerdasan pelacakan offset ke sisi konsumen (Smart Consumer).
- Scaling Horizontal — RabbitMQ menggunakan Competing Consumers yang dinamis dan fleksibel. Kafka membatasi konkurensi di tingkat partisi topik (maksimal 1 konsumen per partisi per consumer group).
- Jaminan Urutan FIFO — RabbitMQ menjamin FIFO per antrean selama konsumen tunggal aktif. Kafka menjamin urutan pesan secara mutlak per partisi log melalui penguncian partisi ke konsumen.
- Rekomendasi Pemilihan — Pilih RabbitMQ untuk pemrosesan tugas independen (task processing) dengan routing dan retry kompleks. Pilih Kafka untuk analisis aliran data besar (event streaming) dan data pipeline yang membutuhkan fitur replay historis.
← Sebelumnya: Retry Pattern Berikutnya: Use Case Comparison →