1. Alarm Tengah Malam: Saat Layanan Produksi Mulai Tumbang
Pukul 02:14 WIB, ponsel saya bergetar liar dengan nada alarm prioritas tertinggi dari PagerDuty. Layar menampilkan metrik yang membuat darah mendadak berdesir: API Gateway Error Rate melonjak menembus angka 42%, sementara Queue Lag untuk pemrosesan transaksi pembayaran dan pengiriman email transaksional melompat dari rata-rata 300 milidetik menjadi lebih dari 45 menit. Di ruang kendali virtual, rekan-rekan backend engineer mulai bersuara panik di kanal Slack darurat. Transaksi pelanggan tertahan, webhook dari payment gateway mulai berguguran karena timeout, dan antrean background worker kita yang biasanya tenang melaju kencang kini tampak lumpuh total.
Sistem yang kitaandalkanโkombinasi klasik PHP-FPM di balik Nginx, dikombinasikan dengan Redis sebagai broker antrean dan cron bawaan sistem operasi untuk penjadwalanโmendadak kolaps di tengah malam buta. Ini bukan sekadar lonjakan trafik musiman, melainkan sebuah kegagalan sistemik yang menguji ketahanan infrastruktur kita sampai batas maksimal. Dan untuk memahaminya, kita harus menyelam jauh ke dalam ruang mesin tempat data dan perintah saling berkejaran tanpa henti.
2. Jejak Digital: Menelusuri Anomali Log & Lonjakan Metrik
Tanpa membuang waktu, saya langsung membuka terminal, melompat melalui SSH bastion host menuju worker node utama, dan mulai menarik log dari sistem manajemen proses supervisor serta memantau kesehatan Redis. Perintah pertama yang saya eksekusi adalah memantau status antrean secara langsung menggunakan CLI Redis:
# Memeriksa ukuran antrean dan memori Redis yang tersisa
redis-cli -h redis.internal.net -p 6379 info memory
redis-cli -h redis.internal.net -p 6379 llen queue:background_jobs
Angka yang muncul di layar sangat mencengangkan: queue:background_jobs mencatat lebih dari 2,4 juta pending items, sementara penggunaan memori Redis telah menyentuh batas maksimal alokasi (OOM limit) dengan kebijakan noeviction yang aktif. Akibatnya, setiap kali worker mencoba mendorong data baru atau memperbarui status tugas, Redis menolak perintah tersebut dengan error OOM command not allowed when used memory > 'maxmemory'.
Namun, kejanggalan tidak berhenti di situ. Saat saya memeriksa log Supervisor (/var/log/supervisord.log), saya menemukan fenomena aneh: puluhan proses worker mendadak mati bersamaan (SIGTERM), lalu hidup kembali beberapa detik kemudian, memicu badai koneksi baru (connection storm) ke Redis yang langsung melumpuhkan port 6379 dalam sekejap. Penyelidikan awal menunjukkan adanya bentrok eksekusi cron job ganda yang mencoba menembakkan tugas masif secara bersamaan, memicu memory spike yang fatal pada satu node, yang kemudian merembet ke seluruh klaster. Dari sinilah kita mulai menarik benang merah menuju akar masalah yang sebenarnya.
3. Membongkar Biang Kerok: Akar Masalah yang Sebenarnya
Setelah menggali lebih dalam ke dalam kode sumber dan konfigurasi infrastruktur, kita menemukan tiga dosa besar arsitektural yang selama ini dibiarkan tidur tenang. Pertama, penggunaan native system cron (/etc/cron.d/) di beberapa worker node terpisah tanpa mekanisme Leader Election. Ketika jam menunjukkan pukul 02:00, setiap node secara serentak mengeksekusi skrip php artisan schedule:run (atau setara di stack Node.js/Go), memicu duplicate job dispatch sebanyak jumlah node yang aktif.
Kedua, implementasi antrean di Redis menggunakan struktur data List (LPUSH dan BRPOPLPUSH) tanpa batas TTL (Time To Live) dan tanpa manajemen Dead Letter Queue (DLQ). Ketika sebuah payload tugas gagal diproses karena kesalahan sintaks atau timeout database, worker secara naif memasukkannya kembali ke antrean utama tanpa henti, menciptakan infinite retry loop yang dengan cepat melahap seluruh alokasi RAM Redis.
[!WARNING] Jangan pernah mengandalkan default system cron untuk memicu tugas terdistribusi di lingkungan multi-server tanpa mekanisme penguncian (locking). Tanpa distributed lock, Anda sedang merakit bom waktu yang siap meledak saat skala infrastruktur Anda bertambah.
Ketiga, konfigurasi PHP-FPM dan pool koneksi Redis tidak memiliki graceful shutdown timeout yang memadai. Ketika proses worker dihentikan secara mendadak oleh skrip deployment atau kehabisan memori, koneksi TCP ke Redis terputus secara tidak normal, meninggalkan orphan connections yang menumpuk hingga memicu maxclients limit di sisi Redis. Transisi dari diagnosis ini menuntut kita untuk segera mengambil tindakan penyelamatan darurat sebelum layanan benar-benar mati total.
4. Tindakan Darurat: Menghidupkan Kembali Layanan dalam Hitungan Menit
Situasi menuntut eksekusi cepat dan terukur. Kita tidak punya waktu untuk menulis ulang kode, melainkan harus memadamkan api yang sedang membakar infrastruktur. Berikut adalah rangkaian perintah darurat yang saya eksekusi langsung di terminal untuk memulihkan stabilitas Redis dan mengontrol kembali arus worker:
# 1. Hentikan sementara seluruh proses worker untuk mencegah badai koneksi
sudo supervisorctl stop worker-group-*
# 2. Flush koneksi klien yang menggantung di Redis untuk membebaskan port
redis-cli -h redis.internal.net client list | awk '{print $2}' | cut -d= -f2 | xargs -I {} redis-cli -h redis.internal.net client kill id {}
# 3. Pangkas sementara antrean yang macet parah atau pindahkan ke file backup darurat
redis-cli -h redis.internal.net lrange queue:background_jobs 0 10000 > /tmp/dead_jobs_backup.json
redis-cli -h redis.internal.net ltrim queue:background_jobs 10001 -1
# 4. Longgarkan kebijakan maxmemory Redis secara instan untuk memberikan ruang napas
redis-cli -h redis.internal.net config set maxmemory-policy allkeys-lru
# 5. Nyalakan kembali worker secara bertahap (staggered startup)
sudo supervisorctl start worker-group-01
sleep 10
sudo supervisorctl start worker-group-02
Langkah taktis di atas berhasil menurunkan penggunaan memori Redis dari 100% ke kisaran 45% dalam kurun waktu kurang dari tiga menit. Grafik error di Grafana mulai melandai, dan antrean transaksi perlahan-lahan terkikis habis. Namun, memadamkan api hanyalah separuh dari tugas seorang engineer; pekerjaan sesungguhnya adalah memastikan api yang sama tidak pernah berkobar lagi di masa depan.
5. Kebijakan Retrospektif: Menambal Celah agar Tak Pernah Terulang
Pengalaman pahit pukul 02:00 pagi itu memaksa kita melakukan perombakan total pada arsitektur penjadwalan dan antrean. Kita meninggalkan system cron mentah dan beralih ke pendekatan Distributed Cron berbasis Redis menggunakan mekanisme atomic locking (SET NX PX). Berikut adalah cuplikan implementasi pola Leader Election yang kini kita gunakan dalam bahasa Go untuk memastikan hanya satu instans cron yang mengeksekusi tugas dalam satu waktu:
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
func AcquireCronLock(ctx context.Context, client *redis.Client, lockKey string, ttl time.Duration) (bool, error) {
val := fmt.Sprintf("node-%d", time.Now().UnixNano())
success, err := client.SetNX(ctx, lockKey, val, ttl).Result()
if err != nil {
return false, err
}
return success, nil
}
// Penggunaan dalam penjadwal latar belakang
func RunScheduledTask(ctx context.Context, client *redis.Client) {
lockKey := "cron:lock:daily_settlement"
acquired, err := AcquireCronLock(ctx, client, lockKey, 30*time.Second)
if err != nil || !acquired {
fmt.Println("Cron sedang dieksekusi oleh node lain, melewati siklus ini.")
return
}
// Eksekusi logika bisnis kritis di sini
fmt.Println("Berhasil mengunci leader, menjalankan tugas latar belakang...")
}
Selain Distributed Cron, kita juga merevolusi struktur antrean Redis dengan menerapkan pola Reliable Queue menggunakan perintah RPOPLPUSH (atau LMOVE di Redis modern) yang memindahkan tugas ke antrean processing sementara sebelum benar-benar dihapus. Jika worker mengalami crash, tugas yang menggantung di antrean processing dapat diambil kembali secara otomatis (orphan recovery). Kita juga menyiapkan Dead Letter Queue (DLQ) khusus untuk menampung payload yang gagal diproses setelah 3 kali percobaan, lengkap dengan mekanisme exponential backoff dan notifikasi otomatis ke kanal Slack tim engineering.
6. Pasang Alarm: Otomasi Deteksi Dini untuk Tim On-Call
Arsitektur yang tangguh tidak lengkap tanpa sistem pengawasan yang proaktif. Kita tidak boleh lagi menunggu pelanggan melapor atau alarm PagerDuty berbunyi karena server sudah terlanjur tumbang. Kita membangun metrik kustom yang dikumpulkan oleh Prometheus dan divisualisasikan secara real-time di dashboard Grafana, dengan aturan peringatan (alerting rules) yang sangat presisi.
Berikut adalah konfigurasi aturan Prometheus (prometheus-alerts.yml) yang memantau kesehatan antrean Redis dan potensi kebuntuan worker secara otomatis:
groups:
- name: background-worker-alerts
rules:
- alert: RedisQueueBacklogHigh
expr: redis_list_length{key="queue:background_jobs"} > 50000
for: 5m
labels:
severity: critical
annotations:
summary: "Antrean background worker menumpuk sangat tinggi"
description: "Jumlah item dalam antrean melebihi 50.000 selama lebih dari 5 menit. Segera periksa kesehatan worker node."
- alert: WorkerProcessDown
expr: supervisor_process_state{state="RUNNING"} == 0
for: 1m
labels:
severity: warning
annotations:
summary: "Proses worker berhenti secara tidak normal"
description: "Proses supervisor untuk worker latar belakang pada node {{ $labels.instance }} telah mati."
- alert: RedisMemoryNearCapacity
expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.85
for: 2m
labels:
severity: warning
annotations:
summary: "Penggunaan memori Redis mendekati batas maksimal"
description: "Redis menggunakan lebih dari 85% dari alokasi maxmemory yang diizinkan. Risiko OOM crash tinggi."
Dengan sistem peringatan berlapis ini, tim on-call kini menerima sinyal bahaya jauh sebelum memori Redis habis atau antrean meluber ke angka jutaan. Pengalaman malam itu mengajarkan kita bahwa infrastruktur skala besar menuntut disiplin arsitektural yang ketat: hilangkan asumsi, amankan setiap titik kegagalan tunggal (single point of failure), dan selalu uji ketahanan sistem Anda sebelum dunia nyata yang mengujinya dengan cara yang paling kejam.