Throughput & Scalability #
Dalam perancangan arsitektur microservices tingkat lanjut (advanced microservices architecture), performa sistem messaging tidak dapat dinilai hanya dari angka-angka hasil benchmark mentah yang tersebar di internet. Setiap platform messaging dirancang dengan serangkaian keputusan kompromi arsitektural (architectural trade-offs) yang sengaja dipilih untuk mengoptimalkan dimensi kinerja tertentu. Apache Kafka sengaja dioptimalkan untuk memproses data dalam volume raksasa dengan throughput ekstrem, sedangkan RabbitMQ dirancang untuk memberikan fleksibilitas perutean tinggi dengan jaminan latensi rendah yang konsisten untuk pesan individu.
Memahami bagaimana kedua platform ini mengelola penskalaan horizontal (horizontal scalability) dan kinerja pemrosesan (throughput) sangat penting untuk mencegah terjadinya bottleneck infrastruktur saat beban lalu lintas data melonjak tajam. Artikel ini akan membedah secara mendalam perbandingan model skalabilitas serta mekanisme internal yang membuat Kafka mampu mencapai throughput jutaan pesan per detik, dibandingkan dengan karakteristik performa RabbitMQ.
Model Skalabilitas Horizontal #
Skalabilitas horizontal mendefinisikan seberapa mudah dan efisien sebuah sistem dalam meningkatkan kapasitas pemrosesan data dengan cara menambahkan lebih banyak node server (broker) atau instansi aplikasi konsumen ke dalam klaster.
1. Model Penskalaan RabbitMQ #
RabbitMQ menawarkan penskalaan konsumen yang sangat dinamis dan fleksibel. Di sisi antrean, kita dapat menghubungkan sejumlah konsumen paralel secara bebas kapan saja (Competing Consumers Pattern).
Namun, di tingkat broker, penskalaan horizontal RabbitMQ memiliki batas atas yang dipengaruhi oleh overhead pelacakan status (state tracking). Karena broker RabbitMQ harus mencatat status transaksional ACK/NACK untuk setiap pesan individu di memori RAM, menambahkan lebih banyak broker ke klaster tidak secara linier meningkatkan performa antrean tunggal.
Untuk menskalakan antrean RabbitMQ, kita harus:
- Membagi antrean secara logis menjadi beberapa antrean terpisah (queue sharding).
- Menggunakan Quorum Queue untuk replikasi data konsensus Raft, yang sayangnya menambah beban overhead jaringan dan I/O disk antar node pemimpin (leader) dan pengikut (follower).
2. Model Penskalaan Kafka #
Kafka mengimplementasikan model penskalaan horizontal yang sangat terstruktur dan linear melalui partisi topik (Partition Scaling). Skalabilitas di Kafka diatur secara tegas di tingkat desain partisi sejak awal.
Di sisi broker, menambahkan node baru ke klaster Kafka akan secara langsung meningkatkan kapasitas penyimpanan dan throughput. Kafka dapat mendistribusikan partisi-partisi dari satu topik secara merata ke seluruh broker baru tersebut.
Di sisi konsumen, batas maksimal konkurensi grup dibatasi secara keras oleh jumlah partisi topik. Menambahkan konsumen melebihi jumlah partisi pada topik yang sama di dalam satu consumer group tidak akan membantu, karena konsumen tambahan tersebut hanya akan menganggur (idle) tanpa mendapatkan partisi data untuk dibaca. Penskalaan konsumen di Kafka menuntut perencanaan partisi topik yang matang di awal perancangan.
Mekanisme Throughput Ekstrem di Apache Kafka #
Apache Kafka dirancang khusus untuk memproses data dalam skala petabyte dengan efisiensi luar biasa. Kemampuan Kafka untuk memproses jutaan event per detik pada hardware standar disokong oleh tiga pilar teknologi internal berikut:
1. Sekuensial Disk I/O (Sequential Disk Access) #
Banyak developer berasumsi bahwa menulis data ke disk penyimpanan (hard drive/SSD) selalu berjalan lambat dibandingkan menulis ke RAM. Asumsi ini tidak sepenuhnya benar. Menulis data ke disk akan terasa sangat lambat jika kita melakukan pencarian data secara acak (random disk access), karena kepala fisik hard drive harus bergerak mencari sektor disk yang tersebar.
Kafka membuang pencarian acak tersebut dengan menulis event log secara Sequensial (Append-Only). Data baru akan selalu ditambahkan di akhir file log partisi. Kecepatan menulis data secara sekuensial pada disk modern sebenarnya sangatlah cepat, hampir setara dengan kecepatan menulis ke memori RAM, karena tidak ada overhead pergeseran kepala disk.
2. Optimalisasi OS Pagecache #
Kafka meminimalkan interaksi langsung aplikasi dengan disk fisik. Saat produsen mengirim data, broker Kafka tidak langsung memanggil perintah fsync yang memblokir thread untuk menulis data ke piringan disk secara sinkron.
Broker Kafka menulis pesan ke memori halaman sistem operasi yang disebut Pagecache. Sistem operasi Linux secara asinkron akan melakukan proses pembilasan (flushing) data dari pagecache ke disk fisik di latar belakang (background threads).
Ketika konsumen meminta data, jika data tersebut baru saja ditulis oleh produsen, data tersebut kemungkinan besar masih tersimpan hangat di dalam pagecache RAM. Broker Kafka dapat langsung menyajikan data tersebut dari RAM OS ke konsumen tanpa perlu memicu operasi pembacaan disk fisik sama sekali (zero disk read overhead).
3. Mekanisme Zero-Copy Transfer (sendfile)
#
Dalam arsitektur web konvensional (termasuk RabbitMQ), saat data pesan dibaca dari disk untuk dikirim ke socket buffer jaringan, data harus melewati beberapa kali siklus penyalinan memori dan perpindahan konteks CPU (context switching):
flowchart LR
Disk["Disk"] --> Pagecache["Kernel Pagecache"]
Pagecache -->|"Copy"| UserSpace["User Space (JVM/Erlang Heap)"]
UserSpace -->|"Copy"| Socket["Socket Buffer"]
Socket --> NIC["NIC Buffer"]Proses ini sangat boros CPU karena data yang sama disalin berkali-kali lintas ruang memori kernel dan user space.
Kafka menghilangkan overhead ini menggunakan system call kernel Linux bernama sendfile, yang mengaktifkan teknologi Zero-Copy. Dengan zero-copy, broker Kafka menginstruksikan sistem operasi untuk langsung menyalin byte data dari pagecache kernel langsung ke buffer kartu jaringan (NIC Buffer) tanpa memuat data tersebut ke memori aplikasi JVM space sama sekali:
flowchart LR
Disk["Disk"] --> Pagecache["Kernel Pagecache"]
Pagecache -->|"Direct Copy via DMA/sendfile"| NIC["NIC Buffer (Kartu Jaringan)"]Mekanisme zero-copy ini mengurangi perpindahan konteks CPU dari 4 kali menjadi 2 kali, dan meniadakan penyalinan data di memori aplikasi, sehingga menghemat konsumsi CPU dan RAM broker secara drastis saat melayani jutaan request pembacaan data.
Karakteristik Performa dan Latensi RabbitMQ #
Dengan segala kecanggihan optimalisasi I/O Kafka di atas, mengapa RabbitMQ tetap menjadi pilihan populer? Jawabannya terletak pada dimensi Latensi.
RabbitMQ dirancang untuk mengutamakan latensi pengiriman pesan individu yang sekecil mungkin. Saat pesan masuk ke RabbitMQ, broker yang bertindak sebagai Smart Broker akan langsung mengevaluasi tabel routing Mnesia di RAM dan sesegera mungkin mendorong (push) pesan tersebut ke koneksi socket TCP konsumen yang sedang menunggu secara aktif.
Proses pengiriman ini berjalan secara instan untuk setiap pesan individu tanpa menunggu akumulasi batch data. Hasilnya, RabbitMQ mampu memberikan latensi pengantrean yang sangat konsisten rendah di level milidetik tunggal (sub-millisecond latency) untuk lalu lintas data normal.
Sebaliknya, Kafka mengandalkan teknik Batching (pengelompokan pesan masal) untuk mencapai throughput tinggi. Produsen Kafka menahan pesan di memori lokalnya selama beberapa milidetik (misal disetel linger.ms = 5) untuk mengumpulkan 100 atau 1.000 pesan sebelum dikirim secara kolektif ke broker. Hal ini mengorbankan latensi pesan individu demi mendapatkan throughput total sistem yang jauh lebih besar.
Tabel Perbandingan Latensi & Throughput #
Berikut adalah tabel perbandingan karakteristik performa dan skalabilitas antara RabbitMQ dan Kafka:
| Dimensi Performa | RabbitMQ | Apache Kafka |
|---|---|---|
| Throughput Maksimum | Sedang (Ribuan hingga puluh ribuan pesan per detik). | Ekstrem (Jutaan pesan per detik). |
| Latensi Pesan Individu | Sangat Rendah (Sub-milidetik, konsisten rendah). | Sedang (Dipengaruhi oleh konfigurasi batching linger.ms). |
| Model Pengiriman Data | Push-based (Instan tanpa akumulasi batch). | Pull-based (Batching massal di sisi produsen & konsumen). |
| Penyimpanan Memory | Paging dinamis ke disk jika RAM penuh. | Memanfaatkan OS Pagecache secara intensif. |
| Teknologi Transfer | Menyalin data lintas user space dan kernel space. | Menggunakan Zero-Copy (sendfile) untuk efisiensi CPU. |
| Overhead Metadata | Tinggi karena broker melacak state ACK per pesan. | Rendah karena broker hanya melacak satu integer offset. |
Implementasi Kode Go (Golang) #
Berikut adalah contoh program benchmark sederhana dalam bahasa Go yang mengilustrasikan perbedaan taktis antara penerbitan pesan individual dengan latensi rendah pada RabbitMQ vs pengiriman pesan berkelompok (batching) throughput tinggi pada Kafka.
1. Penerbitan Pesan Instan di RabbitMQ (Latensi Rendah) #
Di RabbitMQ, pesan dikirimkan secara instan satu per satu untuk langsung memicu pemrosesan tanpa delay akumulasi.
package main
import (
"context"
"log"
"time"
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()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
start := time.Now()
totalMessages := 5000
for i := 0; i < totalMessages; i++ {
// Setiap pesan diterbitkan secara instan satu per satu
err = ch.PublishWithContext(ctx,
"", // default exchange
"payment.normal.queue", // routing key / queue name
false,
false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte("Instantly sent message payload"),
},
)
if err != nil {
log.Printf("Gagal publish ke RabbitMQ: %v", err)
}
}
duration := time.Since(start)
log.Printf("[RABBITMQ] Selesai mengirim %d pesan dalam %v (Rata-rata %.2f pesan/detik)",
totalMessages, duration, float64(totalMessages)/duration.Seconds())
}
2. Penerbitan Pesan Berkelompok di Kafka (Throughput Tinggi) #
Di Kafka, produsen secara otomatis mengelompokkan pesan ke dalam satu request batch (BatchSize dan BatchTimeout) sebelum mengirimkannya secara kolektif ke broker untuk menghemat I/O jaringan.
package main
import (
"context"
"log"
"time"
"github.com/segmentio/kafka-go"
)
func main() {
// Konfigurasi Writer Kafka dengan fitur batching aktif
w := &kafka.Writer{
Addr: kafka.TCP("localhost:9092"),
Topic: "payment-telemetry",
Balancer: &kafka.LeastBytes{},
BatchSize: 1000, // Kirim setelah mengumpul 1.000 pesan
BatchTimeout: 5 * time.Millisecond, // Atau kirim jika sudah menunggu 5 ms
}
defer w.Close()
ctx := context.Background()
start := time.Now()
totalMessages := 5000
messages := make([]kafka.Message, totalMessages)
for i := 0; i < totalMessages; i++ {
messages[i] = kafka.Message{
Value: []byte("Batch sent message payload telemetry data FX pricing info"),
}
}
// Menulis seluruh pesan secara batch massal
err := w.WriteMessages(ctx, messages...)
if err != nil {
log.Fatalf("Gagal menulis ke Kafka: %v", err)
}
duration := time.Since(start)
log.Printf("[KAFKA] Selesai mengirim %d pesan (Batching) dalam %v (Rata-rata %.2f pesan/detik)",
totalMessages, duration, float64(totalMessages)/duration.Seconds())
}
Anti-Pattern vs Solusi Praktis #
Merancang partisi di Kafka secara sembarangan merupakan kesalahan arsitektur umum (anti-pattern) yang dapat menurunkan stabilitas sistem.
Anti-Pattern: Menyetel Jumlah Partisi Secara Berlebihan (Over-Partitioning) Tanpa Rencana #
Mendeklarasikan topik Kafka dengan jumlah ribuan partisi (misalnya 5.000 partisi untuk satu topik) pada klaster kecil hanya dengan asumsi “semakin banyak partisi, semakin cepat skalabilitasnya”.
Mengapa ini salah? #
Meskipun partisi adalah unit skalabilitas Kafka, jumlah partisi yang terlampau banyak pada klaster kecil memicu beberapa bahaya serius:
- Overhead Metadata File: Setiap partisi dipetakan ke direktori folder fisik di OS yang berisi berkas indeks dan segmen log data. Memiliki ribuan partisi berarti broker harus membuka ribuan file handler secara bersamaan, memicu batas limit sistem operasi (open files limit error).
- Failover Latency yang Sangat Tinggi: Setiap partisi dikontrol oleh satu broker bertindak sebagai Leader. Jika salah satu broker Kafka di klaster mengalami crash, broker-broker lainnya harus bernegosiasi untuk memilih Leader baru bagi ratusan partisi yang ditinggalkan. Proses koordinasi metadata ini (melalui KRaft atau ZooKeeper) akan memakan waktu lama, membekukan proses konsumsi data, dan menurunkan performa klaster secara drastis selama beberapa menit.
Solusi Praktis #
Rencanakan jumlah partisi secara terukur sejak awal menggunakan formula berikut:
$$Partisi = \max\left(\frac{Target_Throughput}{Throughput_Producer}, \frac{Target_Throughput}{Throughput_Consumer}\right)$$
Jika satu konsumen aplikasi kita mampu memproses 10 MB data per detik, dan target throughput sistem kita adalah 40 MB data per detik, maka kita cukup mendeklarasikan topik dengan 4 partisi. Angka ini dinilai sangat optimal untuk melayani skalabilitas horizontal konsumen tanpa mengotori metadata broker.
Ringkasan #
- Kompromi Kinerja — RabbitMQ dioptimalkan untuk latensi rendah yang konsisten pada pesan individu. Kafka dioptimalkan untuk performa throughput ekstrem pada volume data raksasa.
- Pilar Performa Kafka — Kafka mencapai throughput jutaan pesan per detik menggunakan sekuensial disk I/O, pemanfaatan OS pagecache RAM, dan transfer memori Zero-Copy (
sendfile).- Overhead RAM RabbitMQ — Broker RabbitMQ melacak state ACK/NACK granular per pesan, sehingga beban memori RAM broker rentan membengkak jika terjadi penumpukan backlog pesan besar.
- Batching vs Instan — Kafka menggunakan batching produsen/konsumen untuk menghemat bandwidth jaringan dengan konsekuensi penambahan sedikit latensi. RabbitMQ mengirim pesan secara instan (push-based) tanpa delay batching.
- Scaling Terikat Partisi — Skalabilitas konsumen Kafka dibatasi maksimal sebanyak jumlah partisi topik. RabbitMQ bebas menambah competing consumers dinamis tanpa batasan topologi antrean.
- Hindari Over-Partitioning — Jangan mendeklarasikan partisi topik dalam jumlah ribuan secara sembarangan karena akan memicu overhead open file OS dan memperlambat failover leader broker saat crash.
← Sebelumnya: Replay Capability Berikutnya: RabbitMQ as Database →