Post-Mortem & Incident Runbook: Arsitektur Event-Driven Berskala Besar dengan Apache Kafka dan Go
1. Alarm Tengah Malam: Saat Layanan Produksi Mulai Tumbang
Pukul 02.14 WIB. PagerDuty saya menjerit kencang lewat nada dering 'Nuclear' yang sengaja dirancang untuk membangunkan orang mati. Sambil meraba-raba ponsel di atas nakas, mata saya langsung terbuka lebar melihat metrik Prometheus di layar laptop yang dibuka setengah sadar: latensi endpoint utama meroket dari 45 milidetik menjadi 8.200 milidetik, sementara error rate HTTP 502 Bad Gateway dari Nginx melonjak melewati ambang batas 35%. Di balik layar, arsitektur event-driven kami yang biasanya tenangโmengandalkan Apache Kafka sebagai tulang punggung logistik data dan microservices berbasis Go sebagai motor penggeraknyaโkini tampak seperti jalan tol Jakarta di jam pulang kerja saat banjir bandang: macet total, klakson di mana-mana, dan kendaraan saling bertubrukan.
Pelanggan mulai mengeluh di Twitter bahwa transaksi pembayaran mereka 'gantung', tidak sukses tapi saldo terpotong. Tim frontend yang melayani antarmuka berbasis WebSocket melaporkan bahwa koneksi klien terputus secara massal karena timeout. Ada apa dengan sistem kita? Mengapa ekosistem yang tadinya mendistribusikan jutaan event per jam ini tiba-tiba lumpuh total? Untuk menemukan jawabannya, kita harus masuk ke dalam parit investigasi, membuka terminal, dan mulai membaca log sistem baris demi baris sebelum situasinya semakin memburuk.
2. Jejak Digital: Menelusuri Anomali Log & Lonjakan Metrik
Langkah pertama seorang penyelidik sistem adalah menatap metrik, bukan menebak-nebak. Saya langsung melompat ke Grafana dashboard khusus Kafka Consumer Group dan melihat pemandangan yang membuat jantung berdegup kencang: Consumer Lag pada topik transaction.events menembus angka 4,2 juta pesan dan terus merangkak naik secara eksponensial. Di sisi lain, penggunaan CPU pada pod-pod backend Go di Kubernetes melompat ke 98%, sementara konsumsi memori menunjukkan grafik menanjak lurus tanpa tanda-tanda berhenti (memory leak klasik).
# Memeriksa status consumer group dan lag secara langsung via CLI Kafka
bin/kafka-consumer-groups.sh --bootstrap-server kafka-broker-01:9092 --describe --group payment-processor-group
Output terminal memperlihatkan kenyataan pahit: partisi 0 hingga 11 mengalami rebalance storm yang berulang-ulang setiap 30 detik. Setiap kali consumer mencoba melakukan heartbeat, prosesor Go mengalami Stop-The-World Garbage Collection yang masif karena kehabisan alokasi heap memori akibat jutaan objek pointer yang menumpuk. Pesan-pesan dari Kafka ditarik ke dalam memori secara membabi buta tanpa manajemen concurrency yang sehat, sementara koneksi ke database MySQL utama di balik aplikasi mulai menolak koneksi baru akibat jenuhnya connection pool.
// Potongan kode asal-asalan yang menjadi biang kerok di lapangan: Unbounded Concurrency
for msg := range consumer.Messages() {
go func(m *KafkaMessage) {
// Tanpa worker pool, goroutine spawned tanpa batas
processTransaction(m)
}(msg)
}
Pola unbounded goroutine spawning di atas tampak elegan di atas kertas, namun di dunia nyata, ketika volume event melonjak, ia menciptakan DoS attack internal terhadap sistem kita sendiri. Dari sinilah kita mulai membongkar akar masalah yang sebenarnya.
3. Membongkar Biang Kerok: Akar Masalah yang Sebenarnya
Setelah menarik napas dalam-dalam dan membaca stack trace dari core dump yang berhasil diselamatkan, akar masalahnya mengerucut pada tiga dosa besar arsitektur yang sering diabaikan oleh para engineer:
- Konsep Partitioning dan Consumer Group yang Salah Kaprah: Kami membuat topik Kafka dengan 12 partisi, namun mendesain consumer group Go dengan worker yang melakukan proses sinkronisasi berat ke database relasional pada setiap pesan masuk, tanpa memanfaatkan mekanisme Partitioning Key yang merata. Akibatnya, sebagian besar payload menumpuk di partisi tertentu, memicu hotspotting.
- Ketiadaan Handling Dead Letter Queue (DLQ) yang Elegan: Ketika satu transaksi gagal divalidasi karena masalah integritas data di database, aplikasi Go melakukan infinite retry secara lokal pada pesan tersebut. Pesan yang sama ditarik ulang, gagal lagi, memblokir offset commit, dan menyeret seluruh partisi ke dalam jurang kemacetan.
- Optimasi Producer Batching di Bahasa Go yang Buruk: Di sisi hulu (producer), aplikasi pengirim memuntahkan pesan ke Kafka satu per satu tanpa batching (
Linger.msdibiarkan 0). Ini membuat beban jaringan (network I/O) melonjak ribuan kali lipat, memicu backpressure yang merembes ke seluruh rantai layanan web kita, termasuk Nginx dan Redis di lapisan cache.
[!WARNING] Jangan pernah membiarkan konfigurasi
Linger.msbernilai 0 pada sistem berskala besar kecuali Anda ingin melumpuhkan klaster Kafka Anda sendiri dalam hitungan menit akibat kehabisan network socket.
4. Tindakan Darurat: Menghidupkan Kembali Layanan dalam Hitungan Menit
Kita tidak punya waktu untuk berdiskusi filosofis tentang arsitektur yang benar. Pasien sedang sekarat di atas meja operasi. Berikut adalah rangkaian perintah darurat (Emergency Runbook) yang langsung saya eksekusi di terminal untuk menghentikan pendarahan sistem:
# 1. Melakukan scaling down pod consumer Go secara paksa untuk meredakan beban database
kubectl scale deployment payment-processor-consumer --replicas=0 -n production
# 2. Melakukan reset offset Kafka untuk melompati pesan beracun (poison pill) yang memblokir antrean
bin/kafka-consumer-groups.sh --bootstrap-server kafka-broker-01:9092 --group payment-processor-group --topic transaction.events --reset-offsets --to-offset 4150000 --execute
# 3. Menaikkan timeout Nginx upstream dan membuang koneksi idle
sudo nginx -t && sudo systemctl reload nginx
Dalam kurun waktu 8 menit setelah eksekusi skrip darurat tersebut, grafik metrik mulai menunjukkan tanda-tanda pemulihan. Latency turun drastis ke level normal, dan error rate 502 lenyap dari layar pemantauan. Namun, pekerjaan rumah sesungguhnya baru saja dimulai: kita harus memastikan insiden ini tidak pernah terulang dengan merombak total pendekatan teknis kita.
5. Kebijakan Retrospektif: Menambal Celah agar Tak Pernah Terulang
Belajar dari tragedi dini hari tersebut, kami langsung menggelar sesi post-mortem dan merumuskan ulang standar operasional arsitektur event-driven kami. Pertama, kami memperketat implementasi Partitioning dan Consumer Group dengan memastikan bahwa setiap consumer di Go wajib menggunakan Worker Pool berkapasitas tetap (bounded concurrency) serta memetakan kunci partisi berdasarkan Merchant ID untuk menghindari hotspotting.
// Implementasi Bounded Worker Pool yang aman dan tahan banting di Go
type WorkerPool struct {
workerQueue chan chan *KafkaMessage
maxWorkers int
}
func NewWorkerPool(maxWorkers int) *WorkerPool {
return &WorkerPool{
workerQueue: make(chan chan *KafkaMessage, maxWorkers),
maxWorkers: maxWorkers,
}
}
// Mencegah memory leak dan goroutine bomb secara permanen
Kedua, kami membangun mekanisme Dead Letter Queue (DLQ) secara Elegan. Setiap pesan yang gagal diproses setelah 3 kali percobaan (retry) tidak akan dibiarkan melakukan infinite block, melainkan langsung dilempar ke topik transaction.dlq dengan menyertakan header alasan kegagalan, sehingga tim operasional dapat memeriksanya nanti tanpa menghentikan aliran transaksi utama.
Ketiga, untuk urusan Optimasi Producer Batching di Bahasa Go, kami beralih menggunakan pustaka segmentio/kafka-go atau confluent-kafka-go dengan konfigurasi batching yang agresif namun aman:
// Konfigurasi Producer Go yang dioptimalkan untuk throughput tinggi
writer := &kafka.Writer{
Addr: kafka.TCP("kafka-broker-01:9092"),
Topic: "transaction.events",
Balancer: &kafka.Hash{},
BatchSize: 100,
BatchTimeout: 10 * time.Millisecond, // Memberikan jeda singkat untuk mengumpulkan batch
RequiredAcks: kafka.RequireOne,
}
[!TIP] Kombinasi
BatchSizedanBatchTimeoutyang tepat akan memangkas penggunaan CPU pada klaster Kafka hingga 60% sekaligus mendongkrak throughput pengiriman pesan secara signifikan.
6. Pasang Alarm: Otomasi Deteksi Dini untuk Tim On-Call
Arsitektur yang tangguh bukan berarti tidak akan pernah gagal, melainkan seberapa cepat ia mendeteksi kegagalan sebelum dirasakan oleh pengguna akhir. Kami menutup seluruh perbaikan ini dengan memasang jaring pengaman berupa aturan alerting baru di Prometheus dan Alertmanager yang terhubung langsung ke kanal Slack dan PagerDuty:
# Konfigurasi Prometheus Alert untuk Consumer Lag yang berbahaya
groups:
- name: KafkaConsumerAlerts
rules:
- alert: HighConsumerLag
expr: kafka_consumergroup_lag{group="payment-processor-group"} > 50000
for: 2m
labels:
severity: critical
annotations:
summary: "Consumer lag melampaui batas aman ( > 50k pesan )"
description: "Consumer group {{ $labels.group }} mengalami kemacetan parah pada topik {{ $labels.topic }}. Segera periksa worker pool!"
Dengan sistem deteksi dini ini, jika anomali serupa mulai merayap naik di masa mendatang, alarm akan berbunyi saat lag masih berada di angka 50.000 pesanโjauh sebelum menyentuh angka jutaan yang memicu kelumpuhan total. Pengalaman pahit ini mengajarkan kita satu hukum abadi dalam dunia infrastruktur skala besar: Sistem yang hebat bukanlah sistem yang tidak pernah rusak, melainkan sistem yang tahu bagaimana cara menyelamatkan dirinya sendiri sebelum kita sempat menyeduh secangkir kopi pagi.