Worker lambat padahal pool DB terlihat sehat adalah kasus yang sering menipu. Dari luar, semua indikator utama tampak baik: health check hijau, CPU rendah, koneksi database belum menyentuh batas, bahkan error rate kecil. Namun antrean job terus menumpuk dan throughput turun. Masalahnya sering bukan pada database down atau worker crash, melainkan sistem yang terlihat sehat tetapi bergerak lambat karena bottleneck laten di connection pool, lock query, timeout, atau transaksi yang terlalu lama.
Analogi sederhananya mirip sistem yang tampak sehat namun mengalami penurunan energi secara diam-diam. Dalam backend, worker tetap hidup, koneksi tetap ada, dan metrik dasar masih normal, tetapi energi kerjanya turun: satu job menahan koneksi terlalu lama, pool tidak segera melepas resource, atau query menunggu lock. Akibatnya worker menjadi healthy but sedentary: hidup, tapi tidak produktif.
Gejala Nyata: Sistem Sehat, Throughput Turun
Studi kasus tipikal terlihat seperti ini:
- Jumlah job masuk stabil, tetapi queue depth terus naik.
- CPU aplikasi dan database tidak tinggi.
- Health endpoint merespons normal.
- Jumlah koneksi database aktif terlihat di bawah batas konfigurasi.
- Tidak ada lonjakan error besar di dashboard utama.
- Latency pemrosesan per job naik perlahan, bukan meledak sekaligus.
Ini penting: koneksi database yang “belum penuh” tidak berarti pool sedang sehat. Yang perlu dilihat bukan hanya jumlah koneksi, tetapi berapa lama worker menunggu koneksi, berapa lama koneksi dipakai, dan apakah query tertahan oleh lock atau transaksi panjang.
Metrik yang Menipu Saat Debug Backend
1. Health check hanya membuktikan proses masih hidup
Banyak health check hanya memverifikasi proses aplikasi bisa merespons HTTP atau melakukan ping sederhana ke database. Itu tidak membuktikan jalur kerja utama worker sehat. Worker queue bisa tetap jalan tetapi macet pada tahap ambil koneksi, menunggu lock, atau retry timeout yang terlalu agresif.
2. CPU rendah bukan berarti tidak ada bottleneck
Jika worker menunggu koneksi dari pool atau menunggu query selesai karena lock, CPU justru bisa terlihat rendah. Dari sisi sistem operasi, proses tidak sibuk menghitung; ia sedang blocked.
3. Jumlah koneksi normal bukan berarti pool longgar
Pool berukuran 20 dengan 12 koneksi aktif terlihat aman. Tetapi bila 12 koneksi itu dipakai lama oleh transaksi berat dan 30 worker lain antre mengambil koneksi, throughput tetap turun drastis. Angka koneksi saja tidak menunjukkan contention.
4. Error rate kecil bisa menyembunyikan latency besar
Jika timeout aplikasi diatur terlalu panjang, job tidak langsung gagal. Ia hanya lambat. Dashboard error tetap tampak baik, padahal antrean memanjang dan SLA gagal terpenuhi.
Pola Akar Masalah yang Paling Sering Terjadi
Connection pool terlihat sehat, tetapi sebenarnya tersaturasi
Beberapa tanda yang sering terlewat:
- Waktu tunggu untuk memperoleh koneksi meningkat.
- Durasi pemakaian koneksi per job naik perlahan.
- Pool memiliki koneksi idle sangat sedikit saat beban naik.
- Worker concurrency lebih besar daripada kapasitas pool yang realistis.
Masalah ini sering muncul ketika jumlah worker ditambah untuk mengejar backlog, tetapi pool database tidak ikut disesuaikan. Hasilnya justru kebalikan: lebih banyak worker saling berebut koneksi.
Transaksi terlalu lama
Worker kadang membuka transaksi terlalu awal, lalu melakukan terlalu banyak pekerjaan di dalamnya: membaca data, memanggil API eksternal, menulis beberapa tabel, menghitung ulang agregasi, lalu commit. Selama transaksi itu hidup, koneksi tertahan lebih lama dan lock bisa bertahan lebih lama juga.
Kesalahan umum:
- Melakukan HTTP request ke layanan lain di dalam transaksi.
- Memproses batch besar dalam satu transaksi panjang.
- Membuka transaksi sebelum validasi atau persiapan data yang sebenarnya tidak perlu di dalam transaksi.
Lock query dan blocking antar worker
Throughput worker bisa turun walau query tampak “berhasil” karena sebagian query menunggu lock. Ini sering terjadi pada job yang memperbarui baris yang sama, tabel agregasi yang sama, atau memakai pola select then update tanpa strategi locking yang tepat.
Timeout tidak selaras
Timeout di sisi aplikasi, driver database, pool, dan job runner harus konsisten. Jika timeout ambil koneksi terlalu panjang, worker akan diam lama tanpa gagal cepat. Jika timeout query terlalu longgar, lock wait atau query buruk bisa menahan banyak worker sekaligus. Jika timeout job lebih pendek daripada transaksi dan retry policy tidak hati-hati, masalah justru berulang dan memperparah antrean.
Langkah Investigasi yang Benar
1. Mulai dari gejala sistemik, bukan asumsi “DB aman”
Periksa metrik berikut secara bersamaan:
- Queue depth: apakah antrean terus naik?
- Job latency: berapa lama dari enqueue sampai selesai?
- Processing time: berapa lama eksekusi job aktif?
- Worker concurrency: berapa job paralel yang benar-benar berjalan?
- Pool wait time: berapa lama menunggu koneksi?
- Connection usage time: berapa lama koneksi dipegang?
- Slow query dan lock wait.
Jika queue naik, processing time naik, dan pool wait time ikut naik, fokuskan investigasi ke pool dan query lifecycle, bukan ke health check.
2. Tambahkan logging pada fase kritis job
Jangan hanya log “job mulai” dan “job selesai”. Tambahkan log terstruktur per fase agar waktu hilang bisa terlihat.
logger.info("job_started", { jobId, type, attempt })
const t1 = Date.now()
const conn = await dbPool.acquire()
logger.info("db_connection_acquired", {
jobId,
waitMs: Date.now() - t1
})
try {
const txStart = Date.now()
await conn.beginTransaction()
logger.info("job_phase", { jobId, phase: "load_records" })
// query baca
logger.info("job_phase", { jobId, phase: "update_state" })
// query tulis
await conn.commit()
logger.info("transaction_committed", {
jobId,
txMs: Date.now() - txStart
})
} catch (err) {
await conn.rollback()
logger.error("job_failed", { jobId, error: err.message })
throw err
} finally {
conn.release()
logger.info("db_connection_released", { jobId })
}Dengan pola ini, Anda bisa membedakan bottleneck pada:
- menunggu koneksi,
- durasi transaksi,
- query tertentu,
- atau fase non-DB yang tetap menahan koneksi.
3. Gunakan tracing untuk melihat durasi per span
Jika sistem sudah memakai OpenTelemetry atau tracing sejenis, pecah span menjadi:
- dequeue job,
- acquire DB connection,
- begin transaction,
- query per operasi penting,
- external API call,
- commit/rollback.
Tujuannya bukan sekadar melihat total latency, tetapi menemukan span yang menahan alur. Pada kasus pool tersembunyi, span acquire connection sering memanjang sebelum query benar-benar berjalan.
4. Periksa database dari sisi lock dan query aktif
Di hampir semua database relasional, Anda perlu memeriksa:
- query yang berjalan lama,
- session yang idle tetapi masih memegang transaksi,
- lock wait atau blocking session,
- statement yang sering muncul di slow query log.
Perintah tepatnya bergantung pada mesin database yang dipakai, jadi hindari menyalin query diagnosis secara buta. Yang penting, cari korelasi antara job lambat dan sesi database yang:
- aktif terlalu lama,
- berstatus menunggu lock,
- atau membuka transaksi tanpa commit cepat.
5. Bandingkan ukuran worker dengan ukuran pool
Misalnya Anda punya 50 worker paralel, tetapi pool efektif hanya 10 koneksi. Secara teori ini belum tentu salah, karena tidak semua worker butuh DB di saat yang sama. Namun jika hampir semua job menyentuh DB pada awal proses dan menahan koneksi cukup lama, Anda menciptakan antrean internal di depan database.
Aturan praktis: sesuaikan concurrency worker dengan karakter job, bukan hanya dengan jumlah CPU. Beban I/O ke database sering menjadi pembatas utama.
Contoh Root Cause dalam Studi Kasus
Dalam satu kasus nyata yang sering terjadi, worker pemroses pembayaran melakukan urutan seperti ini:
- Ambil job dari queue.
- Ambil koneksi DB.
- Buka transaksi.
- Baca status order.
- Panggil API eksternal untuk validasi atau sinkronisasi.
- Update beberapa tabel.
- Commit.
Dari sisi monitoring kasar:
- service up,
- DB connections belum penuh,
- CPU rendah,
- error sedikit.
Namun tracing menunjukkan banyak job menghabiskan waktu lama di dalam transaksi karena panggilan API eksternal dilakukan setelah transaksi dibuka. Akibatnya:
- koneksi pool dipegang lebih lama,
- beberapa row terkunci lebih lama,
- worker lain menunggu koneksi atau lock,
- throughput total turun walau tidak ada komponen yang benar-benar mati.
Ini contoh klasik backend yang tampak sehat tetapi kehilangan energi produksi secara diam-diam.
Patch yang Efektif
1. Pendekkan masa pakai koneksi dan transaksi
Pisahkan fase non-DB dari fase DB. Jangan pegang koneksi jika belum perlu, dan jangan buka transaksi sebelum operasi kritis siap dieksekusi.
// Kurang baik
const conn = await dbPool.acquire()
await conn.beginTransaction()
const external = await fetchExternalData() // menahan transaksi terlalu lama
await conn.query(/* update */)
await conn.commit()
conn.release()
// Lebih baik
const external = await fetchExternalData()
const conn = await dbPool.acquire()
try {
await conn.beginTransaction()
await conn.query(/* update berdasarkan external */)
await conn.commit()
} catch (err) {
await conn.rollback()
throw err
} finally {
conn.release()
}Mengapa ini bekerja? Karena pool dan lock hanya dipakai saat benar-benar perlu. Worker lain mendapat kesempatan lebih besar untuk maju.
2. Kurangi concurrency worker jika pool menjadi titik sempit
Menambah worker tidak selalu menaikkan throughput. Jika bottleneck ada pada database, concurrency berlebih justru menaikkan waktu tunggu. Sering kali lebih baik menurunkan worker concurrency sementara, lalu mengukur ulang:
- queue wait time,
- DB pool wait time,
- durasi transaksi,
- throughput total per menit.
Trade-off: backlog mungkin masih ada, tetapi sistem menjadi stabil dan lebih prediktif. Setelah akar masalah diperbaiki, concurrency bisa dinaikkan lagi.
3. Tinjau ulang ukuran pool dengan hati-hati
Memperbesar pool bisa membantu, tetapi bukan solusi universal. Jika masalah dasarnya lock atau transaksi panjang, pool lebih besar hanya memindahkan antrean lebih dekat ke database dan berpotensi menambah kontensi.
Pilih penyesuaian pool jika:
- database masih punya kapasitas koneksi dan query yang memadai,
- wait time pool tinggi,
- dan durasi query/transaksi sebenarnya sudah sehat.
Hindari membesarkan pool jika:
- slow query tinggi,
- lock wait dominan,
- atau database mulai kesulitan menangani banyak sesi paralel.
4. Tambahkan timeout yang jelas dan selaras
Gunakan timeout di beberapa lapisan:
- timeout ambil koneksi dari pool,
- timeout query,
- timeout lock wait bila tersedia di database,
- timeout eksekusi job,
- retry policy dengan backoff.
Tujuannya bukan sekadar membatalkan kerja lambat, tetapi mencegah worker diam terlalu lama tanpa sinyal. Timeout yang baik membuat masalah terlihat lebih cepat di log dan metrik.
Kesalahan umum: timeout job 30 detik, tetapi query bisa menunggu lebih lama; atau retry langsung tanpa jeda sehingga lock contention menjadi lebih parah.
5. Kurangi lock contention pada query tulis
Jika banyak worker memproses entitas yang sama, pertimbangkan:
- mengurutkan job per kunci bisnis tertentu,
- membuat operasi idempoten,
- menghindari update yang tidak perlu,
- menggunakan pola penguncian yang sesuai untuk antrian internal database,
- memecah batch besar menjadi unit lebih kecil.
Solusi terbaik bergantung pada model data dan konsistensi yang dibutuhkan. Jangan mengurangi locking jika itu mengorbankan integritas data tanpa pengganti yang aman.
Contoh Perbaikan Konfigurasi dan Instrumentasi
Nama opsi berbeda di tiap runtime dan library, tetapi pola konfigurasinya umumnya seperti ini:
worker:
concurrency: 12
job_timeout_ms: 45000
retry:
max_attempts: 5
backoff_ms: 2000
database_pool:
max_connections: 16
min_connections: 4
acquire_timeout_ms: 2000
idle_timeout_ms: 30000
observability:
log_pool_wait_over_ms: 200
log_query_over_ms: 500
trace_transactions: trueHal yang lebih penting daripada nama opsi adalah prinsipnya:
- worker concurrency disesuaikan dengan kapasitas DB nyata,
- acquire timeout tidak terlalu longgar,
- slow query threshold dicatat,
- transaksi dan pool wait dapat diukur.
Checklist Diagnosis: Worker Lambat karena Pool DB Terlihat Sehat
- Apakah queue depth naik saat CPU tetap rendah?
- Apakah durasi job meningkat walau error rate kecil?
- Apakah ada metrik pool wait time atau waktu acquire koneksi?
- Apakah koneksi dipegang terlalu lama per job?
- Apakah transaksi dibuka terlalu awal atau ditutup terlalu lambat?
- Apakah ada panggilan API eksternal di dalam transaksi?
- Apakah worker concurrency lebih besar daripada kemampuan DB menangani beban tulis paralel?
- Apakah slow query log menunjukkan statement yang sama berulang?
- Apakah ada lock wait, blocking session, atau transaksi idle yang belum commit?
- Apakah timeout pool, query, dan job saling konsisten?
- Apakah retry policy justru memperparah antrean saat bottleneck terjadi?
- Apakah setelah menurunkan concurrency throughput justru membaik?
Pencegahan agar Tidak Terulang
Instrumentasi sejak awal
Jangan tunggu insiden. Ukur minimal:
- waktu tunggu koneksi pool,
- durasi transaksi,
- slow query,
- queue age,
- jumlah worker aktif vs idle.
Desain job yang singkat dan sempit
Job yang baik untuk throughput tinggi biasanya:
- punya ruang lingkup sempit,
- idempoten,
- tidak menahan transaksi saat menunggu jaringan eksternal,
- dan meminimalkan query tulis yang saling berebut row yang sama.
Load test yang memodelkan contention, bukan hanya request rate
Banyak pengujian performa hanya mengecek API throughput. Untuk worker backend, Anda juga perlu menguji:
- banyak job paralel yang menulis ke entitas serupa,
- skenario lock contention,
- latency API eksternal yang lebih lambat dari normal,
- pool kecil dengan beban burst.
Tanpa skenario ini, sistem bisa lolos uji tetapi gagal di produksi dengan gejala “sehat tapi sedentary”.
Penutup
Saat melakukan debug backend: worker lambat karena pool DB terlihat sehat, jangan berhenti pada health check, CPU, atau jumlah koneksi aktif. Fokuskan investigasi pada aliran kerja nyata: antrean job, waktu tunggu koneksi, durasi transaksi, lock query, dan keselarasan timeout. Banyak sistem tidak gagal secara terang-terangan; mereka hanya kehilangan throughput sedikit demi sedikit sampai backlog meledak.
Jika Anda menemukan worker hidup tetapi tidak produktif, curigai bottleneck laten lebih dulu. Sistem bisa tampak sehat di permukaan, namun diam-diam kekurangan energi di lapisan connection pool dan transaksi. Di situlah debugging yang teliti biasanya menemukan akar masalah sebenarnya.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!