L
Event-Driven Architecture 🚨 Post-Mortem & Incident Runbook • Literally Web ID

Membangun Event-Driven Architecture Skala Tinggi dengan RabbitMQ

⏱️ 15 menit baca • 📅 20 Oct 2026 • 👁️ 2 views
Membangun Event-Driven Architecture Skala Tinggi dengan RabbitMQ

1. Alarm Tengah Malam: Saat Layanan Produksi Mulai Tumbang

Pukul 02:14 WIB. PagerDuty saya menjerit melengking, membangunkan tidur nyenyak setelah seharian berkutat dengan optimasi kueri MySQL. Di layar ponsel, grafik metrik dari Grafana menunjukkan pemandangan neraka: latency endpoint pembayaran melonjak dari rata-rata 120 milidetik menjadi 14 detik, dan dalam hitungan detik, lonjakan error HTTP 504 Gateway Timeouts mulai membanjiri kanal Slack tim engineering. Pelanggan sedang mencoba melakukan checkout massal untuk sebuah kampanye flash sale, namun sistem backend kami yang berbasis Node.js dan Go mendadak lumpuh total.

Di balik layar, sistem kami mengandalkan arsitektur event-driven menggunakan RabbitMQ sebagai jembatan pesan antara layanan pemesanan (order service) dan layanan pemenuhan inventaris (fulfillment service). Namun, alih-alih mengalir dengan mulus seperti air terjun, antrean pesan justru meluber, memori RAM pada klaster message broker menyentuh angka kritis 98%, dan koneksi TCP dari aplikasi backend mulai terputus secara massal karena kehabisan port ephemereal dan limit file descriptor.

Situasi ini memaksa kami untuk segera beralih dari mode pengembang yang tenang menjadi detektif infrastruktur. Menghadapi sistem skala tinggi yang kolaps di tengah malam butuh kepala dingin dan metode penelusuran yang sistematis, dimulai dari jejak digital yang tertinggal di dalam log server.

2. Jejak Digital: Menelusuri Anomali Log & Lonjakan Metrik

Begitu membuka terminal, langkah pertama saya adalah masuk ke dalam server RabbitMQ melalui SSH dan memantau status internal menggunakan CLI bawaan. Perintah sederhana rabbitmqctl list_queues name messages memory consumers langsung menampar wajah saya dengan fakta pahit: antrean pada exchange utama memiliki lebih dari 2,4 juta pesan tertahan (unacknowledged dan ready), sementara jumlah konsumen (consumers) yang aktif mendadak drop menjadi nol karena proses worker Go di backend mengalami panic akibat kehabisan memori (OOM Killer).

# Memeriksa status antrean kritis dan konsumsi memori secara real-time
sudo rabbitmqctl list_queues name messages messages_ready messages_unacknowledged memory

# Memantau alokasi resource node RabbitMQ yang sedang kritis
sudo rabbitmqctl status | grep -A 10 "memory"

[!WARNING] Jangan pernah melakukan restart service RabbitMQ secara membabi buta saat antrean menumpuk di memori tanpa strategi persistence yang jelas, karena Anda berisiko kehilangan jutaan data transaksi jika flag durable pada queue tidak dikonfigurasi dengan benar sejak awal!

Melalui penelusuran log Nginx dan PHP-FPM di frontend, serta log aplikasi Go di worker service, terlihat jelas bahwa akar masalahnya adalah efek domino. Lonjakan trafik HTTP memicu pembanjiran pesan ke RabbitMQ. Karena worker konsumer kewalahan memproses beban database MySQL di backend, mereka gagal mengirimkan sinyal ack (acknowledgement) tepat waktu. RabbitMQ mengira worker mati, lalu mengembalikan pesan ke antrean, menciptakan lingkaran setan (infinite loop) yang akhirnya menghabiskan seluruh sumber daya sistem. Untuk memahami bagaimana mengurai simpul kusut ini, kita harus membedah anatomi pengiriman pesan yang mendasar.

3. Membongkar Biang Kerok: Akar Masalah yang Sebenarnya

Masalah utama dari kelumpuhan ini bukan terletak pada kapasitas RabbitMQ semata, melainkan pada ketidakpahaman tim saat mendesain Anatomi Message Broker & Exchange di awal proyek. Tim sebelumnya menggunakan Direct Exchange dengan auto-ack yang dimatikan, namun tanpa membatasi prefetch count pada sisi konsumen. Akibatnya, satu worker yang sedang lambat dipaksa menelan ribuan pesan sekaligus, sementara worker lain yang sedang nganggur tidak kebagian tugas.

Mari kita bandingkan dua pola konfigurasi konsumer yang sering menjadi penentu hidup-matinya aplikasi berskala besar:

  • Konfigurasi Naif (Tanpa Prefetch & Tanpa Backpressure):

    // Node.js amqp library contoh buruk
    channel.consume(queueName, (msg) => {
        processHeavyTask(msg.content);
        channel.ack(msg);
    }, { noAck: false });
    

    Dampak: Konsumer menarik seluruh pesan di antrean ke dalam RAM lokalnya secara membabi buta, memicu Node.js Heap Out of Memory.

  • Konfigurasi Profesional (Dengan Prefetch Limit & Manual Ack):

    // Go amqp library contoh ideal
    err := ch.Qos(
        10,     // prefetch count: batasi maksimal 10 pesan per worker
        0,      // prefetch size
        false,  // global
    )
    failOnError(err, "Failed to set QoS")
    
    msgs, err := ch.Consume(
        queueName, // queue
        "",        // consumer tag
        false,     // auto-ack diatur false
        false,     // exclusive
        false,     // no-local
        false,     // no-wait
        nil,       // args
    )
    

[!TIP] Selalu terapkan basic.qos (prefetch count) pada setiap konsumer pesan Anda. Angka ideal biasanya berkisar antara 10 hingga 50 tergantung kompleksitas proses payload agar beban terdistribusi merata (fair dispatch) di antara sekumpulan worker nodes.

Tanpa pengaturan QoS prefetch ini, sistem tidak memiliki mekanisme backpressure alami. Ketika database MySQL mengalami lock contention akibat kueri yang tidak diindeks dengan baik, antrean pesan di RabbitMQ ikut membengkak, menyeret seluruh infrastruktur mikro layanan jatuh bersama-sama. Sebelum kita merancang ulang sistem pertahanan jangka panjang, kita harus memadamkan api yang sedang berkobar saat ini melalui serangkaian tindakan darurat di terminal.

4. Tindakan Darurat: Menghidupkan Kembali Layanan dalam Hitungan Menit

Dengan status server yang berada di ambang kernel panic, saya harus mengeksekusi serangkaian perintah penyelamatan darurat langsung di server produksi dalam kurun waktu 60 detik untuk memulihkan alur lalu lintas web.

Langkah pertama adalah membersihkan antrean yang korup atau poison messages yang terus-menerus gagal diproses, lalu mengisolasi worker yang bermasalah agar tidak membebani database:

# 1. Hentikan sementara seluruh proses backend consumer via Supervisor
sudo supervisorctl stop all-workers:*

# 2. Flush queue yang mengalami deadlock parah (HATI-HATI: Gunakan hanya jika data sudah di-backup)
sudo rabbitmqctl purge_queue critical_order_queue

# 3. Atur ulang kebijakan memory watermark RabbitMQ secara dinamis agar tidak langsung OOM
sudo rabbitmqctl set_vm_memory_high_watermark 0.8

# 4. Restart service RabbitMQ untuk membersihkan memori yang bocor
sudo systemctl restart rabbitmq-server

# 5. Jalankan kembali worker secara bertahap dengan batasan prefetch baru
sudo supervisorctl start all-workers:order-worker-1

Setelah eksekusi skrip darurat tersebut, grafik latensi di Grafana perlahan turun dari zona merah kembali ke zona hijau. Namun, memadamkan kebakaran saja tidak cukup; kita harus memastikan bahwa pesan yang gagal diproses tidak hilang begitu saja dan tidak menyebabkan sistem macet kembali di masa depan. Di sinilah pentingnya Konfigurasi Dead Letter Queue untuk Error Handling.

5. Kebijakan Retrospektif: Menambal Celah agar Tak Pernah Terulang

Belajar dari insiden dini hari tadi, kami merombak total arsitektur pengiriman pesan. Setiap antrean utama kini wajib dikawinkan dengan sebuah Dead Letter Exchange (DLX). Jika sebuah pesan gagal diproses sebanyak tiga kali (karena timeout koneksi ke Redis atau database MySQL), pesan tersebut tidak boleh dikembalikan ke antrean utama (requeue: true), melainkan harus dibuang secara elegan ke Dead Letter Queue (DLQ) untuk diinvestigasi nanti.

Berikut adalah cuplikan kode deklarasi topologi RabbitMQ yang aman dan tahan banting menggunakan bahasa Go:

// Mendeklarasikan Dead Letter Exchange dan DLQ
err = ch.ExchangeDeclare(
    "dlx.ecommerce", // nama exchange DLX
    "direct",        // tipe exchange
    true,            // durable
    false,           // auto-deleted
    false,           // internal
    false,           // no-wait
    nil,
)

_, err = ch.QueueDeclare(
    "orders.dlq",
    true,
    false,
    false,
    false,
    nil,
)

err = ch.QueueBind(
    "orders.dlq",
    "order.failed",
    "dlx.ecommerce",
    false,
    nil,
)

// Mendeklarasikan Queue Utama dengan argumen DLX
args := amqp.Table{
    "x-dead-letter-exchange":    "dlx.ecommerce",
    "x-dead-letter-routing-key": "order.failed",
    "x-message-ttl":             60000, // Opsional: TTL pesan 60 detik
}

_, err = ch.QueueDeclare(
    "orders.primary",
    true,
    false,
    false,
    false,
    args,
)

Dengan pola ini, jika ada payload data yang cacat atau layanan pihak ketiga (seperti payment gateway) sedang down, sistem tetap berjalan normal tanpa mengorbankan throughput keseluruhan. Pesan yang gagal akan terkumpul rapi di DLQ, menunggu tim teknik memperbaikinya tanpa mengunci antrean utama. Agar kejadian serupa dapat terdeteksi sebelum alarm PagerDuty berbunyi keras, kita perlu memasang sistem peringatan dini yang proaktif.

6. Pasang Alarm: Otomasi Deteksi Dini untuk Tim On-Call

Infrastruktur yang handal bukan berarti tidak pernah mengalami kegagalan, melainkan sistem yang tahu kapan dirinya akan gagal dan memberi tahu kita lebih awal. Kami mengintegrasikan Prometheus dan Alertmanager langsung ke metrik eksporter RabbitMQ untuk memantau kesehatan klaster secara ketat.

Berikut adalah aturan peringatan (alerting rules) YAML valid yang kami terapkan di Prometheus untuk memantau anomali antrean pesan:

groups:
  - name: rabbitmq_alerts
    rules:
      - alert: RabbitMQQueueBacklogCritical
        expr: rabbitmq_queue_messages_ready{queue="orders.primary
🏷️ Tags: #Nginx #High Concurrency #Performance #Sysadmin #Web Server