Memahami gejala utama latensi API karena thread pool starvation

Latensi API yang mendadak melonjak sering kali bukan karena database atau jaringan, melainkan karena thread pool backend yang kelimpungan. Dalam kasus ini, masalah utama adalah thread pool starvation: pool worker penuh oleh task yang blocking, padahal antrean request terus datang. Untuk menyelesaikannya, pertama pahami bahwa respon lambat disebabkan threads tidak tersedia, bukan pipeline HTTP atau cache yang salah.

Jawaban langsung: pantau antrean request, metrik penggunaan thread pool, dan trace distribusi untuk memastikan semua request tidak stuck pada satu kelas task. Jika antrean panjang serta jumlah worker maksimal tercapai, artinya thread pool kehabisan tiket — penyebab utama latensi.

Studi kasus: API order processing jadi lambat secara tiba-tiba

Gejala awal

Tim observability menerima alert: p95 latency endpoint /api/v1/orders naik dari 150 ms menjadi 2 detik dalam 10 menit. Tidak ada deployment baru, tetapi throughput tetap tinggi. Log terakhir menunjukkan request baru masuk tetapi response terblokir selama detik.

Investigasi langkah demi langkah

  1. Profiling antrean request: dashboard load balancer memperlihatkan antrean di reverse proxy bertambah, menandakan backend tidak menghabiskan koneksi cepat.
  2. Metrik thread pool: metrik dari runtime (misalnya Prometheus dari thread_pool_active, thread_pool_queue_length) menunjukkan worker mencapai batas maksimum dan antrean memanjang.
  3. Trace distribusi: distributed tracing mengungkap request stuck pada method yang menunggu koneksi eksternal (misalnya payment gateway) tanpa timeout. Semua trace menunjukkan thread yang sama menunggu lebih dari 1 detik.

Log ilustratif:

2024-10-12T13:21:04.102Z INFO api-order Request received : orderId=12345
2024-10-12T13:21:04.746Z WARN payment-client Timeout while waiting for connection (threadPool=order-workers)
2024-10-12T13:21:04.747Z INFO api-order Thread pool queue depth=512 active=64 max=64

Root cause teridentifikasi: beberapa task menggunakan shared connection pool (contoh: koneksi HTTP ke payment gateway) yang blocking karena tidak ada penghematan reuse atau timeout. Sementara itu, thread pool order-workers hanya punya 64 worker. Penambahan task blocking membuat worker penuh, menyebabkan request berikutnya menunggu di queue (starvation) dan latensi membengkak.

Perbaikan konkret untuk menghentikan Starvation

1. Penyesuaian konfigurasi worker pool

Tambahkan konfigurasi scoped worker pool untuk request yang blocking. Contoh (pseudo-code):

ExecutorService orderWorkers = new ThreadPoolExecutor(
    64, 128,
    60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(256),
    new NamedThreadFactory("order-workers"));

Tambah resize pool untuk beban spike, tapi tetap batasi agar tidak menghabiskan CPU. Pakai queue bounded agar queue length tidak tak terkendali.

2. Batasi blocking task dengan timeout dan gaya async

  • Pastikan setiap call ke layanan eksternal punya timeout eksplisit. Contoh: HttpClient.newBuilder().connectTimeout(Duration.ofMillis(200)).
  • Jika memungkinkan, konversi ke async non-blocking (contoh: WebClient di Java Reactor) agar thread tidak menunggu I/O.

3. Load shedding dan circuit breaker

Ketika antrean mendekati batas, sistem bisa mengembalikan HTTP 503 yang eksplisit kepada klien upstream, atau memanfaatkan circuit breaker library untuk memutus permintaan ke layanan external yang bermasalah. Hal ini menghindari agregasi block di thread pool.

4. Connection reuse dan pooling yang sehat

Pastikan koneksi HTTP ke layanan eksternal tidak bocor. Opsional: gunakan connection pool yang membatasi total connection per host. Jika pool habis, request sebaiknya gagal cepat agar thread tidak menunggu lebih lama.

Verifikasi perbaikan dan observabilitas lanjutan

Dashboard metrik

Update dashboard untuk memantau indikator berikut:

  • Thread pool queue depth: harus turun setelah penyesuaian worker pool.
  • Active thread count vs max: idealnya tidak full saat latency stabil.
  • Timeout rate untuk layanan eksternal dan error rate dari circuit breaker.

Verifikasi manual dan automated

  1. Replay request stress test: kirim batch request berkecepatan tinggi pada environment staging dengan data yang sama untuk memastikan worker pool tidak full.
  2. Smoke test pasca deploy: jalankan API utama dan monitor (p95 latency, queue depth).

Jika semua metrik stabil, tandanya starvation telah ditangani.

Observabilitas dan rekomendasi pencegahan

Tambahkan trace untuk setiap request dan catat thread ID serta waktu tunggu I/O. Integrasi log dengan trace memudahkan pencocokan event. Gunakan alert berbasis antrean queue depth dan rate timeout agar tim segera bereaksi sebelum lintasan latensi meningkat.

Kesimpulannya, debugging latensi API akibat thread pool starvation memerlukan kombinasi metrik pool, trace blocking, serta penyesuaian worker pool dan timeout. Dengan observabilitas yang tepat dan strategi mitigation seperti circuit breaker, kita bisa menjaga throughput API tetap stabil.