Persistent #

Dalam perjalanan sebuah pesan (message lifecycle) di dalam broker RabbitMQ, setelah pesan berhasil melewati tahap perutean (Routing) dan masuk ke antrean tujuan, broker harus mengambil keputusan krusial mengenai kelangsungan hidup pesan tersebut: fase Persistence (Persistensi).

Fase Persistensi menentukan apakah pesan tersebut hanya hidup sementara di memori volatil RAM atau harus diabadikan ke dalam media penyimpanan non-volatil disk fisik agar mampu bertahan melewati pemadaman listrik, crash sistem, atau restart broker. Namun, persistensi pesan di RabbitMQ bukan sekadar menulis file biner ke hard drive. Di balik layar, terdapat optimasi batas ukuran pesan, penulisan Write-Ahead Log (WAL) terdistribusi, serta proses daur ulang segmen disk (segment compaction). Artikel ini akan membahas tuntas mekanisme internal penyimpanan pesan di RabbitMQ, perbedaan optimasi untuk pesan berukuran kecil dan besar, daur hidup penyimpanan biner Quorum Queue, serta implementasi kode Go untuk menjamin keamanan transaksi data kita.

Posisi Persistensi dalam Siklus Hidup Pesan #

Fase Persistensi terjadi tepat setelah proses perutean (rabbit_router:route/2) selesai menentukan antrean tujuan, namun sebelum pesan dinyatakan siap dikonsumsi (Ready state) atau dikirimkan ke aplikasi konsumen.

flowchart TD
    Msg["Pesan Masuk (DeliveryMode = 2)"] --> Route{"Rute Ditemukan"}
    Route --> SizeCheck{"Evaluasi Ukuran Pesan"}
    
    subgraph InlineStore["Penyimpanan Indeks (Inline)"]
        SizeCheck -->|< 4096 bytes| Inline["Tulis langsung ke Queue Index (.idx)"]
    end
    
    subgraph StoreMsgStore["Penyimpanan msg_store (Split)"]
        SizeCheck -->|> 4096 bytes| WriteIndex["Tulis referensi ke Queue Index (.idx)"]
        SizeCheck -->|> 4096 bytes| WriteStore["Tulis payload ke msg_store_persistent (.rdq)"]
    end

    Inline --> Commit{"ACK Publisher Confirm"}
    WriteIndex --> Commit
    WriteStore --> Commit

Ketika pesan persistent (delivery_mode = 2) masuk ke dalam Durable Queue:

  1. Evaluasi Ukuran: Broker memeriksa ukuran bodi pesan.
  2. Penyimpanan Inline (Pesan Kecil): Jika pesan berukuran kecil, broker melakukan optimasi dengan menulis pesan langsung ke indeks antrean.
  3. Penyimpanan Terpisah (Pesan Besar): Jika pesan berukuran besar, broker menulis pesan ke repositori global msg_store dan hanya menaruh alamat referensinya di file indeks antrean.
  4. Confirm Execution: Begitu data fisik berhasil ter-fsync ke disk, broker mengirimkan konfirmasi ke produsen.

Arsitektur Erlang Process dan Struktur Filesystem di Balik Layar #

Untuk memahami bagaimana persistensi bekerja di tingkat sistem operasi, kita harus melihat bagaimana RabbitMQ mengelola aktor-aktor Erlang dan representasi datanya di dalam sistem file (filesystem).

Di dalam arsitektur internal RabbitMQ, penyimpanan pesan diatur oleh dua proses Erlang utama yang berjalan sebagai singleton worker per virtual host (vhost):

  • msg_store_persistent: Bertanggung jawab atas penyimpanan seluruh pesan persisten yang masuk ke vhost tersebut.
  • msg_store_transient: Bertanggung jawab menyimpan pesan non-persisten (transient) yang terpaksa ditulis ke disk karena broker kehabisan memori RAM (proses paging out).

Struktur Folder di Disk Fisik #

Data persistensi disimpan di bawah direktori data Mnesia milik node RabbitMQ. Secara default, jalurnya mengikuti pola berikut:

/var/lib/rabbitmq/mnesia/rabbit@<hostname>/msg_stores/vhosts/<vhost_hash>/

Di dalam subfolder ini, kita akan menemukan struktur direktori penyimpanan:

  • msg_store_persistent/: Folder berisi berkas segmen dengan nama berurutan seperti 0.rdq, 1.rdq, 2.rdq, dan seterusnya. Setiap file .rdq (RabbitMQ Data Queue) memiliki batas ukuran default sebesar 16 megabyte (16,777,216 byte).
  • queues/: Setiap antrean Classic memiliki subfoldernya sendiri yang dinamai berdasarkan hash UUID antrean tersebut (misalnya queues/6C8D8.../). Di dalam folder antrean inilah file indeks .idx disimpan.

Mekanisme refc Binaries di Memori RAM #

Erlang VM (BEAM) menggunakan tipe data Reference-Counted Binary (refc binary) untuk data biner berukuran besar (biasanya di atas 64 byte). Payload pesan di RabbitMQ direpresentasikan sebagai refc binary yang dialokasikan di luar heap proses antrean (off-heap binary allocator).

Ketika produsen mempublikasikan satu pesan ke Exchange jenis Fanout yang merutekannya ke 10 antrean berbeda, payload pesan tersebut tetap disimpan di satu lokasi memori fisik tunggal. Setiap antrean hanya menerima pointer referensi kecil berukuran 24 byte bernama ProcBin yang mengarah ke bodi pesan tersebut.

Konsep refc ini juga berlaku ketika pesan ditulis ke disk melalui msg_store. Broker hanya menulis payload pesan satu kali ke file .rdq di msg_store_persistent, sementara masing-masing dari 10 antrean menulis entri referensi indeksnya sendiri ke file .idx mereka masing-masing. Ketika semua antrean telah menghapus (meng-ACK) pesan tersebut, reference count payload tersebut akan turun menjadi nol, dan baru pada saat itulah ruang di segmen .rdq ditandai sebagai sampah untuk dibersihkan saat Log Compaction.


Daur Hidup Transisi State Pesan: RAM vs Disk #

Sepanjang hidupnya di dalam broker, sebuah pesan akan bertransisi di antara beberapa state atau kondisi memori. RabbitMQ melacak status ini secara dinamis untuk mengoptimalkan penggunaan RAM dan performa I/O disk.

Ada tiga status utama penyimpanan pesan yang dikelola oleh proses antrean (rabbit_amqqueue_process):

State PesanPosisi Payload di RAMPosisi Payload di DiskSkenario Penggunaan
RAM OnlyYaTidakPesan transient pada antrean yang belum terbebani backlog memori.
RAM & DiskYaYaPesan persistent yang baru diterbitkan, atau pesan transient pada antrean yang mulai terisi.
Disk Only (Paged Out)TidakYaPesan persistent maupun transient yang dipindahkan ke disk karena RAM melampaui ambang batas watermark.

Mekanisme Memory Paging Out #

Ketika jumlah pesan di dalam antrean menumpuk (backlog) atau sistem secara keseluruhan mendekati batas alarm RAM (high memory watermark), RabbitMQ mengaktifkan mekanisme Memory Pager.

Tugas utama pager ini adalah menyelamatkan broker dari crash kehabisan memori (OOM - Out of Memory). Pager akan memindai antrean-antrean yang memiliki konsumsi memori terbesar, lalu memerintahkan proses antrean untuk melakukan page out:

  1. Bodi pesan yang berada di memori RAM dibebaskan (dihapus dari RAM).
  2. Jika pesan tersebut belum pernah ditulis ke disk sebelumnya (misalnya pesan transient), payload-nya akan ditulis ke folder msg_store_transient.
  3. Di dalam RAM, antrean hanya menyisakan data indeks referensi yang sangat kecil.
  4. Jika konsumen meminta pesan tersebut di kemudian hari, broker akan melakukan pembacaan disk asinkron untuk mengambil payload dari disk ke RAM (page in), lalu mengirimkannya ke konsumen.

Mekanisme ini memastikan stabilitas broker, namun dengan konsekuensi penurunan performa throughput yang signifikan karena kita beralih dari kecepatan akses memori nanodetik ke latensi I/O disk milidetik.


Optimasi Penyimpanan Inline vs Repositori msg_store #

RabbitMQ membagi mekanisme penulisan pesan ke disk menjadi dua strategi berdasarkan ukuran pesan untuk meminimalkan beban disk I/O (disk write amplification). Ambang batas bawaan untuk pembagian ini diatur oleh parameter queue_index_embed_msgs_below (secara default sebesar 4096 byte atau 4 KB).

1. Pesan Kecil (< 4 KB): Penyimpanan Inline #

Jika ukuran pesan (termasuk payload bodi, properti, dan header) lebih kecil dari 4 KB:

  • Logika: RabbitMQ akan menulis seluruh isi pesan tersebut langsung ke dalam file indeks antrean, yaitu berkas .idx (seperti journal.idx pada CQv2 atau file indeks segmen).
  • Keuntungan: Menghindari pemanggilan fungsi tulis disk ganda (double write). Broker tidak perlu menulis ke file indeks dan file payload secara terpisah, menghemat operasi IOPS hingga 50% untuk pesan-pesan berukuran kecil.

2. Pesan Besar (> 4 KB): Penyimpanan Terpisah (Split Store) #

Jika ukuran pesan melampaui batas 4 KB:

  • Logika: Broker memecah penulisan menjadi dua tempat. Payload bodi pesan biner ditulis ke dalam repositori global msg_store_persistent (berkas segmen biner .rdq berukuran maksimum 16MB). Sementara itu, informasi metadata (posisi offset biner dan panjang byte) ditulis ke file indeks .idx antrean.
  • Keuntungan: Memungkinkan efisiensi pembersihan RAM. Jika antrean mengalami backlog panjang, broker dapat dengan mudah menghapus payload 10 MB dari RAM dan hanya menahan indeks referensi kecil di RAM tanpa merusak urutan antrean.

Mekanisme Segmentasi & Log Compaction di msg_store #

Pesan-pesan besar ditulis secara berurutan (append-only) ke dalam berkas segmen .rdq berukuran 16MB di dalam subfolder msg_store_persistent. Karena ditulis secara append-only, RabbitMQ tidak pernah mengubah atau menghapus data di tengah file segmen aktif ketika consumer melakukan ACK pada pesan tertentu.

Bagaimana Sampah Disk Dibersihkan? #

Ketika consumer sukses memproses pesan dan mengirim ACK, pesan tersebut ditandai sebagai “terhapus” (garbage) di file indeks antrean, namun data biner fisiknya tetap tertinggal di dalam berkas segmen 16MB .rdq. Hal ini menyisakan ruang kosong biner (binary gaps).

Untuk mencegah disk penuh oleh data sampah, RabbitMQ menjalankan proses latar belakang bernama Log Compaction (Kompilasi Ulang Segmen):

  1. Evaluasi Rasio: Broker melacak rasio data valid vs data sampah pada setiap segmen disk.
  2. Kompilasi Ulang: Jika persentase data valid di dalam suatu segmen jatuh di bawah ambang batas (secara default < 50%), segmen tersebut dicalonkan untuk kompresi.
  3. Merge & Delete: Proses kompresi membaca dua atau lebih segmen yang terfragmentasi, menyalin pesan-pesan yang masih valid ke dalam berkas segmen baru yang padat, lalu menghapus berkas segmen lama dari disk.
flowchart LR
    S1["\"Segmen 1 (16MB): [ Valid"] [ Sampah ] [ Valid ]"] --> SB["\"Segmen Baru: [ Valid"] [ Valid ]"]
    S2["\"Segmen 2 (16MB): [ Sampah"] [ Valid ] [ Sampah ]"] --> SB

[!WARNING] Dampak Compaction di Produksi: Proses Log Compaction mengonsumsi resource CPU dan disk I/O (Read/Write) yang sangat intensif. Di bawah tekanan throughput tinggi, jika banyak terjadi kompresi segmen secara bersamaan, broker dapat mengalami lonjakan latensi (latency spikes) dan memperlambat waktu respons confirm produsen.


Prosedur fsync Internals dan Penulisan Batch #

Ketika kita mengirim pesan persisten dengan Publisher Confirms aktif, RabbitMQ tidak langsung memanggil perintah sistem fsync secara sinkron untuk setiap pesan tunggal. Melakukan hal tersebut akan membatasi throughput sistem kita hanya beberapa ratus pesan per detik (karena batas latensi fisik disk).

Mekanisme Batch Flush #

Untuk mencapai throughput puluhan ribu pesan per detik dengan tetap menjamin persistensi data, RabbitMQ mengimplementasikan penulisan batch asinkron di dalam aktor rabbit_msg_store:

  1. Buffer Penulisan: Pesan yang masuk ditaruh ke dalam antrean memori internal (flush queue).
  2. Kriteria Pemicu: Broker akan melakukan penulisan ke disk dan memicu fsync ketika salah satu kondisi berikut terpenuhi:
    • Jumlah pesan di buffer mencapai batas tertentu.
    • Waktu tunggu sejak pesan pertama masuk buffer terlampaui (mengacu pada parameter internal flush_after atau waktu tunggu mili-detik tertentu).
    • Tidak ada pesan baru lagi yang masuk ke broker dalam beberapa mikrodetik.
  3. Confirm ACK: Setelah operasi fsync sukses diselesaikan oleh sistem operasi dan disk kontroler, broker akan mengirimkan konfirmasi ACK ke produsen untuk seluruh batch pesan tersebut.

Mekanisme ini memungkinkan kompromi terbaik antara keamanan data ekstrem dan performa pemrosesan yang tinggi.


Persistensi WAL dan Snapshot pada Quorum Queue #

Mekanisme persistensi di atas berlaku untuk Classic Queue. Untuk Quorum Queue, karena didasarkan pada algoritma konsensus Raft, jalur persistensi datanya dikelola sepenuhnya oleh pustaka Erlang ra secara terpisah.

  • Raft WAL (Write-Ahead Log): Setiap node replika Quorum Queue menulis pesan yang masuk ke dalam file WAL Raft lokal di disk sebelum mengevaluasi status komitmen.
  • Raft Snapshots (Log Compaction): Berbeda dengan msg_store yang melakukan pemadatan segmen secara asinkron, pustaka ra menulis ringkasan status antrean saat itu (snapshot) ke disk setelah sejumlah entri log ter-commit (default 1024 entri). Setelah snapshot tertulis aman, seluruh log transaksi biner WAL sebelum indeks snapshot tersebut akan dihapus secara instan untuk menghemat ruang penyimpanan disk.

Skenario Bencana dan Pemulihan (Disaster Recovery) #

Apa yang terjadi ketika node RabbitMQ mati mendadak (misalnya karena kehilangan daya listrik)? RabbitMQ dirancang untuk memulihkan statusnya secara konsisten begitu ia dinyalakan kembali.

Proses Bootstrapping dan Rekonstruksi #

Saat RabbitMQ melakukan booting kembali setelah mati secara tidak bersih (dirty shutdown):

  1. Inisialisasi Database Mnesia: Broker memuat metadata klaster dan skema tabel.
  2. Pemindaian File Indeks .idx: Broker memindai seluruh file indeks antrean di direktori queues/. Broker memverifikasi checksum dan struktur file indeks untuk mendeteksi adanya korupsi data.
  3. Verifikasi Pointer ke .rdq: Untuk setiap pesan persisten di indeks, broker memastikan offset biner di file .rdq memang menunjuk ke bodi pesan yang valid.
  4. Rekonstruksi Antrean: Jika ditemukan ketidaksesuaian (misalnya pesan di indeks tidak ada di berkas .rdq karena operasi fsync sistem operasi yang tertunda), broker akan menghapus entri indeks yang korup tersebut guna mencegah kerusakan status antrean.
  5. Rebuild Mnesia jika Diperlukan: Jika database skema Mnesia rusak, broker mungkin memerlukan intervensi administrator untuk memulihkan data dari backup atau membangun ulang replikasi dari node tetangga.

Implementasi Kode Go: Mempublikasikan Pesan Persistent #

Untuk menjamin pesan ditulis secara permanen ke disk, produsen harus menyetel nilai properti DeliveryMode menjadi amqp.Persistent (angka 2) dan menggunakan Publisher Confirms untuk memastikan sinkronisasi disk fisik selesai.

package main

import (
	"context"
	"log"
	"time"

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

func main() {
	// 1. Membuka Koneksi TCP
	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
	// Broker hanya mengirim ACK setelah pesan persistent benar-benar tertulis di disk (fsync)
	err = ch.Confirm(false)
	if err != nil {
		log.Fatalf("Gagal mengaktifkan confirms: %s", err)
	}
	confirmChan := ch.NotifyPublish(make(chan amqp.Confirmation, 1))

	// 3. Deklarasi Antrean Durable
	queueName := "persistent-orders-queue"
	_, err = ch.QueueDeclare(
		queueName,
		true,  // durable: Wajib true agar antrean bertahan saat restart!
		false, // auto-delete
		false, // exclusive
		false, // no-wait
		nil,
	)
	if err != nil {
		log.Fatalf("Gagal mendeklarasikan antrean: %s", err)
	}

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

	payload := []byte(`{"order_id":"ORD-99001","amount":1500000}`)

	// 4. Menerbitkan Pesan dengan DeliveryMode Persistent (2)
	err = ch.PublishWithContext(ctx,
		"", // Default Exchange
		queueName,
		false, // mandatory
		false, // immediate
		amqp.Publishing{
			DeliveryMode: amqp.Persistent, // KUNCI UTAMA PERSISTENSI PESAN!
			ContentType:  "application/json",
			Body:         payload,
		},
	)
	if err != nil {
		log.Fatalf("Gagal menerbitkan pesan: %s", err)
	}

	// 5. Menunggu Konfirmasi Penulisan Disk Fisik (fsync) dari Broker
	confirm := <-confirmChan
	if confirm.Ack {
		log.Println("✓ Pesan sukses tertulis di disk fisik dan terkonfirmasi!")
	} else {
		log.Println("✗ Pesan gagal tertulis di disk (NACK)!")
	}
}

Anti-Pattern vs Solusi Keamanan Data #

Hindari beberapa kesalahan konfigurasi persistensi berikut di lingkungan produksi:

1. Mengaktifkan Persistensi Penuh pada Media Disk Lambat #

Mengonfigurasi seluruh antrean dan pesan (termasuk log telemetri dan metrik debug) dengan properti amqp.Persistent di atas server dengan disk HDD mekanis atau kapasitas IOPS cloud yang sangat rendah.

Mengapa ini salah? #

Setiap pesan persistent memaksa broker melakukan siklus pemanggilan sistem fsync untuk mengamankan data ke disk. Pada disk lambat, antrean penulisan disk akan menumpuk. Broker akan terpaksa memblokir koneksi produsen (Flow Control), menurunkan throughput keseluruhan sistem dari puluhan ribu menjadi hanya ratusan pesan per detik.

  • Solusi: Terapkan pemisahan beban kerja (separation of concerns). Gunakan antrean durable dan pesan persistent hanya untuk data transaksional kritis (pembayaran, order, ledger). Data non-kritis (logging, klik user, telemetri) harus dikirim sebagai pesan transient (DeliveryMode = 1) untuk menghemat kapasitas IOPS disk.

2. Mengabaikan Monitoring Fragmentasi Disk #

Menjalankan broker dengan kapasitas penyimpanan disk yang hampir penuh (misalnya di atas 80% kapasitas), dengan asumsi proses log compaction akan selalu membersihkan memori secara instan.

Mengapa ini salah? #

Proses log compaction membutuhkan ruang penyimpanan disk kosong tambahan sementara untuk menulis berkas segmen baru sebelum dapat menghapus berkas segmen lama yang terfragmentasi. Jika disk kita terlalu penuh, proses kompresi akan gagal berjalan karena kehabisan ruang disk, menyebabkan broker terhenti total akibat Disk Alarm terpicu.

  • Solusi: Selalu monitor kapasitas disk broker dan setel batas alarm disk (disk_free_limit) secara aman (minimal 1.5 kali dari total memori RAM server, atau batas statis seperti 5 GB). Pastikan ada ruang disk ekstra untuk memfasilitasi proses log compaction dengan lancar.

Ringkasan #

  • Inline vs Split Store — Pesan di bawah 4 KB (queue_index_embed_msgs_below) ditulis langsung secara inline ke berkas indeks .idx antrean untuk menghemat operasi IOPS. Pesan di atas 4 KB ditulis terpisah ke repositori msg_store_persistent.
  • Log Compaction biner — RabbitMQ membersihkan biner pesan yang sudah di-ACK dari disk secara asinkron dengan cara menyalin data valid dari beberapa segmen 16MB .rdq ke segmen baru dan menghapus segmen lama.
  • Raft WAL & Snapshots — Quorum Queue mengelola persistensi menggunakan mekanisme Write-Ahead Log (WAL) terdistribusi dan secara berkala membuat snapshot untuk kompresi log Raft.
  • Biaya fsync & Confirm — Konfirmasi ACK dikirim ke produsen hanya setelah broker berhasil memastikan data ter-fsync aman ke disk fisik atau klaster mayoritas Quorum tercapai.
  • Waspadai I/O Throttling — Penggunaan pesan persistent secara berlebihan pada disk lambat akan memicu Flow Control yang memblokir produsen untuk mengamankan kapasitas disk I/O.
  • Disk Free Space Alarm — Selalu monitor kapasitas sisa disk bebas untuk memfasilitasi proses log compaction asinkron yang membutuhkan ruang kosong sementara saat menulis segmen baru.

← Sebelumnya: Routing & Binding   Berikutnya: Queueing →

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