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:

GejalaIndikasi
Latency tinggip99 naik 10x dari baseline, request lambat
Error rate naikHTTP 5xx naik dari 0.1% ke 5%
OOMjava.lang.OutOfMemoryError: Java heap space
CPU 100%Tidak ada request masuk tetapi CPU jenuh
Memory leakHeap used terus naik walau traffic turun
Thread stuckRequest 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:

kotlin
// 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:

text
=== 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 log

Pada 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:

StatusArtiTindakan
RUNNABLESedang eksekusi atau siapNormal, periksa CPU usage
BLOCKEDMenunggu monitor lockIndikasi deadlock atau lock contention
WAITINGMenunggu notifikasi (Object.wait())Biasanya normal untuk consumer thread
TIMED_WAITINGMenunggu dengan timeout (Thread.sleep())Normal untuk scheduler
TERMINATEDSelesai eksekusiNormal di akhir aplikasi
NEWDibuat tapi belum startJarang 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:

kotlin Heap snapshot (simulasi)
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:

PesanPenyebabSolusi
OutOfMemoryError: Java heap spaceHeap tidak cukupTambah -Xmx, periksa memory leak
OutOfMemoryError: MetaspaceClass metadata penuhTambah -XX:MaxMetaspaceSize
StackOverflowErrorRekursi tak terbatasPeriksa kondisi terminasi, tambah -Xss
InterruptedExceptionThread di-interruptRestor status: Thread.currentThread().interrupt()
RejectedExecutionExceptionPool executor penuhNaikkan pool size atau pakai CallerRunsPolicy

Alat Diagnosis #

JVM menyediakan beberapa alat CLI untuk diagnosis:

text Alat diagnosis bawaan JDK
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-time

Untuk 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:

  1. Mitigasi — rollback atau scale-out untuk hentikan dampak. Tujuan: pengguna bisa pakai aplikasi lagi dalam 5 menit.
  2. Kumpulkan data — thread dump, heap dump, log, metrik. Bila pod direstart, data hilang.
  3. Reproduksi di staging — dengan data produksi (anonimized) agar bisa eksperimen tanpa risiko.
  4. Root cause analysis — gunakan teknik 5 Whys untuk menemukan akar masalah, bukan gejala.
  5. Perbaiki + tambahkan regression test — fix tidak lengkap tanpa test yang membuktikan bug sudah hilang.
  6. Postmortem dokumen — tulis dokumen postmortem (timeline, akar penyebab, action items). Postmortem bukan untuk mencari kesalahan, tetapi untuk pembelajaran tim.

Referensi Lanjutan #

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.