Manajemen Background Jobs Skala Besar dengan Distributed Cron di Node.js
1. Alarm Tengah Malam: Saat Layanan Produksi Mulai Tumbang
Pukul 02.14 WIB. Ponsel saya berdering melengking, memecah keheningan malam lewat nada alarm PagerDuty yang paling saya benci: Critical Alert - Database CPU Utilization > 95%. Belum sempat saya mencuci muka, notifikasi Slack dari kanal #incident-response mulai bergemuruh tiada henti. Layanan invoice processing utama kami melambat drastis, membuat ribuan transaksi checkout pengguna tertahan di gerbang pembayaran. Pelanggan mulai mengeluh tidak bisa menyelesaikan pembelian, sementara tim customer support sudah bersiap menghadapi gelombang amuk.
Sebagai Lead Infrastructure Engineer yang bertahun-tahun mengawal arsitektur web modern, kepanikan bukanlah opsi. Saya langsung membuka terminal, menyambungkan VPN, dan melompat ke server bastion untuk melihat apa yang sedang terjadi pada ekosistem backend Node.js kami yang berjalan di atas klaster Kubernetes. Grafik di Datadog menunjukkan lonjakan aneh: bukan karena trafik pengguna yang meledak, melainkan karena proses background jobs yang seharusnya berjalan sekali sehari, mendadak tereksekusi secara masif di waktu yang sama persis oleh semua pod Node.js yang aktif.
Masalah klasik ini muncul karena kami mengandalkan pustaka penjadwal cron tradisional di dalam aplikasi Node.js tanpa mekanisme kordinasi antar-server. Ketika klaster kami melakukan scaling otomatis dari 2 pod menjadi 10 pod untuk mengantisipasi flash sale, setiap pod secara independen membaca definisi cron dan mengeksekusi tugas pemotongan saldo serta pembuatan invoice secara bersamaan. Akibatnya, database PostgreSQL kami dihantam oleh query ganda yang identik, menciptakan deadlock massal yang melumpuhkan seluruh sistem.
[!WARNING] Jangan pernah menjalankan pustaka in-process cron seperti
node-cronatauagendasecara langsung pada arsitektur multi-instance tanpa pengamanan distributed lock. Kegagalan mengontrol eksekusi ini adalah jalan pintas menuju korupsi data dan kelumpuhan database produksi.
Menatap layar gelap yang diterangi cahaya terminal, saya tahu bahwa menaikkan kapasitas CPU database tidak akan menyelesaikan akar masalah. Kami harus segera memadamkan api ini sebelum pagi tiba, lalu merombak total cara kerja penjadwal latar belakang kami agar kejadian serupa tidak pernah terulang. Mari kita telusuri jejak digital yang ditinggalkan oleh bencana malam itu untuk memahami apa yang sebenarnya terjadi di balik layar.
2. Jejak Digital: Menelusuri Anomali Log & Lonjakan Metrik
Langkah pertama dalam investigasi insiden tentu saja membuka log aplikasi dan metrik infrastruktur. Saya menggunakan kubectl untuk menarik log dari beberapa pod Node.js yang mengalami lonjakan beban menggunakan perintah berikut:
kubectl logs -n production -l app=payment-worker --tail=500 --timestamps=true | grep "CRON_EXECUTION_START"
Hasilnya langsung memperjelas pola kegagalan. Terlihat jelas bahwa pada pukul 02:10:00 UTC, sepuluh pod yang berbeda secara serentak mencetak log "Starting daily reconciliation job". Tidak ada mekanisme antrean, tidak ada pembagian tugas, semuanya berjalan liar. Di sisi lain, metrik Redis dan Nginx menunjukkan grafik yang kontras: trafik HTTP dari klien sebenarnya landai, tetapi connection pool PostgreSQL langsung terkuras habis melayani ribuan transaksi duplikat.
# Konfigurasi Nginx upstream yang mengarahkan trafik ke klaster Node.js worker
upstream node_workers {
server 10.0.1.10:3000 max_fails=3 fail_timeout=10s;
server 10.0.1.11:3000 max_fails=3 fail_timeout=10s;
server 10.0.1.12:3000 max_fails=3 fail_timeout=10s;
keepalive 32;
}
server {
listen 80;
server_name api.company.com;
location / {
proxy_pass http://node_workers;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Melihat konfigurasi di atas, jelas bahwa arsitektur web server kami sudah dirancang dengan baik untuk membagi beban incoming HTTP request. Namun, cron job tidak melewati Nginx; mereka hidup di dalam memori setiap proses Node.js yang berjalan di dalam kontainer. Ketika kontainer bertambah, jumlah eksekutor cron ikut melipatgandakan diri.
[!TIP] Selalu pisahkan web server process (yang melayani HTTP request) dari worker process (yang menangani background jobs) jika skala sistem Anda sudah memasuki ranah multi-instance. Menggabungkan keduanya dalam satu proses aplikasi sering kali mengorbankan stabilitas salah satunya.
Untuk membuktikan hipotesis ini, saya memeriksa implementasi kode lama yang memicu kekacauan tersebut. Sebuah skrip sederhana menggunakan pustaka penjadwal standar yang berjalan tanpa pengaman apa pun:
// KODE LAMA (BERBAHAYA): Menjalankan cron di setiap instance Node.js
const cron = require('node-cron');
const { processDailyInvoices } = require('./services/invoice');
cron.schedule('0 2 * * *', async () => {
console.log('Starting daily reconciliation job...');
try {
await processDailyInvoices();
console.log('Reconciliation completed.');
} catch (error);
console.error('Reconciliation failed:', error);
}
});
Kode di atas terlihat sangat sederhana dan tidak berbahaya di lingkungan local development di mana kita hanya menjalankan satu instans aplikasi. Namun, ketika diaplikasikan ke dalam klaster Kubernetes dengan replika minimal 5 buah, kode ini berubah menjadi bom waktu yang siap meledak setiap pukul 2 pagi. Akar masalahnya sudah ditemukan secara utuh; kini saatnya membongkar biang kerok secara mendalam sebelum kita merumuskan solusi defensifnya.
3. Membongkar Biang Kerok: Akar Masalah yang Sebenarnya
Mari kita bedah mengapa pendekatan di atas gagal total pada skala enterprise. Masalah utamanya terletak pada konsep Shared-Nothing Architecture yang diterapkan secara keliru pada penjadwalan waktu. Di dalam sistem terdistribusi, setiap node memiliki jam internalnya sendiri (meskipun sudah disinkronkan melalui NTP, selisih milidetik tetap bisa terjadi) dan tidak tahu menahu apa yang sedang dikerjakan oleh node di sebelah