Message TTL #
Dalam arsitektur pemrosesan data asinkron (asynchronous processing), tidak semua informasi yang dialirkan oleh produsen memiliki masa relevansi yang tak terbatas. Banyak data bisnis yang secara alami hanya berharga dalam jangka waktu tertentu. Sebagai contoh, sebuah kode verifikasi OTP (One-Time Password) hanya boleh digunakan dalam batas waktu 2 atau 3 menit setelah dikirimkan, atau data harga saham real-time yang diperbarui setiap beberapa detik akan menjadi usang dan tidak berguna jika tertunda pemrosesannya akibat antrean yang sedang menumpuk. Membiarkan pesan-pesan usang ini tetap mengalir di dalam antrean utama hingga diproses oleh konsumen tidak hanya membuang-buang daya komputasi aplikasi, tetapi juga dapat merusak konsistensi logika bisnis sistem kita.
Untuk mengelola relevansi temporal dari pesan-pesan tersebut, RabbitMQ menyediakan fitur native bernama Message TTL (Time-To-Live). TTL memungkinkan kita menetapkan masa berlaku pesan di dalam sistem antrean secara presisi dalam satuan milidetik. Begitu waktu hidup pesan habis sebelum sempat diproses oleh konsumen, broker secara otomatis akan menyingkirkan pesan tersebut dari antrean utama. Fitur ini sangat krusial baik untuk pembersihan data usang secara otomatis maupun untuk merancang arsitektur penundaan retry (delay retry) tanpa memerlukan sistem penjadwal eksternal.
Dua Level Konfigurasi TTL #
RabbitMQ menyediakan fleksibilitas tinggi dengan mengizinkan kita mengonfigurasi batas waktu kedaluwarsa (TTL) pada dua tingkat operasional yang berbeda: di tingkat deklarasi antrean (Queue-level) dan di tingkat pengiriman pesan individu (Message-level).
1. Queue-Level TTL (x-message-ttl)
#
Queue-level TTL diterapkan pada seluruh pesan yang masuk ke dalam antrean tertentu secara seragam. Konfigurasi ini didefinisikan saat kita pertama kali mendeklarasikan antrean menggunakan argumen x-message-ttl dengan nilai bertipe integer positif dalam satuan milidetik.
// Menyetel Queue-level TTL selama 60 detik (60000 ms)
args := amqp.Table{
"x-message-ttl": int32(60000),
}
Ketika konfigurasi ini aktif, broker RabbitMQ akan bertindak sebagai timer otomatis untuk antrean tersebut. Setiap pesan yang mendarat di dalam antrean akan memiliki masa hidup yang sama persis (dalam contoh di atas adalah 60 detik). Begitu pesan berada di antrean melampaui batas waktu tersebut, broker akan langsung menyatakannya expired. Pola ini sangat ideal untuk antrean yang menangani pesan dengan tipe data homogen yang seragam, seperti antrean notifikasi push seluler atau antrean pembersihan log sementara.
2. Message-Level TTL (expiration)
#
Jika kita membutuhkan kontrol yang lebih dinamis, di mana setiap pesan memiliki masa kedaluwarsa yang berbeda-beda tergantung konteks transaksinya, kita dapat menggunakan Message-level TTL. Konfigurasi ini tidak disetel pada antrean, melainkan disisipkan oleh produsen langsung ke dalam properti metadata pesan saat mempublikasikan pesan. Properti ini bernama expiration dan ditulis sebagai string angka dalam satuan milidetik.
// Menyetel Message-level TTL dinamis selama 15 detik (15000 ms)
err := ch.PublishWithContext(ctx,
"main.exchange",
"payment.verify",
false,
false,
amqp.Publishing{
ContentType: "application/json",
Expiration: "15000", // String milidetik
Body: payload,
},
)
Dengan menggunakan metode ini, antrean yang sama dapat menampung pesan dengan waktu kedaluwarsa yang bervariasi—misalnya, pesan pertama kedaluwarsa dalam 10 detik, sedangkan pesan kedua di belakangnya baru kedaluwarsa setelah 5 menit.
Aturan Resolusi Konflik (Conflict Resolution) #
Bagaimana jika kita mendeklarasikan antrean dengan x-message-ttl sebesar 30 detik, namun produsen mengirimkan pesan individu ke antrean tersebut dengan properti expiration sebesar 10 detik atau 50 detik?
RabbitMQ menangani konflik ini dengan aturan yang sangat adil: Nilai TTL yang paling kecil yang akan menang.
- Jika properti pesan individu menetapkan masa hidup 10 detik (lebih kecil dari 30 detik limit antrean), maka pesan tersebut akan kedaluwarsa dalam waktu 10 detik.
- Jika properti pesan individu menetapkan masa hidup 50 detik (lebih besar dari 30 detik limit antrean), broker akan menolak nilai tersebut dan membatasi masa hidup pesan tetap maksimal 30 detik sesuai batas antrean.
Aturan ini memastikan bahwa antrean utama kita tetap memiliki batas atas kedaluwarsa yang keras demi menjaga konsumsi memori broker, tanpa mematikan fleksibilitas produsen untuk menetapkan batas kedaluwarsa yang lebih ketat.
Cara Evaluasi TTL di Broker (Bahaya Head-of-Line Blocking) #
Meskipun RabbitMQ mendukung Queue-level TTL dan Message-level TTL secara bersamaan, terdapat perbedaan fundamental dalam cara mesin broker mengevaluasi dan membersihkan pesan kedaluwarsa untuk kedua metode tersebut. Memahami perbedaan cara kerja internal ini sangat krusial untuk mencegah terjadinya kegagalan memori pada broker kita.
Evaluasi Aktif pada Queue-Level TTL #
Ketika kita menetapkan TTL pada tingkat antrean (x-message-ttl), broker RabbitMQ secara internal menggunakan struktur data berurutan yang efisien dan memantau waktu kedaluwarsa pesan secara aktif. Karena semua pesan memiliki durasi kedaluwarsa yang seragam, pesan yang masuk paling awal pasti akan kedaluwarsa paling awal pula.
Broker dapat dengan mudah melacak pointer kepala antrean dan langsung menghapus atau mengalihkan pesan-pesan yang habis waktunya secara instan, tanpa harus menelusuri isi antrean. Proses pembersihan memori berjalan sangat cepat dan efisien.
Evaluasi Pasif pada Message-Level TTL (Head-of-Line Blocking) #
Perilaku ini sangat berbeda ketika kita menggunakan Message-level TTL (expiration). Karena masa hidup setiap pesan berbeda-beda, broker RabbitMQ tidak dapat dengan mudah memprediksi pesan mana yang akan kedaluwarsa terlebih dahulu. Menelusuri seluruh antrean yang berisi jutaan pesan setiap milidetik untuk mencari pesan expired secara konstan akan memakan resource CPU broker yang teramat besar.
Untuk menghindari penurunan performa tersebut, RabbitMQ memilih jalan pintas: Broker hanya mengevaluasi kedaluwarsa pesan individu ketika pesan tersebut telah mencapai bagian paling depan antrean (head of the queue) dan siap dikirimkan ke konsumen.
Pendekatan pasif ini melahirkan masalah klasik sistem terdistribusi bernama Head-of-Line (HoL) Blocking pada level pesan. Mari pelajari skenario berikut:
flowchart LR
Tail["Tail (Belakang)"] --> MessageC["Pesan C (TTL: 5 Detik)"]
MessageC --> MessageB["Pesan B (TTL: 10 Detik)"]
MessageB --> MessageA["Pesan A (TTL: 10 Menit)"]
MessageA --> Head["Head (Depan)"]- Produsen mengirimkan
Pesan Adengan TTL 10 Menit, diikuti olehPesan Bdengan TTL 10 Detik, danPesan Cdengan TTL 5 Detik. - Semua pesan masuk ke antrean utama yang saat itu sedang tidak memiliki konsumen aktif.
- Setelah 5 detik berlalu, secara teoritis
Pesan Ctelah kedaluwarsa. Setelah 10 detik,Pesan Bjuga kedaluwarsa. - Namun, karena
Pesan A(yang memiliki TTL 10 menit) berada di bagian paling depan antrean (head), broker RabbitMQ tidak akan memeriksa status kedaluwarsaPesan BdanPesan C. - Akibatnya,
Pesan BdanPesan Cyang sudah kedaluwarsa tetap tersimpan di dalam memori dan disk broker, menyita kapasitas penyimpanan, hingga 10 menit berlalu atau sampaiPesan Adikonsumsi oleh aplikasi.
Bahaya laten dari HoL Blocking ini adalah terjadinya penumpukan pesan usang di memori broker secara tidak terduga, yang dapat memicu kehabisan ruang penyimpanan disk (disk space limit alarm) jika produsen secara acak mengirimkan pesan dengan TTL sangat panjang di depan jutaan pesan dengan TTL pendek.
Pola Delay Queue (Retry Delay) Menggunakan TTL dan DLX #
Salah satu pola desain arsitektur paling populer dan elegan di RabbitMQ adalah memanfaatkan efek kedaluwarsa TTL yang dikombinasikan dengan Dead Letter Exchange (DLX) untuk membangun mekanisme penundaan retry (Delay Queue / Retry Delay). Pola ini memungkinkan kita menunda pemrosesan ulang pesan gagal tanpa perlu menahan thread konsumen dengan time.Sleep dan tanpa memerlukan database scheduler eksternal.
Berikut adalah diagram alur kerja pola Delay Queue:
flowchart TD
MQ[Antrean Utama: main.queue] -->|1. Proses Gagal| C{Konsumen}
C -->|2. nack dengan requeue=false| DLX_R(Exchange Retry: retry.exchange)
DLX_R -->|3. Kirim ke| DQ[Antrean Delay: retry.delay.queue]
subgraph Antrean Tanpa Konsumen
DQ
end
DQ -->|4. TTL Expired| DLX_M(Exchange Utama: main.exchange)
DLX_M -->|5. Masuk Kembali| MQ- Langkah 1: Konsumen membaca pesan dari Antrean Utama (
main.queue). - Langkah 2: Terjadi kegagalan pemrosesan sementara (misal database timeout). Konsumen menolak pesan tersebut dengan mengirimkan
basic.nack(requeue=false). - Langkah 3: Karena
main.queuedikonfigurasi dengan DLX yang mengarah keretry.exchange, broker RabbitMQ secara otomatis mengalihkan pesan gagal tersebut ke Exchange Retry. Dari exchange tersebut, pesan masuk ke Antrean Delay (retry.delay.queue). - Langkah 4:
retry.delay.queuedirancang khusus tanpa memiliki konsumen aktif yang mendengarkannya. Antrean ini dikonfigurasi dengan dua parameter vital:x-message-ttldisetel ke nilai waktu tunggu yang kita inginkan (misal10000ms atau 10 detik).x-dead-letter-exchangedisetel untuk mengarah kembali ke Exchange Utama (main.exchange).
- Langkah 5: Pesan akan tertahan di dalam
retry.delay.queueselama 10 detik. Begitu waktu 10 detik habis, pesan dianggap expired oleh broker. - Langkah 6: Karena antrean delay memiliki konfigurasi DLX yang mengarah ke Exchange Utama, broker RabbitMQ secara otomatis akan mengirimkan kembali pesan kedaluwarsa tersebut ke Exchange Utama, yang kemudian merutekannya kembali ke Antrean Utama (
main.queue) untuk diproses ulang oleh konsumen.
Pola ini berjalan sepenuhnya secara asinkron dan terdistribusi di sisi broker RabbitMQ. Konsumen kita bebas memproses pesan-pesan sehat lainnya di antrean utama tanpa terganggu oleh proses tunggu pesan yang gagal.
Kasus Penggunaan Nyata Message TTL #
Mengatur batas waktu hidup pesan sangat penting untuk beberapa skenario industri berikut:
- One-Time Password (OTP) & Kode Verifikasi: OTP via SMS atau Email memiliki masa kedaluwarsa yang sangat ketat (biasanya 2-5 menit). Jika gateway SMS kita sempat down selama 1 jam, menimbun jutaan OTP dan mengirimkannya setelah gateway pulih adalah kesia-siaan karena kode tersebut sudah pasti tidak berlaku lagi bagi pengguna.
- Sensor Telemetri IoT frekuensi tinggi: Sensor suhu mesin mengirimkan data setiap 2 detik. Jika terjadi hambatan jaringan, data suhu yang dikirim 1 menit lalu sudah tidak relevan karena kita membutuhkan pembacaan suhu paling aktual. Kita dapat menetapkan TTL pesan sebesar 6 detik agar data usang otomatis terbuang.
- Real-time Pricing & Market Feed: Layanan penyedia harga saham atau nilai tukar mata uang asing memperbarui data secara konstan. Pesan harga yang tertahan di antrean selama lebih dari 10 detik harus dianggap kedaluwarsa agar aplikasi pengguna tidak menampilkan harga usang yang menyesatkan.
- Idempotent Cache Invalidation: Pesan untuk membersihkan cache memori lokal aplikasi setelah update database. Jika event update berjalan sangat cepat, kita hanya perlu memproses event invalidasi terbaru dan membuang event-event lama yang kedaluwarsa.
Perilaku TTL pada Quorum Queue #
Sebagai tipe antrean modern yang berorientasi pada replikasi konsensus Raft, Quorum Queue mendukung fitur Message TTL sepenuhnya dengan jaminan konsistensi yang sangat ketat.
Pada Classic Queue, pembersihan pesan expired dilakukan secara independen oleh node tempat antrean tersebut aktif. Jika terjadi split jaringan, node yang terisolasi mungkin memiliki persepsi waktu yang berbeda dan membuang pesan secara sepihak.
Namun, pada Quorum Queue, proses evaluasi dan eksekusi kedaluwarsa pesan diatur dengan disiplin terdistribusi:
- Hanya node Pemimpin (Leader) Quorum Queue yang berwenang mengevaluasi apakah masa hidup pesan sudah habis berdasarkan waktu sistem lokalnya.
- Ketika Leader mendeteksi pesan expired, ia tidak langsung menghapus pesan dari memori lokalnya.
- Leader menulis perintah penghapusan pesan mati tersebut ke log Raft.
- Log tersebut direplikasikan ke node pengikut (Followers).
- Begitu kuorum menyetujui, pesan dihapus secara konsisten di seluruh replika klaster, atau dialihkan secara aman ke DLX.
Mekanisme ini memastikan bahwa status kedaluwarsa pesan tetap sinkron dan konsisten di seluruh klaster, mencegah terjadinya duplikasi pengiriman pesan usang akibat proses failover node pemimpin yang crash mendadak.
Implementasi Kode Go (Golang) #
Berikut adalah contoh program lengkap dalam bahasa Go untuk mendeklarasikan arsitektur Delay Queue menggunakan konfigurasi Queue-level TTL dan Dead Letter Exchange (DLX) menggunakan library amqp091-go.
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
func main() {
// 1. Membuka koneksi fisik ke broker RabbitMQ
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
if err != nil {
log.Fatalf("Gagal membuka koneksi ke RabbitMQ: %v", err)
}
defer conn.Close()
// 2. Membuat channel AMQP
ch, err := conn.Channel()
if err != nil {
log.Fatalf("Gagal membuka channel AMQP: %v", err)
}
defer ch.Close()
// -------------------------------------------------------------
// DEKLARASI TOPOLOGI RETRY DELAY QUEUE (DENGAN TTL)
// -------------------------------------------------------------
// A. Deklarasi Exchange Jeda (Retry Exchange)
err = ch.ExchangeDeclare(
"retry.exchange",
"direct",
true,
false,
false,
false,
nil,
)
if err != nil {
log.Fatalf("Gagal mendeklarasikan retry.exchange: %v", err)
}
// B. Deklarasi Antrean Jeda (Delay Queue) dengan TTL 10 Detik
// Antrean ini berfungsi menahan pesan, tanpa konsumen yang terhubung.
// Begitu 10 detik habis, pesan mati dikirim kembali ke exchange utama.
delayQueueArgs := amqp.Table{
"x-queue-type": "quorum",
"x-message-ttl": int32(10000), // Masa jeda retry: 10.000 ms
"x-dead-letter-exchange": "main.exchange", // Kembalikan ke exchange utama setelah expired
"x-dead-letter-routing-key": "payment.execute", // Gunakan routing key ini saat kembali
}
delayQueue, err := ch.QueueDeclare(
"retry.delay.10s.queue", // nama antrean delay
true, // durable
false, // auto-delete
false, // exclusive
false, // no-wait
delayQueueArgs,
)
if err != nil {
log.Fatalf("Gagal mendeklarasikan retry.delay.queue: %v", err)
}
// Mengikat antrean delay ke retry exchange
err = ch.QueueBind(
delayQueue.Name,
"payment.retry",
"retry.exchange",
false,
nil,
)
if err != nil {
log.Fatalf("Gagal binding delay queue: %v", err)
}
// -------------------------------------------------------------
// DEKLARASI TOPOLOGI ANTREAN UTAMA
// -------------------------------------------------------------
// C. Deklarasi Exchange Utama
err = ch.ExchangeDeclare(
"main.exchange",
"direct",
true,
false,
false,
false,
nil,
)
if err != nil {
log.Fatalf("Gagal mendeklarasikan main.exchange: %v", err)
}
// D. Deklarasi Antrean Utama
// Jika pesan gagal di antrean ini, ia dialihkan ke retry exchange
mainQueueArgs := amqp.Table{
"x-queue-type": "quorum",
"x-dead-letter-exchange": "retry.exchange", // Kirim ke exchange retry jika gagal
"x-dead-letter-routing-key": "payment.retry", // Gunakan routing key kegagalan
}
mainQueue, err := ch.QueueDeclare(
"main.queue",
true,
false,
false,
false,
mainQueueArgs,
)
if err != nil {
log.Fatalf("Gagal mendeklarasikan main.queue: %v", err)
}
err = ch.QueueBind(
mainQueue.Name,
"payment.execute",
"main.exchange",
false,
nil,
)
if err != nil {
log.Fatalf("Gagal binding main queue: %v", err)
}
log.Println("[INFO] Topologi Delay Queue sukses diinisialisasi. Menjalankan konsumen...")
// -------------------------------------------------------------
// SIMULASI KONSUMEN KONTROL REQUEUE DENGAN DELAY
// -------------------------------------------------------------
msgs, err := ch.Consume(
mainQueue.Name,
"payment-worker",
false, // manual ACK wajib
false,
false,
false,
nil,
)
if err != nil {
log.Fatalf("Gagal memulai konsumen: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
for d := range msgs {
log.Printf("[RECEIVED] Memproses transaksi: %s", d.MessageId)
// Simulasikan kegagalan koneksi database sementara (transient error)
// Kita menolak pesan dengan requeue = false agar dialihkan ke DLX (retry.exchange)
log.Printf("[WARN] Terjadi gangguan transient database. Mengirimkan pesan ke delay queue...")
err := d.Nack(
false, // multiple
false, // requeue = false (pesan masuk ke retry.exchange -> retry.delay.10s.queue)
)
if err != nil {
log.Printf("Gagal mengirimkan NACK: %v", err)
}
}
}()
// Tunggu sinyal interupsi OS
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
log.Println("[INFO] Mematikan service secara aman...")
}
Anti-Pattern vs Solusi Praktis #
Penggunaan TTL yang tidak tepat dapat menyebabkan penurunan kinerja broker yang parah. Berikut adalah beberapa kesalahan umum (anti-pattern) yang wajib dihindari:
Anti-Pattern 1: Menggunakan Message-Level TTL Dinamis untuk Pola Jeda Retry pada Satu Antrean #
Produsen mengirimkan pesan gagal dengan properti Expiration dinamis yang berbeda-beda (misal pesan A diberi 10 menit, pesan B diberi 10 detik) ke dalam satu antrean delay yang sama untuk membuat jeda retry dinamis.
Mengapa ini salah? #
Karena RabbitMQ hanya mengevaluasi kedaluwarsa pesan di tingkat pesan ketika pesan mencapai kepala antrean (Head-of-Line Blocking), pesan B dengan TTL 10 detik yang berada di belakang pesan A dengan TTL 10 menit tidak akan pernah kedaluwarsa tepat waktu. Pesan B terpaksa tertahan selama 10 menit mengikuti waktu hidup pesan A di depannya. Hal ini mengakibatkan waktu jeda retry menjadi kacau, tidak konsisten, dan memicu penumpukan memori RAM pada broker.
Solusi Praktis #
- Multiple Dedicated Delay Queues: Deklarasikan beberapa antrean delay dengan TTL statik yang berbeda menggunakan Queue-level TTL, misalnya
retry.delay.5s.queue,retry.delay.1m.queue, danretry.delay.10m.queue. Arahkan pesan gagal ke antrean delay yang sesuai dengan tingkatan retry-nya. Karena menggunakan Queue-level TTL, proses kedaluwarsa berjalan aktif dan bebas dari masalah HoL blocking. - Delayed Message Plugin: Pasang plugin resmi RabbitMQ
x-delayed-message. Plugin ini memungkinkan kita menyetel jeda waktu dinamis per pesan secara aman menggunakan database Mnesia internal broker untuk menahan pesan di sisi exchange, menghindari antrean fisik sebelum waktunya tiba.
Anti-Pattern 2: Menyetel TTL Tanpa Mengonfigurasi Dead Letter Exchange #
Mengaktifkan fitur TTL pada antrean utama atau pesan individu untuk membatasi backlog usang, tetapi membiarkan pesan expired terhapus secara permanen tanpa terikat dengan DLX.
Mengapa ini salah? #
Pola ini rentan memicu kehilangan data transaksi penting tanpa jejak (silent data loss). Jika terjadi penumpukan backlog parah akibat konsumen down selama beberapa jam, jutaan pesan transaksi sah akan kedaluwarsa dan dihapus selamanya oleh broker. Kita tidak akan pernah tahu transaksi apa saja yang terbuang dan tidak memiliki cara untuk melakukan rekonsiliasi data.
Solusi Praktis #
Selalu kombinasikan penggunaan TTL dengan Dead Letter Exchange (DLX). Pesan yang kedaluwarsa harus dialihkan ke Dead Letter Queue (DLQ) khusus agar tim operasional dapat memantau tingkat kedaluwarsa pesan dan melakukan audit manual terhadap transaksi yang kedaluwarsa tersebut.
Ringkasan #
- Pengertian Message TTL — Time-To-Live menentukan batas maksimal waktu hidup pesan di dalam antrean dalam satuan milidetik sebelum dianggap kedaluwarsa oleh broker.
- Queue-Level vs Message-Level — Queue-Level TTL (
x-message-ttl) membatasi seluruh pesan secara seragam dan dievaluasi secara aktif. Message-Level TTL (expiration) disetel dinamis per pesan oleh produsen dan dievaluasi pasif.- Masalah Head-of-Line Blocking — Evaluasi pasif pada Message-Level TTL menyebabkan pesan kedaluwarsa di baris belakang tertahan di memori dan tidak bisa keluar selama pesan di kepala antrean belum kedaluwarsa atau dikonsumsi.
- Mekanisme Delay Queue — Pola jeda retry dapat dibangun tanpa scheduler eksternal dengan mengalirkan pesan gagal ke antrean tanpa konsumen yang dikonfigurasi dengan TTL (sebagai timer) dan DLX (sebagai rute kembali ke antrean utama).
- Konsistensi Quorum Queue — Evaluasi kedaluwarsa pada Quorum Queue diatur secara konsisten oleh node Pemimpin melalui konsensus Raft, menghindari ketidaksinkronan status antar replika klaster.
- Solusi Desain Terbaik — Hindari menyetel TTL dinamis pada antrean tunggal; gunakan pola beberapa antrean delay dengan TTL statis seragam atau pasang plugin
x-delayed-message.
← Sebelumnya: Dead Letter Exchange (DLX) Berikutnya: Exponential Backoff →