Queueing #
Dalam perjalanan sebuah pesan melalui broker RabbitMQ (message lifecycle), setelah pesan diterbitkan oleh produsen (Publishing) dan berhasil dirutekan oleh Exchange (Routing & Binding), pesan tersebut memasuki fase yang sering kali dianggap pasif namun sebenarnya sangat krusial bagi stabilitas dan performa sistem: fase Queueing (Pengantrean).
Fase Pengantrean adalah tahap ketika pesan disimpan secara aman di dalam antrean (queue) tujuan, berada dalam status siap dikonsumsi (Ready state), dan menunggu untuk dikirimkan ke aplikasi konsumen (delivery). Pada fase inilah seluruh beban beban kerja sistem (backlog) terakumulasi, manajemen memori RAM dan penyimpanan disk diuji secara ekstrem, serta koordinasi konsensus klaster berjalan. Memahami dinamika internal pengantrean bukan sekadar mengetahui cara kerja antrean FIFO biasa, melainkan memahami bagaimana RabbitMQ mengelola memori Erlang, membagi status penyimpanan, mengatur batas kedaluwarsa, dan menerapkan proteksi aliran data (flow control) ketika sistem berada di bawah tekanan throughput yang tinggi.
Posisi Queueing dalam Siklus Hidup Pesan #
Fase Queueing terjadi tepat setelah broker memutuskan antrean tujuan pada tahap perutean, dan (jika pesan ditandai persisten) setelah bodi pesan berhasil diamankan ke media penyimpanan fisik.
flowchart TD
Routing["Tahap Routing Selesai"] --> Enqueue{"Proses Enqueue"}
subgraph StorageEngine["Storage Engine (Antrean)"]
Enqueue --> Ready["Status: Ready"]
Ready --> MemoryStore["RAM (q1, q2, q3, q4)"]
Ready --> DiskStore["Disk (Spill / Lazy)"]
end
Ready --> TTLCheck{"Evaluasi TTL / Expiration"}
TTLCheck -->|Expired| DLX["Buang atau Salurkan ke DLX"]
TTLCheck -->|Valid| Delivery["Dikirim ke Consumer (basic.deliver)"]
Delivery --> Unacked["Status: Unacknowledged"]Ketika pesan berada dalam fase Queueing, statusnya secara eksplisit ditandai sebagai Ready. Pesan tetap berada dalam kondisi ini sampai broker memilihnya untuk dikirimkan ke konsumen yang memenuhi syarat, yang kemudian mengubah status pesan tersebut menjadi Unacknowledged.
Anatomi Internal Penyimpanan Antrean: Classic Queue v1 vs v2 #
Untuk mengelola antrean pesan yang fleksibel namun tetap efisien, RabbitMQ Classic Queue membagi sistem penyimpanannya menjadi beberapa lapisan memori dan disk. Pada Classic Queue versi 1 (CQv1), hal ini diatur oleh komponen Erlang bernama backing_queue (khususnya modul rabbit_variable_queue).
1. Struktur Antrean CQv1: Lapisan q1 hingga q4
#
Modul rabbit_variable_queue membagi antrean di memori RAM dan disk menjadi 5 struktur data internal berbentuk antrean kecil guna mempertahankan urutan FIFO sambil meminimalkan latensi akses I/O:
q1: Antrean di memori RAM yang menampung pesan-pesan yang baru saja masuk ke antrean.q2: Antrean di memori RAM yang berisi pesan-pesan yang telah ditulis ke disk tetapi salinannya masih disimpan di RAM. Lapisan ini digunakan sebagai buffer sebelum pesan di-page out sepenuhnya dari RAM.delta: Antrean pesan yang hanya ada di disk. Payload pesan di dalamdeltatidak lagi memakan ruang RAM sama sekali, hanya menyisakan data indeks referensi.q3: Antrean pesan di memori RAM yang berisi pesan-pesan yang dibaca kembali dari disk (delta) sebelum dikirimkan ke konsumen.q4: Antrean di memori RAM yang berisi pesan-pesan yang siap dikirim langsung ke konsumen dari RAM.
flowchart LR
A["Pesan Masuk"] --> B["q1 (RAM)"] --> C["q2 (RAM)"] --> D["delta (DISK ONLY)"] --> E["q3 (RAM)"] --> F["q4 (RAM)"] --> G["Consumer"]Broker memindahkan pesan di antara lapisan-lapisan ini secara dinamis berdasarkan beban memori RAM. Jika RAM melimpah, pesan mengalir dengan cepat dari q1 langsung ke q4. Namun, jika RAM mulai penuh, pesan akan dipindahkan (paged out) dari q2 ke dalam file disk delta, dan kemudian dibaca kembali (paged in) ke q3 ketika konsumen siap memprosesnya.
2. Peningkatan pada Classic Queue v2 (CQv2) #
Sejak RabbitMQ versi 3.10, diperkenalkan arsitektur Classic Queue v2 (CQv2). CQv2 menyederhanakan mekanisme lima lapisan CQv1 dengan mengimplementasikan mesin penyimpanan log terpadu yang baru.
Pada CQv2, pesan tidak lagi dipindahkan secara bertahap melalui antrean RAM yang rumit. Sebagai gantinya, pesan langsung ditulis ke file log append-only di disk secara terpadu. CQv2 secara dramatis mengurangi konsumsi memori RAM dasar (baseline memory footprint) per antrean dan menyajikan kinerja throughput yang jauh lebih stabil, menghindari lonjakan latensi (latency spikes) yang sering terjadi pada CQv1 saat proses transisi memori RAM ke disk terjadi secara massal.
Perbandingan Penyimpanan: Classic, Lazy, dan Quorum Queue #
Setiap tipe antrean di RabbitMQ memiliki cara yang berbeda dalam mengelola fase Queueing ini. Pilihan tipe antrean sangat menentukan bagaimana data disimpan dan bagaimana konsensus klaster dicapai.
1. Classic Queue (Default) #
Classic Queue dirancang dengan filosofi RAM-first. Broker akan mencoba menahan pesan di dalam memori RAM sebanyak mungkin untuk mengejar performa latensi terendah.
- Karakteristik: Pesan transient hanya disimpan di RAM (kecuali jika terjadi memory pressure). Pesan persistent ditulis ke disk secara asinkron sambil tetap dipertahankan salinannya di memori RAM agar siap dikirim dengan cepat.
- Kelemahan: Jika terjadi penumpukan pesan (backlog) yang besar, konsumsi RAM akan membengkak, memaksa broker mengaktifkan proses paging disk yang intensif dan menurunkan performa secara drastis.
2. Lazy Queue #
Lazy Queue dirancang dengan filosofi Disk-first. Antrean ini secara proaktif memindahkan pesan langsung ke disk segera setelah pesan diterima oleh broker.
- Karakteristik: Pesan hanya dimuat ke memori RAM ketika benar-benar dibutuhkan oleh konsumen (ketika dikirim). Penggunaan RAM untuk antrean Lazy sangat kecil dan stabil, terlepas dari seberapa panjang backlog antrean tersebut.
- Skenario Terbaik: Sangat ideal untuk antrean dengan backlog pesan yang besar (misalnya jutaan pesan), atau untuk sistem pemrosesan batch yang mengumpulkan pesan dalam waktu lama sebelum diproses sekaligus.
3. Quorum Queue #
Quorum Queue adalah tipe antrean terdistribusi berbasis Raft Consensus yang memprioritaskan keamanan data ekstrem di atas segalanya.
- Karakteristik: Setiap pesan yang masuk ke Quorum Queue ditulis ke dalam berkas log Raft di disk lokal terlebih dahulu, kemudian direplikasikan ke node-node follower di dalam klaster.
- Mekanisme Enqueue: Proses enqueue baru dianggap selesai dan sah (committed) setelah mayoritas node replika klaster (kuorum) berhasil menulis log tersebut ke disk fisik mereka masing-masing. Urutan pesan (ordering) dijamin secara ketat oleh indeks log Raft global.
Dinamika State Antrean: Ready vs Unacknowledged #
Selama fase Queueing, RabbitMQ membedakan status pesan di dalam antrean menjadi dua kategori utama yang sangat penting untuk dipantau melalui dashboard monitoring atau metrik Prometheus.
1. Ready State #
Pesan dalam status Ready adalah pesan yang saat ini berada di dalam antrean penyimpanan, belum dikirimkan ke konsumen mana pun, dan siap untuk disalurkan segera setelah ada konsumen yang terhubung dan memiliki kapasitas menerima (prefetch quota).
- Penyimpanan: Pesan
Readydapat berada di RAM (q1,q2,q4) atau di disk (delta/ Lazy storage). - Indikator: Jumlah pesan
Readyyang terus meningkat adalah indikator utama terjadinya backlog sistem atau kegagalan pada aplikasi konsumen.
2. Unacknowledged (Unacked) State #
Pesan berubah status menjadi Unacknowledged saat broker telah mengirimkannya ke konsumen melalui protokol AMQP (basic.deliver), namun broker masih menunggu sinyal konfirmasi balik (basic.ack) dari konsumen tersebut.
- Penyimpanan: Payload pesan yang berstatus
Unackedtetap dipertahankan oleh broker di dalam memori RAM atau disk. Broker tidak boleh menghapus pesan ini dari penyimpanan fisiknya karena jika koneksi konsumen terputus sebelum mengirimkan ACK, broker wajib mengembalikan pesan tersebut ke statusReady(requeue) untuk dikirim ulang. - Batas: Jumlah maksimum pesan
Unackedyang dapat ditahan oleh konsumen per channel diatur oleh setelan QoS Prefetch Limit.
Garansi Urutan Pesan (Ordering Guarantees) dan Batasannya #
Secara default, RabbitMQ menjamin urutan pesan masuk yang keluar dari antrean mengikuti prinsip FIFO (First-In, First-Out) yang ketat. Jika produsen mempublikasikan pesan A kemudian pesan B ke antrean yang sama, maka konsumen akan menerima pesan A terlebih dahulu sebelum pesan B.
Namun, dalam sistem terdistribusi riil di lingkungan produksi, terdapat beberapa skenario yang dapat merusak atau mengubah garansi urutan FIFO ini:
1. Pengembalian Pesan ke Antrean (Requeueing) #
Jika konsumen menolak pesan (menggunakan basic.nack atau basic.reject) dengan menyetel parameter requeue = true, pesan tersebut akan dikembalikan ke dalam antrean.
- Dampak: RabbitMQ akan mencoba meletakkan kembali pesan yang ditolak tersebut ke posisi paling depan antrean (head of the queue). Jika saat itu ada pesan baru yang masuk, pesan yang di-requeue akan diproses ulang terlebih dahulu. Namun, jika ada beberapa konsumen paralel, urutan pemrosesan logis di aplikasi dapat menjadi tidak teratur karena konsumen lain mungkin sedang memproses pesan yang datang setelahnya.
2. Penggunaan Priority Queues (x-max-priority)
#
Ketika antrean dikonfigurasi sebagai Priority Queue, broker tidak lagi menggunakan antrean FIFO murni.
- Dampak: Broker akan memeriksa properti prioritas (
prioritypada header AMQP) dari setiap pesan yang masuk. Pesan dengan prioritas lebih tinggi akan disisipkan di depan pesan berprioritas rendah, melompati antrean yang sudah ada sebelumnya.
3. Penggunaan Multiple Consumers #
Meskipun broker mengirimkan pesan ke jaringan dalam urutan FIFO yang ketat, jika ada lebih dari satu instansi konsumen yang membaca dari antrean yang sama secara paralel:
- Dampak: Kecepatan pemrosesan di tiap instansi konsumen berbeda-beda (misalnya karena latensi jaringan atau beban CPU). Konsumen yang menerima pesan B mungkin menyelesaikan pemrosesan lebih cepat daripada konsumen yang memproses pesan A, sehingga di tingkat database akhir, urutan eksekusi tampak terbalik.
Mekanisme Masa Aktif Pesan: TTL (Time-To-Live) #
RabbitMQ memungkinkan kita untuk membatasi masa hidup suatu pesan di dalam antrean menggunakan mekanisme TTL. Timer TTL ini mulai dihitung secara aktif tepat ketika pesan masuk ke dalam fase Queueing.
Ada dua cara utama untuk menerapkan TTL di RabbitMQ:
1. Per-Queue TTL (x-message-ttl)
#
Batas kedaluwarsa diterapkan secara global pada tingkat konfigurasi antrean. Semua pesan yang masuk ke antrean tersebut akan memiliki masa aktif yang seragam.
- Optimasi Internal: Karena semua pesan memiliki TTL yang sama, urutan kedaluwarsa pesan pasti selaras dengan urutan FIFO antrean. Broker dapat dengan sangat efisien menghapus pesan yang kedaluwarsa hanya dengan memeriksa pesan yang berada di posisi paling depan antrean (head of the queue). Begitu pesan terdepan kedaluwarsa, broker menghapusnya, lalu memeriksa pesan berikutnya.
2. Per-Message TTL (Properti expiration)
#
Batas kedaluwarsa ditentukan secara individual oleh produsen untuk setiap pesan saat dipublikasikan.
- Kendala Performa: Karena pesan di tengah antrean bisa memiliki TTL yang lebih pendek daripada pesan di depannya, RabbitMQ Classic Queue tidak memindai seluruh antrean secara konstan untuk mencari pesan kedaluwarsa (karena operasi pemindaian memori ini terlalu mahal). Broker hanya akan mendeteksi dan membuang pesan kedaluwarsa ketika pesan tersebut secara alami mengalir sampai ke kepala antrean (head of the queue) dan siap dikirim ke konsumen.
- Dampak: Pesan yang sudah kedaluwarsa di tengah antrean akan tetap memakan kapasitas RAM/disk sampai pesan-pesan di depannya selesai dikonsumsi.
Kebijakan Batas Panjang (Max Length) dan Overflow Policy #
Untuk mencegah antrean tumbuh tanpa batas yang dapat menyebabkan kehabisan kapasitas disk atau memori RAM, kita dapat mengonfigurasi batasan fisik pada antrean menggunakan argumen:
x-max-length: Membatasi jumlah maksimum pesan yang boleh ada di antrean.x-max-length-bytes: Membatasi total akumulasi ukuran payload (dalam byte) dari seluruh pesan di antrean.
Ketika batas ini terlampaui saat proses enqueue, broker akan menjalankan kebijakan Overflow Policy yang kita tentukan:
flowchart TD
A["Antrean Penuh"] -->|"Mengevaluasi x-overflow argumen"| B{"x-overflow?"}
B -->|drop-head| C["Buang pesan tertua di kepala antrean"]
B -->|reject-publish| D["Kirim NACK ke produsen dan buang pesan baru"]
B -->|reject-publish-dlx| E["Kirim pesan baru ke DLX dan kirim NACK ke produsen"]1. drop-head (Default)
#
Broker akan menghapus secara instan pesan tertua yang berada di kepala antrean (head) untuk memberikan ruang bagi pesan baru yang masuk di ekor antrean. Jika dikonfigurasi dengan Dead Letter Exchange (DLX), pesan tertua yang dibuang tersebut akan dikirimkan ke DLX.
2. reject-publish
#
Broker akan menolak pesan baru yang mencoba masuk ke antrean yang sudah penuh. Broker akan membuang pesan baru tersebut dan mengirimkan sinyal NACK (Negatif Konfirmasi) kembali ke produsen melalui mekanisme Publisher Confirms. Ini memberi tahu produsen bahwa pesan gagal diantrekan karena kapasitas penuh.
3. reject-publish-dlx
#
Mirip dengan reject-publish, namun pesan baru yang ditolak oleh antrean penuh tersebut akan langsung dialihkan ke Dead Letter Exchange (DLX) yang terkonfigurasi, sementara produsen tetap menerima konfirmasi penolakan.
Backlog dan Pemicu Backpressure (Flow Control) #
Fase Queueing adalah barometer utama kesehatan sistem RabbitMQ. Ketika kecepatan publikasi pesan dari produsen jauh melampaui kecepatan konsumsi pesan oleh konsumen, terjadilah akumulasi backlog.
Backlog pesan yang besar memicu serangkaian proteksi internal di dalam broker:
1. Garbage Collection (GC) Erlang yang Intensif #
Setiap antrean dijalankan sebagai satu proses Erlang. Ketika memori proses membengkak karena menampung ribuan pesan, Erlang VM akan melakukan siklus Garbage Collection secara berkala untuk membebaskan RAM. Proses GC ini mengonsumsi resource CPU yang sangat besar dan dapat memblokir eksekusi pembacaan data antrean untuk sementara waktu.
2. Credit Flow Control #
Untuk mencegah broker kehabisan memori secara total saat terjadi backlog parah, RabbitMQ menggunakan algoritma Credit Flow Control:
- Proses antrean (
rabbit_amqqueue_process) membatasi pemberian “kredit” pemrosesan ke proses channel (rabbit_channel). - Proses channel yang kehabisan kredit akan berhenti membaca data dari proses pembaca koneksi TCP (
rabbit_reader). - Akibatnya, socket TCP koneksi produsen berhenti membaca data (TCP window size menyusut menjadi nol). Produsen akan merasakan koneksinya terhambat atau tertunda (blocked connection).
Mekanisme backpressure ini memastikan bahwa broker tidak akan pernah menerima data melebihi kemampuannya untuk mengamankan data tersebut ke disk atau RAM.
Implementasi Kode Go: Mengonfigurasi Atribut Antrean Lengkap #
Berikut adalah implementasi lengkap dalam bahasa Go untuk mendeklarasikan antrean Classic dengan mengonfigurasi batas TTL, batas panjang maksimum, kebijakan overflow reject-publish, serta menyetel antrean ke mode Lazy.
package main
import (
"context"
"log"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
func main() {
// 1. Membuka Koneksi ke Broker
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
if err != nil {
log.Fatalf("Gagal terhubung ke RabbitMQ: %s", err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
log.Fatalf("Gagal membuka channel: %s", err)
}
defer ch.Close()
// 2. Mengaktifkan Publisher Confirms
// Wajib aktif karena kita menggunakan kebijakan overflow 'reject-publish'
err = ch.Confirm(false)
if err != nil {
log.Fatalf("Gagal mengaktifkan Publisher Confirms: %s", err)
}
confirmChan := ch.NotifyPublish(make(chan amqp.Confirmation, 1))
// 3. Menentukan Argumen Konfigurasi Antrean (Fase Queueing)
args := amqp.Table{
// Mengatur TTL pesan di antrean menjadi 30 detik (30000 ms)
"x-message-ttl": int32(30000),
// Membatasi panjang antrean maksimum 1000 pesan
"x-max-length": int32(1000),
// Menentukan kebijakan overflow jika antrean penuh (menolak pesan baru)
"x-overflow": "reject-publish",
// Mengonfigurasi antrean agar beroperasi dalam mode Lazy (disk-first)
"x-queue-mode": "lazy",
}
queueName := "highly-controlled-queue"
// 4. Deklarasi Antrean dengan Argumen Kustom
_, err = ch.QueueDeclare(
queueName,
true, // durable
false, // auto-delete
false, // exclusive
false, // no-wait
args, // Menyuntikkan konfigurasi queueing kita!
)
if err != nil {
log.Fatalf("Gagal mendeklarasikan antrean: %s", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 5. Mengirimkan Pesan Uji Coba
payload := []byte(`{"event":"user_signup","timestamp":1781290382}`)
err = ch.PublishWithContext(ctx,
"", // Default Exchange
queueName,
false, // mandatory
false, // immediate
amqp.Publishing{
DeliveryMode: amqp.Persistent,
ContentType: "application/json",
Body: payload,
},
)
if err != nil {
log.Fatalf("Gagal mempublikasikan pesan: %s", err)
}
// 6. Menunggu Konfirmasi dari Broker
confirm := <-confirmChan
if confirm.Ack {
log.Println("✓ Pesan sukses diterima dan masuk ke antrean!")
} else {
// Jika antrean penuh dan x-overflow disetel reject-publish, broker mengirim NACK
log.Println("✗ Pesan ditolak oleh antrean (NACK) — kemungkinan batas max-length terlampaui!")
}
}
Anti-Pattern vs Solusi Praktis di Lingkungan Produksi #
Hindari beberapa kesalahan desain fase pengantrean berikut yang sering merusak performa klaster:
1. Menggunakan Per-Message TTL Secara Acak pada Classic Queue Panjang #
Mengirimkan pesan dengan nilai properti expiration yang sangat bervariasi secara acak ke dalam satu Classic Queue yang memiliki backlog jutaan pesan.
Mengapa ini salah? #
Karena RabbitMQ Classic Queue hanya membuang pesan kedaluwarsa ketika pesan tersebut mencapai kepala antrean (head), pesan dengan TTL 1 detik yang berada di belakang pesan dengan TTL 1 jam tidak akan pernah dihapus sebelum pesan 1 jam tersebut dikonsumsi atau kedaluwarsa. Hal ini menyebabkan penumpukan sampah data biner di RAM dan disk, serta memicu kebocoran memori terselubung.
- Solusi: Jika membutuhkan penanganan masa aktif pesan yang dinamis dan acak, gunakan antrean Quorum dengan kombinasi Dead Letter Exchange (DLX), atau buat antrean khusus per kategori TTL (misalnya antrean khusus TTL 10 detik, antrean khusus TTL 5 menit) untuk mempertahankan keseragaman waktu hapus.
2. Menyalahgunakan Priority Queue untuk Seluruh Alur Data #
Mengaktifkan argumen x-max-priority dengan nilai prioritas yang sangat tinggi (misalnya x-max-priority: 255) pada seluruh antrean di sistem broker.
Mengapa ini salah? #
Setiap tingkat prioritas memaksa proses antrean memelihara struktur indeks memori tambahan di dalam Erlang. Menggunakan tingkat prioritas di atas 10 akan mengonsumsi memori RAM dan CPU yang sangat besar untuk melakukan pengurutan ulang setiap kali ada pesan baru masuk. Hal ini menurunkan throughput hingga lebih dari 60%.
- Solusi: Batasi nilai prioritas maksimum antrean antara
1hingga10saja (misalnyax-max-priority: 5). Ini sudah sangat cukup untuk membedakan antara pesan VIP kritis dan pesan telemetri biasa tanpa mengorbankan stabilitas Erlang VM.
Ringkasan #
- Lapisan backing_queue Classic — Classic Queue v1 mengalirkan pesan melalui 5 lapisan internal (
q1keq4dandeltadisk) untuk menyeimbangkan konsumsi RAM dan performa tulis disk. Classic Queue v2 menyederhanakan ini dengan single log engine terpadu.- Ready vs Unacknowledged — Pesan
Readymenunggu dikirim ke konsumen. PesanUnackedadalah pesan yang telah dikirim ke konsumen tetapi belum mendapat konfirmasi balik ACK.- Integritas FIFO & Requeue — RabbitMQ menjamin urutan FIFO secara default, namun urutan ini dapat terganggu oleh penolakan pesan yang di-requeue, penggunaan prioritas, atau konsumsi paralel oleh banyak konsumen.
- Per-Queue vs Per-Message TTL — Per-Queue TTL diproses secara instan di kepala antrean, sedangkan Per-Message TTL hanya dievaluasi ketika pesan bersangkutan secara alami mencapai kepala antrean.
- Kebijakan Overflow — Argumen
x-overflowmenentukan tindakan broker saat antrean penuh: membuang pesan tertua (drop-head) atau menolak pesan baru yang masuk (reject-publish).- Credit Flow Backpressure — Backlog yang parah akan memicu mekanisme internal flow control yang menangguhkan pembacaan socket TCP produsen guna mengamankan broker dari kehabisan RAM.