Message #

Dalam arsitektur penanganan pesan asinkron, Message (Pesan) adalah unit atomik pertukaran informasi terkecil yang mengalir lintas sistem. Semua pembicaraan mengenai keandalan produsen, fleksibilitas routing exchange, hingga konkurensi konsumen pada akhirnya bermuara pada bagaimana kita mendesain dan mengelola pesan tersebut. Di RabbitMQ, pesan bukanlah sekadar payload data mentah (seperti string JSON) yang dipindahkan dari satu server ke server lain. Secara teknis, pesan adalah objek biner terstruktur yang dibungkus oleh protokol AMQP (Advanced Message Queuing Protocol) dengan berbagai metadata, aturan daur hidup (lifecycle), dan implikasi fisik terhadap jaringan serta memori. Artikel ini akan membedah anatomi fisik pesan, efisiensi pembagian frame di jaringan, perbandingan format serialisasi, serta batas operasional pesan di lingkungan produksi.

Anatomi Frame AMQP: Pembagian Fisik Pesan di Jaringan #

RabbitMQ berkomunikasi menggunakan protokol AMQP 0-9-1 yang merupakan protokol tingkat jaringan (wire-level protocol). Artinya, ketika produsen mengirimkan sebuah pesan logis ke broker, pesan tersebut tidak dikirimkan secara utuh sebagai satu paket biner raksasa. Protokol AMQP secara fisik memecah pesan tersebut menjadi beberapa unit biner kecil yang disebut Frame.

flowchart TD
    subgraph Stream ["Aliran Socket TCP (Binary Stream)"]
        direction LR
        Frame1["Method Frame - Perintah basic.publish"] --> Frame2["Content Header Frame - Metadata dan Ukuran Payload"]
        Frame2 --> Frame3["Body Frame 1 - Fragmen Data Biner"]
        Frame3 --> Frame4["Body Frame N - Sisa Fragmen Data"]
    end

Pemisahan fisik ini bertujuan untuk mencegah alokasi buffer memori yang berlebihan pada soket dan memungkinkan broker membaca data secara streaming. Ada tiga tipe frame utama yang membentuk satu pesan logis:

1. Method Frame #

Frame pertama yang dikirim oleh produsen untuk memberi tahu broker tindakan apa yang sedang dilakukan.

  • Isi: Berisi kelas AMQP (misalnya Basic Class), metode AMQP (misalnya publish), ID channel logis, serta argumen perintah seperti nama exchange dan routing key.
  • Fungsi: Membuka jalur instruksi bagi broker untuk bersiap menerima data pesan berikutnya.

2. Content Header Frame #

Setelah method frame dikirim, produsen segera mengirimkan header konten.

  • Isi: Mencatat ukuran total payload pesan (payload size) dalam byte, serta tabel properti bawaan AMQP (seperti delivery_mode, content_type, dan tabel custom headers).
  • Fungsi: Memberi tahu broker seberapa besar memori buffer yang harus dialokasikan untuk membaca isi pesan.

3. Body Frames #

Frame terakhir yang berisi payload data aktual (isi pesan).

  • Isi: Fragmen biner dari isi pesan.
  • Mekanisme: Jika ukuran total payload pesan melebihi batas maksimal ukuran frame yang dinegosiasikan saat inisialisasi koneksi (Max Frame Size, default 128KB), AMQP akan memotong payload tersebut menjadi beberapa Body Frame berturut-turut yang dikirimkan secara berurutan di jaringan.

Pecahan frame ini dibaca secara berurutan oleh proses Erlang penanggung jawab koneksi soket TCP di node RabbitMQ, merekonstruksinya di tingkat memori lokal, dan menyalinnya ke antrean tujuan.


Manajemen Penjadwalan Frame (AMQP Class-Method IDs) #

Ketika broker menerima Method Frame dari produsen atau mengirimkannya ke konsumen, RabbitMQ harus dapat mengidentifikasi instruksi spesifik tersebut dengan sangat cepat. Untuk mencapai efisiensi ini, spesifikasi AMQP 0-9-1 memetakan setiap perintah ke dalam kombinasi angka biner terstruktur yang disebut Class ID (2 byte) dan Method ID (2 byte).

Di dalam arsitektur RabbitMQ, Class ID mendefinisikan kategori fungsionalitas utama, sedangkan Method ID mendefinisikan aksi spesifik dalam kategori tersebut. Berikut adalah beberapa Class dan Method ID penting yang sering kita jumpai dalam siklus hidup pesan:

ClassClass ID (Hex / Dec)MethodMethod ID (Dec)Aksi Operasional
Connection0x000A / 10start tune open10 30 40Negosiasi jabat tangan awal, alokasi memori buffer koneksi TCP, dan pembukaan koneksi logis.
Channel0x0014 / 20open close10 40Menginisialisasi atau menutup saluran virtual di atas koneksi TCP fisik yang sama.
Exchange0x0028 / 40declare delete10 20Membuat atau menghapus konfigurasi exchange pada metadata internal broker (Mnesia).
Queue0x0032 / 50declare bind10 20Membuat antrean baru atau menghubungkannya ke exchange tujuan menggunakan binding key.
Basic0x003C / 60publish consume deliver ack40 20 60 80Aksi pengiriman pesan dari produsen (publish), pendaftaran konsumen (consume), pengiriman pesan ke konsumen (deliver), dan konfirmasi penerimaan (ack).

Pembagian terstruktur ini memungkinkan modul jaringan RabbitMQ (yang ditulis dalam bahasa Erlang) melakukan pencocokan pola (pattern matching) langsung pada tingkat representasi biner socket stream. Sebagai contoh, ketika RabbitMQ menerima biner dengan prefix kelas 60 dan metode 40, proses Erlang penanggung jawab channel langsung mengetahui bahwa paket berikutnya yang akan datang adalah Content Header Frame dan Body Frame yang berisi pesan, lalu segera mempersiapkan state internalnya tanpa perlu melakukan parsing string yang mahal.


Batas Ukuran Frame dan Risiko Fragmentasi Payload Besar #

Saat produsen pertama kali melakukan jabat tangan (handshake) koneksi dengan server RabbitMQ, keduanya menegosiasikan konfigurasi batas maksimal ukuran satu frame biner melalui properti frame_max. Secara default, RabbitMQ menyetel batas ini ke 131.072 byte (128 KB).

Apa yang Terjadi jika Pesan Terlalu Besar? #

Jika produsen mempublikasikan pesan dengan ukuran 1 MB:

  1. Pesan tersebut akan dipecah menjadi minimal 8 Body Frame di jaringan TCP.
  2. Erlang VM pada broker harus menerima 8 paket biner ini secara berurutan, mengalokasikan RAM buffer dinamis untuk menyatukannya kembali, baru kemudian melakukan evaluasi routing key.
  3. Beban CPU dan Memori (Overhead): Proses fragmentasi dan rekonstruksi pesan berukuran megabyte memakan waktu CPU yang signifikan. Lebih buruk lagi, Erlang VM menggunakan model penyalinan pesan di memori (message copying). Jika pesan 10 MB diduplikasi ke 5 antrean berbeda melalui Fanout Exchange, RabbitMQ akan menduplikasi 10 MB tersebut sebanyak 5 kali di memori RAM Erlang Heap, memicu lonjakan penggunaan memori secara instan.
  4. Garbage Collection Pressure: Setelah pesan-pesan besar selesai dikonsumsi dan dihapus, Garbage Collector Erlang harus bekerja keras menyapu memori-memori lokal tersebut, menyebabkan lonjakan CPU yang tidak stabil.

Oleh karena itu, sangat penting untuk menjaga ukuran pesan RabbitMQ sekecil mungkin. Idealnya, ukuran pesan rata-rata di produksi harus berada di bawah 100 KB, dan sangat disarankan tidak pernah melebihi 10 MB.


Properties vs Headers: Metadata Bawaan vs Kustom #

AMQP memisahkan metadata pesan menjadi dua kategori utama pada Content Header Frame: Properties (properti terstruktur dengan skema kaku) dan Headers (tabel pasangan kunci-nilai fleksibel).

1. Properties (Metadata Skema Kaku) #

Properties adalah field biner berukuran tetap yang didefinisikan secara resmi oleh spesifikasi protokol AMQP. Karena memiliki posisi byte yang pasti pada header frame, proses pembacaan (parsing) properti ini oleh broker berjalan sangat cepat dengan konsumsi CPU minimal.

Berikut adalah properti standar AMQP yang paling penting untuk operasional:

  • delivery_mode: Bernilai 1 untuk pesan transient (RAM saja), bernilai 2 untuk pesan persistent (ditulis ke disk).
  • content_type: Merekam jenis serialisasi payload, membantu aplikasi menghindari kesalahan pembacaan (contoh: application/json).
  • message_id: ID unik pesan. Wajib disertakan produsen untuk audit log dan validasi idempotensi di tingkat konsumen.
  • correlation_id: ID unik transaksi, sangat membantu distributed tracing untuk menghubungkan log dari produsen ke konsumen.
  • reply_to: Nama antrean tujuan untuk mengirimkan pesan balasan (digunakan pada pola Request-Response/RPC).
  • expiration: Batas waktu hidup pesan (TTL) dalam milidetik yang dihitung per pesan individu.

2. Headers (Metadata Kustom) #

Headers adalah tabel kamus dinamis (dictionary table) di mana pengembang bebas menyisipkan metadata buatan sendiri (misalnya versi event event_version: "v1.2", atau identitas pengirim source_app: "billing-service").

  • Perilaku: Berbeda dengan properti biasa, Headers harus dibaca secara dinamis dari byte stream. Hal ini membutuhkan sedikit lebih banyak waktu CPU.
  • Headers Exchange: Kita dapat memanfaatkan tabel Headers ini sebagai kriteria routing pesan menggunakan tipe exchange Headers, di mana broker akan mencocokkan kunci-nilai pada headers pesan dengan binding key antrean.

Serialisasi Data dan Manajemen Kontrak Skema #

Karena RabbitMQ memperlakukan isi payload pesan murni sebagai opaque binary stream (aliran biner tanpa struktur yang dikenal), aplikasi kita bertanggung jawab sepenuhnya untuk mendefinisikan bagaimana objek data diubah menjadi byte biner (serialization) saat dikirim dan diubah kembali menjadi objek (deserialization) saat diterima.

Ada tiga opsi format serialisasi utama yang umum digunakan di produksi:

1. JSON (JavaScript Object Notation) #

  • Keunggulan: Sangat mudah dibaca oleh manusia (human-readable), didukung secara bawaan oleh hampir semua bahasa pemrograman, dan fleksibel.
  • Kelemahan: Ukuran data biner hasil serialisasi relatif besar karena menyertakan nama field berulang kali dalam format teks. Proses komparasi karakter saat parsing JSON memakan waktu CPU yang cukup tinggi.

2. Protocol Buffers (Protobuf) #

  • Keunggulan: Format serialisasi biner yang sangat ringkas dan efisien. Kecepatan parsing Protobuf bisa mencapai 5-10 kali lebih cepat daripada JSON, dan ukuran payload biner yang dihasilkan jauh lebih kecil (bisa menghemat bandwidth hingga 70%).
  • Kelemahan: Data berbentuk biner murni sehingga tidak bisa dibaca langsung oleh manusia saat debugging tanpa alat bantu. Memerlukan kompilasi file skema .proto.

3. Apache Avro #

  • Keunggulan: Menyimpan data dalam format biner yang sangat kecil dengan menyertakan skema referensi. Sangat cocok untuk sistem event-driven skala enterprise.
  • Schema Registry: Penggunaan Avro atau Protobuf idealnya dikombinasikan dengan Schema Registry terpusat. Hal ini menjamin bahwa produsen dan konsumen selalu berbagi kontrak skema data yang kompatibel (backward & forward compatibility).

Dampak Serialisasi terhadap Garbage Collection (GC) Erlang #

Pilihan format serialisasi di sisi produsen dan konsumen memiliki dampak langsung yang sangat masif terhadap performa internal broker RabbitMQ. Hal ini berkaitan erat dengan cara kerja Virtual Machine Erlang (BEAM) dalam mengelola memori heap dan proses Garbage Collection (GC).

Di dalam BEAM VM, setiap proses Erlang (seperti proses penanggung jawab antrean atau channel) memiliki memori heap sendiri yang terisolasi. Ketika GC berjalan pada suatu proses, ia hanya membersihkan heap lokal proses tersebut tanpa menghentikan jalannya proses Erlang lainnya (no global stop-the-world). Namun, perilaku alokasi memori ini sangat dipengaruhi oleh karakteristik tipe data payload pesan yang kita lewatkan:

1. Refc Binaries (Binari Off-Heap) vs Heap Binaries #

Erlang membagi penyimpanan data biner menjadi dua mekanisme berdasarkan ukurannya:

  • Heap Binaries (≤ 64 byte): Data biner berukuran kecil disimpan langsung di dalam heap lokal proses Erlang. Ketika pesan dipindahkan dari satu saluran (channel) ke antrean (queue), data biner ini akan disalin (deep copy) secara fisik antar heap proses.
  • Refc Binaries (> 64 byte): Data biner berukuran sedang hingga besar disimpan di dalam area memori bersama di luar heap (global off-heap binary allocator). Proses Erlang lokal hanya menyimpan objek referensi kecil berukuran 24 byte yang disebut ProcBin pada heap lokalnya. Ketika pesan dirutekan ke beberapa antrean berbeda, RabbitMQ hanya menyalin ProcBin (referensi 24 byte) ke antrean-antrean tujuan, bukan payload fisiknya. Langkah ini mencegah duplikasi data di RAM dan menghemat beban I/O memori secara signifikan.

2. Beban GC Erlang pada Parsing JSON vs Protobuf/Avro #

Ketika kita mengirimkan payload dalam format JSON berukuran besar:

  1. GC Pressure pada Konsumen & Produsen: Di tingkat aplikasi klien, parsing string JSON yang memiliki banyak kunci teks memaksa bahasa pemrograman (seperti Java, Python, atau Node.js) melakukan ribuan alokasi objek kecil di heap, yang memicu GC intensif di sisi aplikasi kita.
  2. Beban di Sisi Broker: Meskipun RabbitMQ memperlakukan isi pesan sebagai aliran biner mentah (opaque binary stream) tanpa membaca isinya, jika kita menggunakan Headers Exchange dengan kriteria pencocokan yang rumit pada properti header JSON (atau jika broker dipaksa mem-parsing payload untuk kebutuhan plugin tertentu), RabbitMQ harus mengubah biner teks tersebut menjadi representasi term Erlang. Parsing teks JSON memaksa broker membuat struktur data dinamis di heap Erlang, memicu siklus GC Erlang yang lebih sering dan meningkatkan latensi pemrosesan pesan (latency spikes).

Sebaliknya, dengan menggunakan serialisasi biner seperti Protocol Buffers or Apache Avro:

  1. Format Ringkas: Ukuran biner yang dikirimkan sangat kecil, sehingga hampir selalu masuk ke kategori Refc Binaries. Broker hanya memproses pointer referensi yang ringan.
  2. Optimasi Saluran Jaringan: Ukuran payload yang lebih kecil meminimalkan jumlah Body Frame AMQP yang harus dikirim melalui koneksi TCP. Hal ini mereduksi frekuensi alokasi buffer socket soket di broker, meminimalkan interrupt sistem operasi, dan menjaga penggunaan memori broker tetap flat dan stabil bahkan di bawah beban trafik yang sangat tinggi.

Dengan demikian, keputusan arsitektur untuk meninggalkan JSON dan beralih ke Protobuf/Avro pada sistem berkecepatan tinggi bukan hanya tentang menghemat bandwidth jaringan, melainkan taktik vital untuk menjaga stabilitas memori RAM dan latensi broker RabbitMQ dari fluktuasi akibat siklus Garbage Collection Erlang.


Time-To-Live (TTL) dan Daur Hidup Kedaluwarsa Pesan #

RabbitMQ memungkinkan kita untuk menyetel batas umur pesan di dalam sistem melalui fitur Time-To-Live (TTL). Jika sebuah pesan berada di dalam antrean melebihi batas waktu TTL yang ditentukan tanpa ada konsumen yang mengonsumsinya, pesan tersebut akan dianggap kedaluwarsa (expired).

Ada dua cara untuk mengonfigurasi TTL pada RabbitMQ:

A. Queue TTL (Batas Umur Tingkat Antrean) #

  • Mekanisme: Kita menyetel argumen x-message-ttl saat mendeklarasikan antrean.
  • Perilaku: Semua pesan yang masuk ke antrean tersebut akan secara otomatis mewarisi batas TTL yang sama (misal 60 detik). RabbitMQ dapat mendeteksi kedaluwarsa secara linier dari kepala antrean (queue head) secara efisien karena pesan yang masuk lebih dulu pasti akan kedaluwarsa lebih dulu.

B. Message TTL (Batas Umur Per Pesan) #

  • Mekanisme: Produsen menyematkan properti expiration (dalam milidetik) pada header masing-masing pesan secara individual saat melakukan publikasi.
  • Perilaku: Setiap pesan memiliki umur yang berbeda-beda. Hal ini memaksa RabbitMQ untuk memeriksa status kedaluwarsa secara berkala. Pesan kedaluwarsa di tengah antrean tidak langsung dihapus secara fisik; mereka baru akan dibuang saat mencapai bagian kepala antrean (head of the queue) ketika siap dikirim ke konsumen.

Pesan yang kedaluwarsa akan langsung dihapus dari sistem, atau dialihkan secara otomatis ke Dead Letter Exchange (DLX) jika antrean tersebut telah dikonfigurasi dengan parameter x-dead-letter-exchange.


Anti-Pattern vs Solusi: Mengirimkan Payload File Biner Raksasa secara Langsung #

Salah satu kesalahan paling fatal dalam merancang sistem pengiriman pesan adalah mencoba menggunakan RabbitMQ sebagai media transfer file biner besar.

Kasus Anti-Pattern: Mengirim File Gambar / PDF dalam Payload Pesan #

Dalam contoh ini, produsen membaca file laporan PDF berukuran 15 MB dari disk, mengubah seluruh file biner tersebut menjadi array byte, menyisipkannya langsung ke dalam payload pesan RabbitMQ, lalu mempublikasikannya.

// ANTI-PATTERN: Mengirim file biner besar langsung di payload RabbitMQ
func PublishLargeFileBad(ch *amqp.Channel, pdfPath string) {
    // Membaca file 15MB ke memori
    pdfBytes, _ := ioutil.ReadFile(pdfPath)
    
    // ✗ JANGAN: Mengirim data biner raksasa langsung ke RabbitMQ
    _ = ch.Publish("document-exchange", "doc.upload", false, false, amqp.Publishing{
        DeliveryMode: amqp.Persistent,
        ContentType:  "application/pdf",
        Body:         pdfBytes, // 15MB payload!
    })
    
    // Masalah: Mengakibatkan disk I/O mematikan di server RabbitMQ, memicu OOM alarm, 
    // dan menghancurkan throughput pengiriman pesan lainnya yang berukuran kecil.
}

Solusi Praktis: Menggunakan Claim Check Pattern #

Pendekatan terbaik untuk menangani data biner besar adalah menerapkan Claim Check Pattern (Pola Tiket Klaim). Produsen menyimpan file biner tersebut ke penyimpanan objek eksternal (seperti Amazon S3, MinIO, atau Google Cloud Storage), lalu hanya mengirimkan pesan kecil berisi metadata referensi dan URL akses file tersebut ke RabbitMQ.

// BENAR: Menggunakan Claim Check Pattern untuk menangani file besar
type DocumentUploadedEvent struct {
    DocumentID string `json:"document_id"`
    StorageURL string `json:"storage_url"` // ✓ Hanya mengirimkan URL referensi
    FileSize   int64  `json:"file_size"`
}

func PublishLargeFileGood(ch *amqp.Channel, s3Client *S3Client, pdfPath string) {
    // 1. Unggah file biner 15MB ke S3 secara langsung
    storageURL, docID, _ := s3Client.UploadFile(pdfPath)
    
    // 2. Buat payload event kecil yang hanya berisi metadata referensi
    event := DocumentUploadedEvent{
        DocumentID: docID,
        StorageURL: storageURL,
        FileSize:   15 * 1024 * 1024, // 15MB info
    }
    payload, _ := json.Marshal(event)
    
    // 3. ✓ SOLUSI: Publikasikan pesan metadata ringan ke RabbitMQ (Ukuran payload < 1KB)
    _ = ch.Publish("document-exchange", "doc.upload", false, false, amqp.Publishing{
        DeliveryMode: amqp.Persistent,
        ContentType:  "application/json",
        Body:         payload,
    })
    
    // Konsumen yang menerima pesan ini akan mengunduh file biner secara mandiri dari S3
    // menggunakan URL yang disediakan, membebaskan server RabbitMQ dari beban I/O biner.
}

Ringkasan #

  • AMQP Frame Segmentation — Alur biner TCP yang membagi satu pesan logis menjadi Method, Header, dan Body Frames guna menyederhanakan alokasi buffer RAM soket.
  • Max Frame Size (128KB) — Batasan negosiasi frame yang mengakibatkan fragmentasi biner jika ukuran payload melampaui batas, meningkatkan beban komputasi Erlang GC.
  • Claim Check Pattern — Pola arsitektur wajib untuk menangani file biner besar dengan mengunggahnya ke S3/MinIO dan hanya mengirimkan pesan URL referensi ke RabbitMQ.
  • Queue TTL vs Message TTL — Batas kedaluwarsa tingkat antrean yang dievaluasi secara linier di kepala antrean jauh lebih efisien daripada TTL tingkat pesan individual.

← Sebelumnya: Producer   Berikutnya: Exchange →

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