Aplikasi produksi tidak pernah bebas masalah. Latency tiba-tiba naik, error rate melonjak, atau pod restart tanpa henti — semua skenario yang harus bisa kita diagnosis dengan cepat. JVM menyediakan beberapa alat bawaan untuk memeriksa kondisi runtime: jstack untuk thread dump, jmap untuk heap snapshot, dan jcmd untuk perintah diagnostik lainnya. Pada chapter ini kita akan mempraktikkan diagnosis dengan data simulasi, mengenali pesan kesalahan umum, dan menutup dengan strategi incident response.
Gejala Umum Masalah Produksi #
Sebelum masuk ke diagnosis, kita perlu mengenali gejala umum. Berikut daftar gejala paling sering di produksi Kotlin/JVM:
| Gejala | Indikasi |
|---|---|
| Latency tinggi | p99 naik 10x dari baseline, request lambat |
| Error rate naik | HTTP 5xx naik dari 0.1% ke 5% |
| OOM | java.lang.OutOfMemoryError: Java heap space |
| CPU 100% | Tidak ada request masuk tetapi CPU jenuh |
| Memory leak | Heap used terus naik walau traffic turun |
| Thread stuck | Request timeout, thread BLOCKED beruntun |
Thread Dump #
Thread dump adalah snapshot semua thread yang sedang berjalan di JVM, beserta status dan call stack. Thread dump membantu mendiagnosis deadlock, thread yang stuck, dan pola pemakaian thread pool:
// production_troubleshooting.kt
// Mendemonstrasikan konsep troubleshooting produksi:
// - Mengumpulkan thread dump
// - Menganalisis heap usage
// - Mendeteksi deadlock
// - Membaca pesan kesalahan umum
//
// Mensimulasikan diagnosis dengan data dummy yang deterministik.
import java.util.concurrent.locks.ReentrantLock
data class ThreadInfo(
val nama: String,
val status: String,
val stack: List<String>
)
data class HeapSnapshot(
val usedMB: Int,
val committedMB: Int,
val maxMB: Int,
val gcCount: Int
) {
fun penggunaanPersen(): Int = (usedMB * 100) / maxMB
}
class Diagnosis {
fun ambilThreadDump(): List<ThreadInfo> {
return listOf(
ThreadInfo("main", "RUNNABLE", listOf("App.main(App.kt:42)")),
ThreadInfo("pool-1-thread-1", "WAITING", listOf("LockSupport.park", "LinkedBlockingQueue.take")),
ThreadInfo("pool-1-thread-2", "TIMED_WAITING", listOf("Thread.sleep", "Worker.run")),
ThreadInfo("db-pool-1", "BLOCKED", listOf("ReentrantLock.lock", "DatabaseQuery.execute")),
ThreadInfo("kafka-consumer", "RUNNABLE", listOf("KafkaConsumer.poll", "Consumer.run"))
)
}
fun ambilHeapSnapshot(): HeapSnapshot =
HeapSnapshot(usedMB = 680, committedMB = 1024, maxMB = 2048, gcCount = 87)
fun deteksiDeadlock(threads: List<ThreadInfo>): List<String> {
return threads.filter { it.status == "BLOCKED" }.map { it.nama }
}
}
fun main() {
println("=== Production Troubleshooting ===")
println()
println("--- 1. Gejala umum masalah produksi ---")
val gejala = listOf(
Pair("Latency tinggi", "p99 naik 10x dari baseline"),
Pair("Error rate naik", "HTTP 5xx naik dari 0.1% ke 5%"),
Pair("OOM", "java.lang.OutOfMemoryError: Java heap space"),
Pair("CPU 100%", "Tidak ada request masuk tetapi CPU jenuh"),
Pair("Memory leak", "Heap used terus naik walau traffic turun"),
Pair("Thread stuck", "Request timeout, thread BLOCKED beruntun")
)
gejala.forEach { (g, d) ->
println(" $g.padEnd(20) -> $d")
}
println()
println("--- 2. Thread dump (simulasi) ---")
val diag = Diagnosis()
val dump = diag.ambilThreadDump()
dump.forEach { t ->
println(" \"${t.nama}\" status=${t.status}")
t.stack.take(2).forEach { println(" at $it") }
}
println()
println("--- 3. Heap snapshot ---")
val heap = diag.ambilHeapSnapshot()
println(" Used : ${heap.usedMB} MB")
println(" Committed : ${heap.committedMB} MB")
println(" Max : ${heap.maxMB} MB")
println(" GC count : ${heap.gcCount}")
println(" Penggunaan: ${heap.penggunaanPersen()}% (analisa bila > 80%)")
println()
println("--- 4. Deteksi deadlock ---")
val deadlocked = diag.deteksiDeadlock(dump)
if (deadlocked.isEmpty()) {
println(" Tidak ada thread BLOCKED (tidak ada indikasi deadlock)")
} else {
println(" Thread BLOCKED: $deadlocked")
println(" -> Periksa apakah dua thread saling menunggu lock")
}
println()
println("--- 5. Pesan kesalahan umum dan artinya ---")
val errors = listOf(
Triple("OutOfMemoryError: Java heap space",
"Heap tidak cukup",
"Tambah -Xmx, periksa memory leak"),
Triple("OutOfMemoryError: Metaspace",
"Class metadata penuh",
"Tambah -XX:MaxMetaspaceSize, periksa class loader leak"),
Triple("StackOverflowError",
"Stack terlalu dangkal",
"Periksa rekursi tak terbatas, tambah -Xss"),
Triple("InterruptedException",
"Thread di-interrupt",
"Restor interrupt status: Thread.currentThread().interrupt()"),
Triple("RejectedExecutionException",
"Pool executor penuh",
"Naikkan pool size atau pakai CallerRunsPolicy")
)
errors.forEach { (err, cause, fix) ->
println(" $err")
println(" Penyebab : $cause")
println(" Solusi : $fix")
}
println()
println("--- 6. Alat diagnosis ---")
val alat = listOf(
"jstack <pid> -> thread dump",
"jmap -heap <pid> -> heap summary",
"jcmd <pid> GC.heap_dump -> heap dump ke file",
"async-profiler -> CPU/allocation profiler",
"VisualVM -> GUI monitoring",
"JITWatch -> JIT compiler inspector",
"Micrometer + Prometheus -> metrik runtime"
)
alat.forEach { println(" $it") }
println()
println("--- 7. Strategi incident response ---")
val strategi = listOf(
"1. Mitigasi: rollback atau scale-out untuk hentikan dampak",
"2. Kumpulkan data: thread dump, heap dump, log",
"3. Reproduksi di staging dengan data produksi (anonymized)",
"4. Root cause analysis (5 Whys)",
"5. Perbaiki + tambahkan regression test",
"6. Postmortem dokumen + action items"
)
strategi.forEach { println(" $it") }
println()
println("--- 8. Logging yang membantu troubleshooting ---")
val logAturan = listOf(
"Setiap request -> trace ID (correlation)",
"Log level INFO untuk alur normal",
"Log level WARN untuk kondisi recoverable",
"Log level ERROR untuk kegagalan butuh insiden",
"Sertakan konteks: user_id, request_id, latency",
"Hindari PII (email, NIK, kartu kredit) di log"
)
logAturan.forEach { println(" $it") }
}Output yang Dihasilkan:
=== Production Troubleshooting ===
--- 1. Gejala umum masalah produksi ---
Latency tinggi.padEnd(20) -> p99 naik 10x dari baseline
Error rate naik.padEnd(20) -> HTTP 5xx naik dari 0.1% ke 5%
OOM.padEnd(20) -> java.lang.OutOfMemoryError: Java heap space
CPU 100%.padEnd(20) -> Tidak ada request masuk tetapi CPU jenuh
Memory leak.padEnd(20) -> Heap used terus naik walau traffic turun
Thread stuck.padEnd(20) -> Request timeout, thread BLOCKED beruntun
--- 2. Thread dump (simulasi) ---
"main" status=RUNNABLE
at App.main(App.kt:42)
"pool-1-thread-1" status=WAITING
at LockSupport.park
at LinkedBlockingQueue.take
"pool-1-thread-2" status=TIMED_WAITING
at Thread.sleep
at Worker.run
"db-pool-1" status=BLOCKED
at ReentrantLock.lock
at DatabaseQuery.execute
"kafka-consumer" status=RUNNABLE
at KafkaConsumer.poll
at Consumer.run
--- 3. Heap snapshot ---
Used : 680 MB
Committed : 1024 MB
Max : 2048 MB
GC count : 87
Penggunaan: 33% (analisa bila > 80%)
--- 4. Deteksi deadlock ---
Thread BLOCKED: [db-pool-1]
-> Periksa apakah dua thread saling menunggu lock
--- 5. Pesan kesalahan umum dan artinya ---
OutOfMemoryError: Java heap space
Penyebab : Heap tidak cukup
Solusi : Tambah -Xmx, periksa memory leak
OutOfMemoryError: Metaspace
Penyebab : Class metadata penuh
Solusi : Tambah -XX:MaxMetaspaceSize, periksa class loader leak
StackOverflowError
Penyebab : Stack terlalu dangkal
Solusi : Periksa rekursi tak terbatas, tambah -Xss
InterruptedException
Penyebab : Thread di-interrupt
Solusi : Restor interrupt status: Thread.currentThread().interrupt()
RejectedExecutionException
Penyebab : Pool executor penuh
Solusi : Naikkan pool size atau pakai CallerRunsPolicy
--- 6. Alat diagnosis ---
jstack <pid> -> thread dump
jmap -heap <pid> -> heap summary
jcmd <pid> GC.heap_dump -> heap dump ke file
async-profiler -> CPU/allocation profiler
VisualVM -> GUI monitoring
JITWatch -> JIT compiler inspector
Micrometer + Prometheus -> metrik runtime
--- 7. Strategi incident response ---
1. Mitigasi: rollback atau scale-out untuk hentikan dampak
2. Kumpulkan data: thread dump, heap dump, log
3. Reproduksi di staging dengan data produksi (anonymized)
4. Root cause analysis (5 Whys)
5. Perbaiki + tambahkan regression test
6. Postmortem dokumen + action items
--- 8. Logging yang membantu troubleshooting ---
Setiap request -> trace ID (correlation)
Log level INFO untuk alur normal
Log level WARN untuk kondisi recoverable
Log level ERROR untuk kegagalan butuh insiden
Sertakan konteks: user_id, request_id, latency
Hindari PII (email, NIK, kartu kredit) di logPada cuplikan di atas, Diagnosis.ambilThreadDump() mengembalikan daftar thread dengan status dan call stack. Thread db-pool-1 berstatus BLOCKED — indikasi kuat adanya deadlock atau resource contention di database layer. deteksiDeadlock memfilter semua thread BLOCKED untuk pemeriksaan lebih lanjut.
Status Thread di JVM #
Thread di JVM punya enam status utama yang perlu kita pahami:
| Status | Arti | Tindakan |
|---|---|---|
RUNNABLE | Sedang eksekusi atau siap | Normal, periksa CPU usage |
BLOCKED | Menunggu monitor lock | Indikasi deadlock atau lock contention |
WAITING | Menunggu notifikasi (Object.wait()) | Biasanya normal untuk consumer thread |
TIMED_WAITING | Menunggu dengan timeout (Thread.sleep()) | Normal untuk scheduler |
TERMINATED | Selesai eksekusi | Normal di akhir aplikasi |
NEW | Dibuat tapi belum start | Jarang muncul di dump |
Banyak thread BLOCKED adalah indikasi kuat deadlock. Banyak thread WAITING di thread pool juga wajar (idle thread), tetapi bila terus bertambah, mungkin thread leak.
Heap Snapshot #
Heap snapshot adalah snapshot memori JVM — berapa byte terpakai, berapa committed, dan berapa kali GC sudah berjalan:
data class HeapSnapshot(
val usedMB: Int,
val committedMB: Int,
val maxMB: Int,
val gcCount: Int
) {
fun penggunaanPersen(): Int = (usedMB * 100) / maxMB
}Ambil heap snapshot lewat jcmd <pid> GC.heap_info atau jmap -heap <pid>. Bila penggunaan heap > 80%, pertimbangkan naikkan -Xmx atau investigasi memory leak. Bila gcCount naik cepat dalam waktu singkat, ada indikasi pressure GC yang serius.
Pesan Kesalahan Umum #
Berikut pesan kesalahan runtime paling umum di Kotlin/JVM beserta penyebab dan solusi:
| Pesan | Penyebab | Solusi |
|---|---|---|
OutOfMemoryError: Java heap space | Heap tidak cukup | Tambah -Xmx, periksa memory leak |
OutOfMemoryError: Metaspace | Class metadata penuh | Tambah -XX:MaxMetaspaceSize |
StackOverflowError | Rekursi tak terbatas | Periksa kondisi terminasi, tambah -Xss |
InterruptedException | Thread di-interrupt | Restor status: Thread.currentThread().interrupt() |
RejectedExecutionException | Pool executor penuh | Naikkan pool size atau pakai CallerRunsPolicy |
Alat Diagnosis #
JVM menyediakan beberapa alat CLI untuk diagnosis:
jstack <pid> -> thread dump
jmap -heap <pid> -> heap summary
jcmd <pid> GC.heap_dump -> heap dump ke file
jcmd <pid> Thread.print -> thread dump (alternatif jstack)
jstat -gc <pid> 1s -> GC statistik real-timeUntuk profiling yang lebih dalam, gunakan async-profiler (low overhead, flame graph), VisualVM (GUI monitoring), atau Micrometer + Prometheus untuk metrik runtime berkelanjutan.
Strategi Incident Response #
Saat insiden terjadi, strategi yang baik adalah:
- Mitigasi — rollback atau scale-out untuk hentikan dampak. Tujuan: pengguna bisa pakai aplikasi lagi dalam 5 menit.
- Kumpulkan data — thread dump, heap dump, log, metrik. Bila pod direstart, data hilang.
- Reproduksi di staging — dengan data produksi (anonimized) agar bisa eksperimen tanpa risiko.
- Root cause analysis — gunakan teknik 5 Whys untuk menemukan akar masalah, bukan gejala.
- Perbaiki + tambahkan regression test — fix tidak lengkap tanpa test yang membuktikan bug sudah hilang.
- Postmortem dokumen — tulis dokumen postmortem (timeline, akar penyebab, action items). Postmortem bukan untuk mencari kesalahan, tetapi untuk pembelajaran tim.
Referensi Lanjutan #
- IntelliJ IDEA Debugging — panduan resmi IntelliJ IDEA untuk troubleshooting aplikasi produksi
Selanjutnya #
Setelah menguasai diagnosis masalah produksi, chapter terakhir section ini membahas release management — bagaimana kita merilis versi baru dengan aman, termasuk semantic versioning, changelog, dan strategi rollback.