Quorum Queue #

Dalam ekosistem RabbitMQ modern, jika kita berbicara tentang merancang infrastruktur pesan berskala produksi yang andal, toleran terhadap kesalahan, dan memiliki ketersediaan tinggi (High Availability), hampir seluruh pembicaraan akan mengarah pada satu tipe antrean: Quorum Queue.

Sebelumnya, RabbitMQ mengandalkan fitur Mirrored Queue (antrean klasik yang direplikasi secara aktif). Namun, Mirrored Queue sering kali rentan terhadap masalah inkonsistensi data saat terjadi pemisahan jaringan (network partition atau split-brain) dan memiliki mekanisme sinkronisasi yang lambat serta membebani CPU. Quorum Queue hadir sebagai standar industri modern di RabbitMQ untuk menggantikan Mirrored Queue yang kini telah deprecated. Dengan memanfaatkan algoritma konsensus terdistribusi Raft, Quorum Queue memberikan jaminan konsistensi data yang ketat dan pemulihan bencana yang deterministik. Artikel ini akan membedah secara mendalam arsitektur internal Quorum Queue, pustaka Erlang ra yang mengendalikannya, cara kerja replikasi log, mitigasi skenario kegagalan kluster, serta implementasi praktis dengan Go.

Apa itu Quorum Queue & Konsensus Raft? #

Quorum Queue adalah jenis antrean terdistribusi yang dirancang untuk menjaga ketersediaan data dan konsistensi tinggi di dalam klaster multi-node RabbitMQ. Antrean ini dideklarasikan dengan menetapkan parameter "x-queue-type": "quorum".

Algoritma Konsensus Raft #

Quorum Queue dibangun di atas Raft Consensus Algorithm, sebuah protokol yang dirancang untuk mengelola replikasi log state machine pada sistem terdistribusi. Di dalam klaster:

  • Leader: Untuk setiap Quorum Queue yang dideklarasikan, satu node ditunjuk sebagai Leader (Pemimpin). Semua operasi publikasi (publish) dan konsumsi (consume) pesan wajib melewati node Leader ini.
  • Followers: Node-node lainnya dalam klaster yang ditunjuk sebagai replika bertindak sebagai Followers (Pengikut). Mereka tidak melayani klien secara langsung, melainkan secara pasif mereplikasi log yang dikirimkan oleh Leader.
  • Mayoritas (Quorum): Agar sebuah pesan dianggap berhasil diterima dan aman, pesan tersebut harus berhasil ditulis ke disk oleh mayoritas node anggota grup replika. Rumus mayoritas quorum adalah: $$Q = \lfloor N/2 \rfloor + 1$$ Di mana $N$ adalah jumlah total replika antrean. Pada klaster 3 node, mayoritas yang dibutuhkan adalah 2 node. Pada klaster 5 node, dibutuhkan 3 node.
flowchart TD
    Producer["Produsen (Publish)"] --> Leader["Raft Leader (Node A)"]
    
    subgraph Cluster["Klaster RabbitMQ 3 Node"]
        Leader -->|1. Append Log| LogA["WAL Local (Node A)"]
        Leader -->|2. Replicate Log| Follower1["Raft Follower (Node B)"]
        Leader -->|2. Replicate Log| Follower2["Raft Follower (Node C)"]
        
        Follower1 -->|3. Confirm ACK| LogB["WAL Local (Node B)"]
        Follower2 -. Belum Respon .-> LogC["WAL Local (Node C)"]
    end
    
    LogB -->|4. Quorum Tercapai: 2/3 Node Setuju| Commit{"Commit State Machine"}
    Commit -->|5. Confirm ACK| Producer

Arsitektur Internal Erlang (Pustaka ra) #

RabbitMQ tidak menulis algoritma Raft dari nol. Di tingkat runtime Erlang BEAM, RabbitMQ mengimplementasikan Raft menggunakan pustaka open-source terdedikasi bernama ra yang dikembangkan oleh tim inti RabbitMQ.

Pustaka ra dan Mesin State Machine #

Setiap kali antrean quorum dibuat, RabbitMQ men-spawn satu grup konsensus Raft melalui pustaka ra. Setiap replika antrean pada node yang berbeda berjalan sebagai proses Erlang independen yang saling berkomunikasi melalui protokol distribusi Erlang.

  • Write-Ahead Log (WAL): Ketika pesan dipublikasikan ke Leader, pesan tersebut tidak langsung dimasukkan ke struktur data antrean memori. Pustaka ra menulis pesan tersebut ke dalam berkas log biner khusus bernama Write-Ahead Log (WAL) di disk lokal node Leader secara berurutan (append-only).
  • Log Replication: Leader mengirimkan entri WAL baru ke seluruh node Follower. Follower menulis data tersebut ke file WAL lokal mereka masing-masing dan mengirimkan sinyal konfirmasi kembali ke Leader.
  • State Machine Commit: Setelah menerima konfirmasi dari mayoritas node Follower, Leader menyatakan entri tersebut telah committed. Leader kemudian memasukkan isi pesan ke dalam state machine lokal (yang merepresentasikan antrean memori aktif) dan memicu pengiriman sinyal Publisher Confirm ACK ke aplikasi produsen klien.

Penyimpanan Fisik Quorum di Disk #

Secara fisik, data log dan snapshot Quorum Queue disimpan di dalam folder data node di bawah sub-direktori:

/var/lib/rabbitmq/mnesia/rabbit@hostname/quorum/rabbit@hostname/

Di dalam direktori ini, pustaka ra mengelola berkas WAL dan berkas snapshot. Snapshot digunakan untuk proses kompresi log (log compaction). Jika berkas WAL sudah terlalu besar, pustaka ra akan merangkum status antrean saat itu ke dalam satu berkas snapshot dan menghapus log-log transaksi lama untuk menghemat kapasitas ruang disk.


Analisis Skenario Kegagalan Kluster #

Kekuatan utama Quorum Queue adalah kemampuannya menghadapi kegagalan node tanpa memicu kerusakan data. Mari kita analisis perilaku kluster 3 node (Node A = Leader, Node B = Follower, Node C = Follower) pada berbagai skenario crash:

Skenario 1: Salah Satu Node Follower Mati (Node C) #

Jika Node C tiba-masing kehilangan daya atau mati:

  1. Operasi Tetap Berjalan: Node A (Leader) dan Node B (Follower) masih aktif. Jumlah node aktif = 2 dari total 3 (memenuhi mayoritas quorum $\ge 2$).
  2. Konfirmasi Tetap Dikirim: Produsen masih dapat mengirimkan pesan, dan pesan tetap dikonfirmasi karena Node A dan Node B berhasil menulis ke disk.
  3. Pemulihan Otomatis: Saat Node C hidup kembali, ia akan meminta sinkronisasi log yang tertinggal dari Node A. Node A akan mengirimkan sisa log WAL secara asinkron hingga Node C seimbang (up-to-date).

Skenario 2: Node Leader Mati (Node A) #

Jika Node A mengalami kegagalan total:

  1. Leader Election: Deteksi kegagalan dipicu oleh hilangnya detak jantung (heartbeat) Raft. Node B dan Node C yang mendeteksi matinya Leader akan memulai fase pemilihan pemimpin (Leader Election).
  2. Pemilihan Demokratis: Node yang memiliki log WAL paling mutakhir (misal Node B) akan dipilih menjadi Leader baru.
  3. Transisi Cepat: Klien produsen dan konsumen secara otomatis dialihkan ke Node B oleh pustaka klien. Antrean kembali beroperasi normal dalam waktu beberapa detik.

Skenario 3: Pemisahan Jaringan (Split-Brain Immunity) #

Bayangkan terjadi gangguan jaringan yang membagi klaster 3 node menjadi dua bagian terisolasi: Sisi Kiri (Node A - Leader) dan Sisi Kanan (Node B dan Node C).

flowchart TD
    subgraph Left["Sisi Kiri (Partition A)"]
        NodeA["Node A (Leader saat ini)"]
        Fail["Quorum Gagal (1/3)<br>Publishes DITOLAK"]
        NodeA --> Fail
    end
    
    subgraph Right["Sisi Kanan (Partition B)"]
        NodeBC["Node B + Node C (Election Terjadi)"]
        Success["Node B terpilih jadi Leader<br/>Quorum Tercapai (2/3)<br/>Publishes DITERIMA"]
        NodeBC --> Success
    end
    
    Left -. "Jaringan Terputus" .-> Right
  • Sisi Kiri (Node A): Node A mendeteksi ia tidak lagi bisa berkomunikasi dengan Node B dan Node C. Ketika ada pesan baru masuk ke Node A, ia mencoba mereplikasi namun gagal mencapai mayoritas (hanya 1 dari 3 node yang setuju). Node A akan menolak publikasi pesan tersebut dan menahan diri dari mengirimkan ACK ke produsen.
  • Sisi Kanan (Node B & Node C): Kedua node mendeteksi hilangnya Leader A. Karena mereka berdua membentuk mayoritas (2 dari 3 node), mereka mengadakan pemilihan dan menunjuk Node B sebagai Leader baru. Sisi kanan tetap dapat menerima dan mengonfirmasi pesan baru dengan aman.
  • Rekonsiliasi (Healing): Saat jaringan pulih, Node A mendeteksi keberadaan Leader B yang sah. Node A akan turun pangkat menjadi Follower, membatalkan semua log lokal yang tidak sempat ter-commit ke mayoritas, dan menyinkronkan seluruh datanya mengikuti log dari Node B. Inkonsistensi data berhasil dicegah sepenuhnya.

Perbandingan Quorum vs Classic vs Mirrored (Legacy) #

Berikut adalah tabel komparasi teknis untuk memahami perbedaan Quorum Queue dengan tipe antrean RabbitMQ lainnya:

Karakteristik SistemClassic Queue (Non-Replicated)Classic Mirrored Queue (Legacy)Quorum Queue (Raft-backed)
Replikasi DataTidakYa (Aktif-Pasif Mirroring)Ya (Raft Consensus Group)
Protokol ReplikasiN/ACustom SynchronizationRaft (pustaka ra)
Konsistensi DataRendah (RAM/Single Disk)Lemah (Rentan Split-Brain)Sangat Kuat (Strict WAL Consensus)
Toleransi KegagalanTidak AdaSedangSangat Tinggi
Throughput WriteSangat TinggiSedang-TinggiSedang (Terbatas kecepatan fsync disk)
Status DeprecateAktifDeprecated (Jangan Gunakan)Standar Utama Produksi

FIFO & Manajemen Memori pada Quorum Queue #

Sebagai sistem berbasis konsensus terdistribusi, terdapat dua aspek operasional penting yang sering menjadi pertanyaan: bagaimana Quorum Queue menjamin urutan pesan (FIFO) dan bagaimana ia mengelola penggunaan memori RAM.

Jaminan Urutan FIFO dalam Konsensus Raft #

Pustaka ra di Erlang menjamin urutan pesan (First-In, First-Out) tetap konsisten meskipun pesan direplikasi ke banyak node Follower secara paralel.

  • Sequential Log Indexing: Setiap entri log yang diterima oleh Leader diberi nomor indeks log yang berurutan secara monoton (log index) dan penanda periode kepemimpinan (term).
  • Ordered Replication: Leader mengirimkan replikasi log ke Follower dalam urutan indeks yang persis sama. Follower menulis entri tersebut ke disk lokal mereka sesuai urutan indeks tersebut.
  • Committed Index Barrier: Pesan tidak akan pernah dikirimkan ke aplikasi consumer sebelum indeks log-nya melampaui committed_index (indeks yang telah disetujui oleh mayoritas node). Consumer hanya membaca data dari state machine yang sudah ter-commit, memastikan bahwa urutan FIFO tetap terjaga secara deterministik di seluruh klaster, bahkan setelah terjadi kegagalan Leader dan transisi pemilihan baru.

Optimasi Manajemen Memori (Soft & Hard Limits) #

Berbeda dengan Classic Queue yang dapat memindahkan payload pesan sepenuhnya dari RAM ke disk (mode lazy), Quorum Queue versi awal cenderung menahan pesan-pesan ter-commit di RAM sebagai cache untuk menjaga performa konsumsi cepat. Namun, jika terjadi backlog besar, ini dapat menyebabkan broker kehabisan memori.

Untuk mengatasi hal tersebut, RabbitMQ versi modern memperkenalkan konfigurasi batas memori Quorum:

  1. quorum_commands_soft_limit: Batas lunak jumlah entri log Raft yang boleh disimpan di memori RAM (default 256 entri). Jika backlog melampaui batas ini, broker mulai mengosongkan payload pesan dari RAM dan menyisakan indeks penunjuknya saja.
  2. quorum_commands_hard_limit: Batas keras (default 1024 entri). Jika penumpukan pesan melampaui batas ini, broker akan langsung memaksa paging log Raft ke disk secara agresif dan membatasi throughput ingress produsen untuk mengamankan stabilitas memori RAM.

Kode Implementasi Go: Mendeklarasikan Quorum Queue #

Untuk membuat Quorum Queue, kita harus mendeklarasikan antrean dengan argumen "x-queue-type" disetel ke "quorum". Antrean ini wajib dideklarasikan sebagai durable (durable = true).

Berikut adalah contoh kode implementasi lengkap di Go:

package main

import (
	"context"
	"log"
	"time"

	amqp "github.com/rabbitmq/amqp091-go"
)

func main() {
	// 1. Menghubungkan ke klaster RabbitMQ
	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
	// Sangat penting untuk quorum queue agar produsen tahu kapan konsensus Raft tercapai
	err = ch.Confirm(false)
	if err != nil {
		log.Fatalf("Gagal mengaktifkan Publisher Confirms: %s", err)
	}
	confirmChan := ch.NotifyPublish(make(chan amqp.Confirmation, 1))

	// 3. Mendefinisikan Argumen Quorum Queue
	queueArgs := amqp.Table{
		"x-queue-type": "quorum", // Menetapkan jenis antrean ke Quorum!
	}

	queueName := "critical-transactions-queue"

	// 4. Deklarasi Antrean (Wajib Durable = true)
	_, err = ch.QueueDeclare(
		queueName,
		true,      // durable: harus true untuk Quorum Queue!
		false,     // auto-delete
		false,     // exclusive
		false,     // no-wait
		queueArgs, // Mendaftarkan argumen tipe quorum
	)
	if err != nil {
		log.Fatalf("Gagal mendeklarasikan quorum queue: %s", err)
	}
	log.Printf("✓ Quorum Queue %s berhasil dideklarasikan.", queueName)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	payload := []byte(`{"transaction_id":"TX-9090","amount":2500000}`)

	// 5. Mengirimkan pesan persisten
	err = ch.PublishWithContext(ctx,
		"", // Default Exchange
		queueName,
		false,
		false,
		amqp.Publishing{
			DeliveryMode: amqp.Persistent, // Menulis pesan ke WAL disk fisik
			ContentType:  "application/json",
			Body:         payload,
		},
	)
	if err != nil {
		log.Fatalf("Gagal mengirim pesan ke quorum: %s", err)
	}

	// Menunggu konsensus mayoritas node tercapai dan ter-ACK
	confirm := <-confirmChan
	if confirm.Ack {
		log.Println("✓ Pesan berhasil direplikasi ke mayoritas node klaster dan terkonfirmasi!")
	} else {
		log.Println("✗ Pesan gagal mencapai konsensus mayoritas klaster (NACK)!")
	}
}

Anti-Pattern yang Harus Dihindari #

Desain terdistribusi quorum menuntut kepatuhan terhadap aturan sistem terdistribusi berikut untuk menghindari degradasi performa:

1. Menggunakan Jumlah Replika Genap (2 atau 4 Node) #

Mendeklarasikan Quorum Queue untuk disebarkan pada jumlah node klaster yang genap.

Mengapa ini salah? #

Sistem dengan jumlah replika genap tidak efisien dan membuang-buang resource disk/network tanpa menambah toleransi kegagalan. Mari kita hitung:

  • Jika kita menggunakan 2 node, quorum yang dibutuhkan adalah 2 node ($2/2 + 1 = 2$). Jika 1 node mati, sistem tidak bisa mencapai quorum, sehingga antrean langsung tidak tersedia. Toleransi kegagalan = 0 node.
  • Jika kita menggunakan 3 node, quorum yang dibutuhkan adalah 2 node ($3/2 + 1 = 2$). Jika 1 node mati, sistem tetap berjalan karena ada 2 node aktif. Toleransi kegagalan = 1 node.
  • Jika kita menggunakan 4 node, quorum yang dibutuhkan adalah 3 node ($4/2 + 1 = 3$). Jika 1 node mati, sisa 3 node (bisa jalan). Namun jika 2 node mati, sisa 2 node (tidak bisa mencapai quorum). Toleransi kegagalan = 1 node.

Dapat kita lihat bahwa klaster 4 node memiliki toleransi kegagalan yang persis sama dengan klaster 3 node, namun membuang resource disk dan network untuk replika keempat secara sia-sia.

  • Solusi: Selalu gunakan jumlah replika ganjil, yaitu 3 atau 5 node, untuk mengoptimalkan toleransi kegagalan terhadap biaya infrastruktur.

2. Menggunakan Quorum Queue untuk Antrean Sementara (Short-lived Queue) #

Mendeklarasikan antrean respons RPC atau antrean monitoring dinamis yang berumur pendek sebagai Quorum Queue.

Mengapa ini salah? #

Membuat dan menghapus grup konsensus Raft di tingkat pustaka ra membutuhkan biaya komputasi sinkronisasi klaster yang sangat besar di database Mnesia. Jika aplikasi kita melakukan siklus pembuatan dan penghapusan Quorum Queue secara dinamis berkali-kali per menit, performa CPU klaster akan lumpuh karena penguncian skema database (Mnesia schema locks).

  • Solusi: Hanya gunakan Quorum Queue untuk antrean data transaksional bisnis yang statis dan permanen. Antrean sementara berumur pendek harus selalu menggunakan Exclusive Queue atau Transient Queue.

Ringkasan #

  • Distributed Raft Consensus — Quorum Queue menjamin konsistensi tinggi dengan mereplikasi pesan ke beberapa node kluster menggunakan algoritma konsensus Raft yang dikelola Erlang library ra.
  • Syarat Mayoritas (Quorum) — Pesan hanya akan dianggap aman dan memicu konfirmasi ACK ke produsen jika berhasil ditulis ke disk fisik lokal (WAL) oleh mayoritas node anggota grup replika.
  • Imunitas Split-Brain — Desain konsensus Raft mencegah terjadinya dual-master (split-brain) saat terjadi pemisahan jaringan kluster dengan menolak penulisan pada sisi minoritas jaringan.
  • Wajib Durable — Quorum Queue secara arsitektural wajib dideklarasikan dengan parameter durable = true dan dipadukan dengan pesan persistent (DeliveryMode = 2).
  • Replika Jumlah Ganjil — Selalu konfigurasikan jumlah replika dalam jumlah ganjil (3 atau 5 node) demi efisiensi konsensus dan optimalisasi ketahanan kegagalan sistem.
  • Hindari Dynamic Churn — Jangan pernah menggunakan Quorum Queue untuk antrean dinamis berumur pendek (seperti RPC response) guna menghindari beban penguncian skema klaster Mnesia.

← Sebelumnya: Lazy vs Classic   Berikutnya: Publishing →

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