Studi Kasus Debugging Spring Boot: Deadlock ThreadPool Logging menjelaskan langkah konkret ketika logging asynchronous yang tinggi menyebabkan aplikasi terhenti. Dalam dua paragraf pertama ini, masalah utama dijelaskan: thread pool worker saling menunggu karena sink logging tetap menunggu I/O yang terblokir, sehingga throughput API langsung anjlok.

Solusi dimulai dari observasi konsisten, menentukan root cause blok I/O di callback logging, lalu menerapkan refaktor asinkron yang lebih aman dan pembatasan pool. Artikel berikut merinci setiap langkah agar developer bisa langsung menindaklanjuti.

Gejala: Deadlock ThreadPool Saat Logging Asynchronous Melonjak

Gejala awal yang muncul adalah endpoint yang semula responsif tiba-tiba menjadi timeout atau queue request bertumpuk. Saat traffic logging meningkat—misalnya ketika banyak event audit ditulis ke sink eksternal karena permintaan API tertentu—profil CPU rendah tetapi request tetap menunggu. Thread dump memperlihatkan worker ThreadPoolTaskExecutor pada Spring Boot menunggu pada blok Appender yang sama, sementara tidak ada thread baru yang dijalankan.

Logging asynchronous tidak segera terlihat sebagai penyebab; tetapi ketika flag debug dinyalakan dan sink logging mengalami latensi, seluruh pool menjadi tidak bergerak. Kondisi ini menggambarkan deadlock karena sink log menahan resource yang juga diperlukan oleh worker lain yang tengah mengejar log yang sama.

Langkah Observasi yang Sistematis

1. Thread Dump dan Stack Trace

Ambil thread dump menggunakan jcmd <pid> Thread.print atau jstack ketika metrik latensi naik. Fokus pada threads dari pool yang sering muncul dalam state WAITING atau BLOCKED dan periksa stack trace terakhir mereka; pola umum adalah menunggu di ch.qos.logback.core.AsyncAppenderBase atau sink logging eksternal.

2. Metrik ThreadPool dan Queue

Gunakan actuator /metrics untuk mencatat task.executor.pool.size, task.executor.active, dan length queue. Lonjakan queue bersamaan dengan penurunan throughput logging menunjukkan backlog pada worker. Catat pula latency I/O dari sink logging (misalnya via log aggregator atau tool observability) agar sink eksternal tidak overlooked.

3. Trace Asynchronous Logging

Gunakan tracing (Spring Sleuth atau OpenTelemetry) untuk melacak lifecycle request hingga bagian logging. Jika trace menunjukkan worker menghabiskan waktu di LoggingEvent sebelum response dikirim, berarti deadlock sudah terjadi di level logging. Ini membantu memastikan bahwa bukan isu database atau service lain.

Root Cause: Callback Logging Memblokir Resource

Root cause utama adalah callback logging yang tetap memblokir I/O (misalnya sink HTTP/DB) dalam thread satu per satu, sementara pipeline logging asynchronous tidak dikonfigurasi untuk memutus siklus blocking tersebut. Contoh umum: AsyncAppender menulis ke sink yang melakukan blocking send ke HTTP atau database. Ketika sink berhenti merespons, buffer AsyncAppender penuh, lalu thread task executor yang memproduksi log terhenti menunggu untuk mengirim berikutnya.

Salah satu indikator tambahan adalah MDC yang dibawa ke callback logging tetapi tidak di-reset saat log diproses ulang. Jika sink logging menggunakan MDC untuk contextual data dan sink blocking menyebabkan worker menunggu, MDC tetap dikunci sehingga thread lain tidak bisa mengambil context yang diperlukan.

Perbaikan Praktis

Refactor Logging Asynchronous agar Non-blocking

Ganti sink logging blocking dengan implementasi yang menulis ke buffer internal terlebih dahulu, kemudian diproses oleh thread terpisah. Contoh konfigurasi Logback:

<appender name="ASYNC_SOCKET" class="ch.qos.logback.classic.AsyncAppender" includeCallerData="false" queueSize="512" discardingThreshold="0" maxFlushTime="1000" >
  <appender-ref ref="SOCKET" />
</appender>

<appender name="SOCKET" class="ch.qos.logback.classic.net.SocketAppender" />

Pada contoh ini, queueSize dan maxFlushTime harus disesuaikan dengan karakteristik latency sink. Jika sink tetap blocking, pertimbangkan menggunakan buffer in-memory dan thread dedicated agar worker utama tidak terblok.

Batasi Ukuran ThreadPool dan Batasi Flood Logging

Ukuran ThreadPoolTaskExecutor sebaiknya tidak dipori tanpa batas. Atur corePoolSize, maxPoolSize, dan queueCapacity agar tidak menyebabkan saturasi. Misalnya:

@Bean
public TaskExecutor taskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(50);
    executor.setMaxPoolSize(100);
    executor.setQueueCapacity(200);
    executor.setThreadNamePrefix("api-worker-");
    return executor;
}

Jika logging asynchronous mengirim event ke sink, pastikan ada mekanisme backpressure alias log-drop/perputaran agar flood tidak memperluas queue dan menahan resource lainnya.

Atur MDC dengan Clear di Callback

Masalah MDC muncul ketika callback logging tetap membawa context yang menunggu lock. Tambahkan MDC.clear() di bagian callback ketika sink asinkron selesai memproses event. Contoh:

public void append(LogEvent event) {
    try {
        // processing...
    } finally {
        MDC.clear();
    }
}

Ini memastikan thread pool tidak menahan MDC yang membuat logging lain tidak bisa melanjutkan.

Nonaktifkan Sink yang Blocking

Jika sink tertentu terus bermasalah, gunakan mekanisme fallback dengan menonaktifkan sink tersebut saat latency terlalu tinggi. Bisa dengan fitur Logback failover atau toggling appender lewat config dynamic (misalnya file config yang reloadable). Ini membantu menghentikan deadlock saat sink tidak stabil.

Verifikasi dan Mitigasi Jangka Panjang

Verifikasi Perbaikan

Jalankan script sederhana untuk menghasilkan beban logging lalu kumpulkan thread dump setelah perbaikan, misalnya:

#!/bin/bash
for i in {1..5}; do
  curl http://localhost:8080/api/log-heavy
  sleep 1
done
jstack <PID> | grep -n "AsyncAppender"

Keluarkan hasilnya ke file dan pastikan tidak ada thread dalam state BLOCKED karena append logging. Bandingkan metrik pool sebelum dan sesudah perbaikan.

Mitigasi Jangka Panjang

  • Pasang monitoring latency append target—gunakan histogram atau percentiles untuk melihat pertambahan latensi sink.
  • Automasi alert saat queue AsyncAppender mendekati kapasitas maksimum.
  • Documentasikan konfigurasi logging dan peringatan terkait MDC agar tim lain tidak menambahkan callback blocking tanpa evaluasi.
  • Gunakan chaos testing pada sink logging jika memungkinkan, agar deadlock dapat direproduksi dan di-prevent.

Catatan Tambahan

Debugging deadlock ThreadPool logging memerlukan kombinasi observasi sistem, analisis stack trace, dan perbaikan arsitektural. Fokus utama adalah memisahkan lapisan logging dari thread utama agar I/O tidak menahan pool worker.

Dengan pendekatan di atas, Spring Boot dapat kembali stabil meskipun logging asynchronous meningkat.