# Membangun Event-Driven Architecture Skala Jutaan Event dengan Kafka

> Diterbitkan oleh Literally Web ID pada 21 Oct 2026
> Sumber: https://literally.web.id/articles/membangun-event-driven-architecture-skala-jutaan-event-dengan-kafka

---

## 1. Alarm Tengah Malam: Saat Layanan Produksi Mulai Tumbang

Pukul 02:14 WIB, suara nyaring dari aplikasi PagerDuty di ponsel saya memecah keheningan malam. Bukan sekadar notifikasi biasa, melainkan *sev-one alert* yang menandakan tingkat keparahan maksimal. Dashboard Grafana memperlihatkan pemandangan yang membuat jantung berdegup kencang: *error rate* pada layanan backend Node.js melonjak drastis melewati angka 45%, sementara latensi rata-rata dari edge Cloudflare melesat dari 45 milidetik menjadi lebih dari 8 detik. Di balik layar, jutaan event transaksi dari aplikasi web yang didukung oleh arsitektur event-driven berbasis Kafka tiba-tiba mengalami kemacetan parah layaknya jalan tol Jakarta yang lumpuh total saat jam pulang kantor.

Sebagai Principal Systems Architect yang memegang kendali infrastruktur, kepanikan bukan pilihan. Masalah ini bermula ketika kampanye flash sale tengah malam mendadak membanjiri klaster Apache Kafka kami dengan jutaan event per detik, jauh melampaui kapasitas normal harian. Alih-alih sistem berjalan mulus secara asinkron seperti yang dijanjikan dalam teori arsitektur modern, backend web yang menggunakan kombinasi Nginx, PHP-FPM, dan Go mendadak kolaps karena kehabisan koneksi database MySQL, sementara Redis caching mengalami *cache stampede* yang masif. Pesan-pesan berukuran masif mulai menumpuk di dalam topik Kafka, memicu efek domino yang merembet ke seluruh ekosistem layanan mikro. Sebelum kita bisa memperbaiki keadaan, kita harus melacak jejak digital dari anomali ini melalui tumpukan log yang menggunung di server penampung.

## 2. Jejak Digital: Menelusuri Anomali Log & Lonjakan Metrik

Menghadapi sistem skala besar yang sedang sekarat, langkah pertama saya adalah membuka terminal dan langsung menerobos masuk ke dalam container log aggregator menggunakan cangkang bash untuk membedah isi perut sistem. Saya langsung mengarahkan pandangan ke metrik Kafka consumer lag menggunakan perintah bawaan untuk melihat seberapa parah antrean pesan yang tertinggal:

```bash
# Memeriksa consumer lag secara real-time pada Kafka cluster produksi
/opt/kafka/bin/kafka-consumer-groups.sh \
  --bootstrap-server kafka-broker-01.internal:9092 \
  --describe --group order-processing-group
```

> [!WARNING]
> Ketika nilai `CURRENT-OFFSET` jauh tertinggal dari `LOG-END-OFFSET` dengan selisih jutaan angka, itu adalah indikasi kuat bahwa *consumer worker* mengalami *deadlock*, kehabisan memori (*OOM Killer*), atau sedang terjebak dalam lingkaran setan *retry* akibat *poison pill message*.

Hasil terminal langsung menampakkan angka yang mencengangkan: lag mencapai 4.200.000 pesan pada partisi nomor 4, sementara partisi lainnya relatif sepi. Ini aneh. Dalam arsitektur event-driven yang sehat, distribusi beban seharusnya merata. Saya lantas membuka file konfigurasi Nginx dan PHP-FPM di salah satu web server backend untuk melihat apakah ada pembatasan koneksi yang mencurigakan:

```nginx
# Konfigurasi upstream Nginx yang sempat kewalahan menghadapi lonjakan event
upstream backend_cluster {
    server 10.0.1.10:9000 max_fails=3 fail_timeout=10s;
    server 10.0.1.11:9000 max_fails=3 fail_timeout=10s;
    keepalive 32;
}

server {
    listen 80;
    server_name api.domain.com;

    location /v1/events {
        include fastcgi_params;
        fastcgi_pass backend_cluster;
        fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
        fastcgi_read_timeout 300s;
    }
}
```

Ternyata, lonjakan event tersebut tidak terdistribusi dengan baik karena kesalahan fatal dalam pemilihan kunci partisi (*partition key*) saat event diproduksi dari aplikasi web klien. Mari kita bedah akar masalah yang sebenarnya mengapa sistem megah ini bisa bertekuk lutut di hadapan jutaan event.

## 3. Membongkar Biang Kerok: Akar Masalah yang Sebenarnya

Setelah menggali lebih dalam melalui log sistem, APM (Application Performance Monitoring), dan metrik sistem operasi, kami menemukan tiga dosa besar arsitektur yang bersekongkol meruntuhkan infrastruktur:

1. **Anatomi Partisi Kafka yang Buruk**: Tim pengembang menggunakan `UUID` acak sebagai *message key* untuk semua event yang dikirimkan ke Kafka. Akibatnya, Kafka mendistribusikan data secara tidak merata, dan sebagian besar event penarikan saldo menumpuk di segmen partisi tertentu. Ketika satu partisi kebanjiran 80% dari total trafik, *consumer thread* yang bertanggung jawab di partisi tersebut langsung mengalami kelelahan sumber daya (*thread starvation*).
2. **Strategi Consumer Group yang Naif**: Kami mengandalkan *default auto-commit* dengan ukuran *batch* yang terlalu besar (`max.poll.records = 5000`). Ketika satu pesan korup atau memicu *timeout* saat koneksi ke database MySQL terputus, *consumer* terus-menerus mencoba memproses ulang *batch* yang sama tanpa henti, menciptakan efek *rebalance storm* yang membuat klaster Kafka sibuk melakukan negosiasi ulang kepemilikan partisi alih-alih memproses data.
3. **Handling Poison Pill Message yang Absen**: Ada satu pesan aneh berformat JSON yang rusak dari salah satu *client payload* lama. Setiap kali pesan ini dibaca oleh consumer berbasis Go, aplikasi mengalami panik (*panic: interface conversion*) dan langsung *crash*. Kubernetes dengan sigap menghidupkannya kembali, membuat *pod* tersebut membaca ulang pesan maut yang sama secara berulang-ulang—sebuah skenario klasik *poison pill message*.

> [!IMPORTANT]
> Pesan beracun (*poison pill*) adalah silent killer dalam arsitektur berbasis antrean. Jika tidak ditangkap dan dipindahkan ke *Dead Letter Queue* (DLQ) secara instan, ia akan melumpuhkan seluruh rantai pemrosesan asinkron.

Untuk menyelamatkan sistem yang sedang sekarat ini detik itu juga, kita perlu meluncurkan serangkaian perintah darurat tanpa harus menunggu matahari terbit.

## 4. Tindakan Darurat: Menghidupkan Kembali Layanan dalam Hitungan Menit

Waktu adalah uang, dan setiap detik downtime berarti kerugian finansial yang masif bagi perusahaan. Berikut adalah *runbook* darurat yang saya eksekusi langsung di terminal untuk memulihkan kestabilan sistem dalam waktu kurang dari 60 detik:

Langkah pertama, hentikan sementara semua *consumer group* yang bermasalah untuk menghentikan siklus *rebalance storm*:

```bash
# Menghentikan konsumsi event secara paksa untuk meredakan beban database
/opt/kafka/bin/kafka-consumer-groups.sh \
  --bootstrap-server kafka-broker-01.internal:9092 \
  --group order-processing-group \
  --reset-offsets --to-current --execute
```

Langkah kedua, skala turun (*scale down*) pod consumer di Kubernetes untuk mencegah mereka terus menerus memakan memori CPU:

```bash
# Menurunkan replika consumer worker untuk meredakan tekanan pada MySQL
kubectl scale deployment event-consumer-worker --replicas=0 -n production
```

Langkah ketiga, buat topik khusus *Dead Letter Queue* (DLQ) secara manual untuk menampung pesan beracun agar tidak mengganggu antrean utama:

```bash
# Membuat topik DLQ baru dengan retensi 14 hari
/opt/kafka/bin/kafka-topics.sh \
  --create --bootstrap-server kafka-broker-01.internal:9092 \
  --replication-factor 3 --partitions 3 \
  --topic order-processing-dlq \
  --config retention.ms=1209600000
```

Setelah klaster tenang dan beban database kembali normal, kami menaikkan kembali jumlah *consumer worker* dengan kode aplikasi yang telah ditambal dengan mekanisme *try-catch* dan pengalihan ke DLQ. Sistem kembali bernapas lega, latensi turun drastis, dan error rate menyentuh angka nol. Namun, pekerjaan rumah sejati baru saja dimulai: memastikan insiden ini tidak pernah terulang melalui kebijakan arsitektur defensif.

## 5. Kebijakan Retrospektif: Menambal Celah agar Tak Pernah Terulang

Pengalaman pahit pukul 2 pagi tersebut memaksa kami merombak total fondasi arsitektur event-driven perusahaan. Kami menerapkan standar baru yang ketat bagi seluruh engineer:

- **Redesign Partisi Kafka**: Mengganti *message key* acak dengan atribut bisnis yang deterministik seperti `merchant_id` atau `user_id`. Dengan cara ini, semua event dari entitas yang sama dijamin masuk ke partisi yang sama secara berurutan, sementara beban antar partisi tersebar secara merata (*consistent hashing*).
- **Implementasi Robust Consumer Group**: Mengubah konfigurasi pembersihan *offset* dari *auto-commit* menjadi *manual commit* setelah database berhasil mengonfirmasi penyimpanan data. Kami juga memperkecil ukuran `max.poll.records` menjadi 100 untuk memastikan setiap batch dapat diselesaikan dalam batas waktu *session timeout*.
- **Penerapan Dead Letter Queue (DLQ) Otomatis**: Setiap consumer kini dibekali logika *retry* maksimal sebanyak 3 kali. Jika masih gagal, payload beserta *stack trace error*-nya secara otomatis dilempar ke topik DLQ, sementara sistem mengirimkan notifikasi *high-priority* ke saluran Slack tim engineering tanpa menghentikan aliran pesan utama.

Berikut adalah cuplikan kode Go yang kami terapkan pada *consumer worker* untuk menangani *poison pill message* secara elegan:

```go
package main

import (
	"context"
	"log"
	"github.com/segmentio/kafka-go"
)

func processMessageWithDLQ(ctx context.Context, r *kafka.Reader, w *kafka.Writer) {
	for {
		m, err := r.FetchMessage(ctx)
		if err != nil {
			log.Printf("Gagal mengambil pesan dari Kafka: %v", err)
			break
		}

		// Coba proses payload dengan mekanisme retry
		err = executeBusinessLogic(m.Value)
		if err != nil {
			log.Printf("Pesan korup terdeteksi, mengirim ke DLQ: %v", err)
			
			// Lempar ke topik Dead Letter Queue
			errDLQ := w.WriteMessages(ctx, kafka.Message{
				Key:   m.Key,
				Value: m.Value,
				Headers: append(m.Headers, kafka.Header{Key: "error-reason", Value: []byte(err.Error())}),
			})
			
			if errDLQ != nil {
				log.Printf("Kritis: Gagal menulis ke DLQ! %v", errDLQ)
				continue
			}
		}

		// Commit offset secara manual setelah sukses atau berhasil dilempar ke DLQ
		if err := r.CommitMessages(ctx, m); err != nil {
			log.Printf("Gagal melakukan commit offset: %v", err)
		}
	}
}

func executeBusinessLogic(data []byte) error {
	// Logika bisnis utama yang aman dari panic
	return nil
}
```

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

Agar tidak ada lagi alarm PagerDuty yang merenggut waktu tidur kami secara mengejutkan, kami membangun sistem peringatan dini yang proaktif menggunakan Prometheus dan Alertmanager yang terintegrasi langsung dengan webhook Slack dan PagerDuty. Berikut adalah aturan *alerting* valid yang kami pasang di klaster monitoring:

```yaml
groups:
- name: kafka_consumer_alerts
  rules:
  - alert: KafkaConsumerLagHigh
    expr: sum(kafka_consumergroup_lag{topic="order-processing"}) by (consumergroup) > 50000
    for: 2m
    labels:
      severity: critical
    annotations:
      summary: "Consumer lag sangat tinggi pada klaster Kafka"
      description: "Consumer group {{ $labels.consumergroup }} mengalami lag lebih dari 50.000 pesan selama 2 menit berturut-turut. Segera periksa kesehatan database dan consumer worker!"

  - alert: KafkaPartitionImbalance
    expr: max(kafka_partition_current_offset) by (topic) - min(kafka_partition_current_offset) by (topic) > 100000
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "Ketidakseimbangan partisi Kafka terdeteksi"
      description: "Distribusi event antar partisi pada topik {{ $labels.topic }} tidak merata, berisiko menyebabkan bottleneck."

  - alert: HighDLQTraffic
    expr: rate(kafka_messages_produced_total{topic=~ ".*-dlq"}[5m]) > 5
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: "Lonjakan pesan masuk ke Dead Letter Queue (DLQ)"
      description: "Terdapat lebih dari 5 pesan korup per detik yang masuk ke DLQ. Kemungkinan ada payload klien yang rusak."

  - alert: BackendDatabaseConnectionExhaustion
    expr: mysql_global_status_threads_connected / mysql_global_variables_max_connections > 0.85
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: "Koneksi database MySQL hampir habis"
      description: "Penggunaan koneksi MySQL telah melampaui 85% dari batas maksimum. Segera cek pool koneksi backend web!"
```

Melalui kombinasi arsitektur partisi yang matang, penanganan *poison pill* menggunakan DLQ yang disiplin, serta sistem pemantauan proaktif berbasis Prometheus, infrastruktur kami kini berdiri kokoh—siap menelan puluhan juta event harian tanpa rasa cemas.