1. Alarm Tengah Malam: Saat Layanan Produksi Mulai Tumbang
Pukul 02.14 WIB. Suar melengking dari PagerDuty memecah keheningan kamar saya. Layar ponsel mendadak menyala terang, menampilkan notifikasi merah menyala: High Error Rate on Production Web Cluster. Belum sempat saya menyeduh kopi hitam, dashboard Grafana sudah menyuguhkan pemandangan horor berupa grafik latensi API yang melompat vertikal dari 120 milidetik menjadi 14 detik, dibarengi lonjakan penggunaan memori pada Redis instance utama yang menyentuh angka 98%.
Di balik layar, jutaan job asinkron seperti pengiriman email transaksional, pemrosesan webhook pembayaran dari payment gateway, dan sinkronisasi data inventaris e-commerce sedang menumpuk layaknya antrean BBM bersubsidi. Arsitektur background worker yang selama ini berjalan mulus tiba-tiba mengalami choke. Proses worker PHP-FPM di balik layar berguguran karena kehabisan alokasi memori, sementara Supervisor yang bertugas menjaga mereka tetap hidup justru kewalahan melakukan spawn proses baru akibat resource starvation.
[!WARNING] Membiarkan Redis mencapai batas maksimal memori tanpa konfigurasi kebijakan eviction yang tepat (seperti
noeviction) adalah jalan pintas menuju bencana Out-Of-Memory (OOM) Killer yang akan mematikan proses database in-memory secara paksa.
Kepanikan awal ini harus segera diredam dengan kepala dingin. Sebagai engineer yang telah bertahun-tahun bergulat dengan infrastruktur skala besar, saya tahu betul bahwa menyalahkan aplikasi saja tidak cukup. Kita harus turun ke ruang mesin, menyelami log mentah, dan membedah apa yang sebenarnya terjadi di balik layar.
2. Jejak Digital: Menelusuri Anomali Log & Lonjakan Metrik
Langkah pertama yang saya ambil adalah masuk ke server bastion menggunakan SSH, lalu menerobos masuk ke node worker melalui jaringan internal. Jari-jemari langsung menari di atas terminal untuk memeriksa kesehatan sistem secara real-time menggunakan htop dan memantau log Supervisor.
# Masuk ke direktori log Supervisor untuk memantau proses worker yang sekarat
cd /var/log/supervisor/
tail -f worker-process-err.log | grep -i "Allowed memory size exhausted"
Baris-baris log langsung memuntahkan pesan error yang sudah tidak asing lagi, namun dalam skala yang mengerikan:
[2023-10-24 02:15:30] production.ERROR: FatalErrorException: Allowed memory size of 536870912 bytes exhausted (tried to allocate 20480 bytes) in /var/www/html/app/Jobs/ProcessBulkOrders.php on line 114
Ternyata, job ProcessBulkOrders yang bertugas menarik data ribuan transaksi sekaligus ke dalam satu payload memori menjadi biang keladi utama Memory Leak. Karena job tersebut gagal mengeksekusi dengan sukses, Redis secara otomatis mengembalikan item tersebut ke dalam antrean (queue) untuk dicoba ulang (retry). Siklus ini berulang ribuan kali per detik, menciptakan infinite loop yang secara agresif melahap sisa RAM yang ada.
[!TIP] Gunakan perintah
redis-cli monitorhanya untuk debugging kilat di staging environment. Di production, utilitas ini dapat melumpuhkan performa Redis seketika karena beban I/O yang masif.
Melalui pemeriksaan lebih lanjut pada CLI Redis, ditemukan fakta bahwa antrean default membengkak hingga 450.000 unprocessed jobs. Angka yang fantastis untuk sistem yang kapasitas normal prosesnya hanya 50 job per detik.
3. Membongkar Biang Kerok: Akar Masalah yang Sebenarnya
Setelah menarik napas panjang dan membedah struktur kode di balik job tersebut, akar masalahnya menjadi sangat terang benderang. Ada tiga dosa besar arsitektural yang dilakukan pada implementasi awal:
- Monolitik Payload: Seluruh objek Eloquent Model beserta relasinya di-serialize dan dimasukkan mentah-mentah ke dalam Redis list sebagai string JSON yang sangat besar.
- Absennya Backoff Strategy & Max Tries: Job dikonfigurasi untuk terus retry tanpa jeda waktu (exponential backoff), membuat Redis dan database relasional (MySQL) lumpuh karena dihantam ribuan kueri paralel secara konstan.
- Salah Konfigurasi Supervisor: Parameter
numprocsdiset terlalu tinggi (50 proses per server) tanpa memperhitungkan batas maksimal koneksi database, sehingga koneksi MySQL langsung habis (Too many connections).
Perbandingan antara desain yang rapuh dan desain yang tahan banting dapat dirangkum sebagai berikut:
| Aspek Arsitektur | Desain Rapuh (Sebelumnya) | Desain Tahan Banting (Target) |
|---|---|---|
| Payload Redis | Menyimpan seluruh objek data (berat) | Hanya menyimpan ID unik / Primary Key |
| Retry Policy | Instan tanpa jeda (Infinite loop) | Exponential Backoff + Max Tries limit |
| Dead Letter Queue | Tidak ada (Job gagal hilang/looping) | Tersedia (Dipindahkan ke DLQ setelah X gagal) |
| Process Manager | Supervisor over-provisioned | Supervisor terkalibrasi dengan Resource Limits |
Memahami kelemahan ini menuntun kita pada langkah penyelamatan darurat yang harus dieksekusi dalam hitungan detik.
4. Tindakan Darurat: Menghidupkan Kembali Layanan dalam Hitungan Menit
Situasi genting menuntut tindakan bedah minor yang presisi. Kita tidak punya waktu untuk melakukan refactor kode secara menyeluruh malam itu juga. Yang kita butuhkan adalah menghentikan pendarahan resource di server.
Berikut adalah rangkaian perintah darurat (Emergency Runbook) one-liner terminal yang langsung saya eksekusi untuk menyelamatkan sistem dalam waktu kurang dari 60 detik:
# 1. Hentikan sementara seluruh worker agar tidak memperparah beban database
sudo supervisorctl stop all
# 2. Flush antrean yang rusak parah di Redis untuk menyelamatkan RAM (Opsional/Darurat)
redis-cli -h 127.0.0.1 -p 6379 eval "return redis.call('del', unpack(redis.call('keys', 'queues:ProcessBulkOrders*')))" 0
# 3. Sesuaikan ulang konfigurasi file Supervisor untuk membatasi jumlah proses worker
sudo nano /etc/supervisor/conf.d/worker.conf
Di dalam file konfigurasi Supervisor, saya melakukan penyesuaian parameter agar beban server kembali stabil dan terukur. Transisi menuju pengaturan yang lebih defensif ini menjadi fondasi agar kejadian serupa tidak terulang.
5. Kebijakan Retrospektif: Menambal Celah agar Tak Pernah Terulang
Keesokan harinya, tim melakukan blameless post-mortem. Kami merombak total cara kerja background worker berbasis Redis dan Supervisor ini. Langkah pertama adalah menulis ulang konfigurasi Supervisor agar benar-benar tangguh dan aman dijalankan di lingkungan production skala enterprise.
Berikut adalah file konfigurasi supervisord.conf yang telah dioptimalkan dengan standar production-grade:
[program:enterprise-queue-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work redis --queue=high,default,low --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=8
redirect_stderr=true
stdout_logfile=/var/log/supervisor/worker.log
stopwaitsecs=3600
# Mencegah memory leak menumpuk dengan membatasi ukuran memori per proses
environment=LARAVEL_QUEUE=true
Selain itu, implementasi strategi Retry dan Dead Letter Queue (DLQ) di sisi kode aplikasi diperketat. Alih-alih menyimpan data berat, job kini hanya membawa referensi ID:
// Contoh struktur job yang ramah memori dan aman dari OOM
class ProcessBulkOrders implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $backoff = [60, 180, 600]; // Exponential backoff: 1m, 3m, 10m
protected int $batchId;
public function __construct(int $batchId)
{
$this->batchId = $batchId;
}
public function handle()
{
// Tarik data secara chunk langsung dari database berdasarkan ID
Model::where('batch_id', $this->batchId)->chunk(100, function ($records) {
// Proses data dengan aman tanpa membebani RAM
});
}
public function failed(\Throwable $exception)
{
// Pindahkan ke Dead Letter Queue atau catat ke sistem alert khusus
Log::critical("Job gagal secara permanen untuk Batch ID: {$this->batchId}");
}
}
6. Pasang Alarm: Otomasi Deteksi Dini untuk Tim On-Call
Sistem yang hebat bukan berarti tidak akan pernah gagal, melainkan sistem yang tahu cara memperingatkan kita sebelum pengguna menyadarinya. Untuk memastikan alarm tengah malam tidak lagi mengejutkan, kami memasang aturan alerting baru di Prometheus dan Alertmanager.
# Contoh aturan Prometheus Alert untuk memantau antrean Redis yang menumpuk
groups:
- name: queue_alerts
rules:
- alert: RedisQueueBacklogHigh
expr: redis_list_length{queue="default\