Kode yang lolos seluruh pengujian tetap bisa berperilaku aneh di produksi: beban nyata, data nyata, dan interaksi antar komponen yang tidak pernah terjadi di mesin pengembang. Insiden produksi berbeda dari debugging lokal: debugger tidak selalu bisa ditempelkan ke proses yang sibuk melayani pengguna, dan sebagian bukti berumur pendek — isi heap dan status thread hilang begitu proses direstart. Chapter ini menyusun metode diagnosis sistematis: urutan penanganan insiden, alat JDK pada proses hidup, dan tabel keputusan gejala → bukti → tindakan.

Urutan Penanganan Insiden #

Naluri pertama saat insiden adalah me-restart agar layanan pulih cepat — tetapi restart juga menghapus bukti: heap yang hampir penuh, thread yang macet, dan antrean yang menumpuk tidak bisa diperiksa lagi setelah proses baru menyala. Urutan yang benar membaliknya: amankan bukti lebih dulu (thread dump, heap dump, rekaman JFR), barulah pulihkan layanan, lalu cari akar masalah dari bukti yang tersimpan.

Tabel Keputusan Diagnosis #

Insiden datang sebagai gejala; gejala yang sama bisa berakar pada masalah berbeda. Tabel keputusan menuntun langkah pertama — pilih bukti yang dikumpulkan sebelum melompat ke kesimpulan:

import java.util.List;

/** Tabel keputusan diagnosis produksi: gejala -> sumber bukti -> tindakan pertama. */
public class PetaDiagnosis {

    /** Satu baris tabel keputusan: gejala, indikator pendukung, bukti yang dikumpulkan, tindakan. */
    record Baris(String gejala, String indikator, String bukti, String tindakan) {}

    /** Tabel keputusan insiden — tiap baris adalah satu jalur diagnosis. */
    static final List<Baris> TABEL = List.of(
            new Baris("latensi p95 menanjak", "CPU rendah",
                    "thread dump (jcmd Thread.print)", "cari thread WAITING/BLOCKED dan pemilik kunci"),
            new Baris("latensi p95 menanjak", "CPU jenuh",
                    "rekaman JFR (jfr print)", "perbaiki hotspot teratas dari profil"),
            new Baris("OutOfMemoryError heap", "heap terus membesar",
                    "heap dump (jcmd GC.heap_dump)", "analisis retensi objek terbesar"),
            new Baris("container mati mendadak", "exit code 137",
                    "anggaran memori non-heap", "perbesar limit atau turunkan MaxRAMPercentage"),
            new Baris("aplikasi tidak menyala", "berhenti saat startup",
                    "log startup + tabel konfigurasi", "periksa environment variable wajib"),
            new Baris("koneksi database habis", "antrean pool menanjak",
                    "metrik pool + thread dump", "cari koneksi yang diambil tanpa ditutup"));

    /** Jalankan satu insiden lewat tabel: temukan baris yang cocok, cetak jalurnya. */
    static void diagnosis(int nomor, String gejala, String indikator) {
        System.out.println("Insiden " + nomor + ": " + gejala + " (" + indikator + ")");
        Baris cocok = TABEL.stream()
                .filter(b -> b.gejala().equals(gejala) && b.indikator().equals(indikator))
                .findFirst()
                .orElseThrow();
        System.out.println("  bukti   : kumpulkan " + cocok.bukti());
        System.out.println("  tindakan: " + cocok.tindakan());
        System.out.println();
    }

    public static void main(String[] args) {
        System.out.println("== Tabel keputusan diagnosis ==");
        System.out.printf("%-24s %-22s %-32s %s%n",
                "gejala", "indikator", "bukti yang dikumpulkan", "tindakan pertama");
        for (Baris b : TABEL) {
            System.out.printf("%-24s %-22s %-32s %s%n",
                    b.gejala(), b.indikator(), b.bukti(), b.tindakan());
        }
        System.out.println();

        System.out.println("== Tiga insiden melewati tabel ==");
        diagnosis(1, "latensi p95 menanjak", "CPU rendah");
        diagnosis(2, "container mati mendadak", "exit code 137");
        diagnosis(3, "koneksi database habis", "antrean pool menanjak");

        System.out.println("== Urutan penanganan insiden ==");
        String[] langkah = {
                "1. amankan bukti: thread dump, heap dump, JFR, sebelum restart apa pun",
                "2. pulihkan layanan: rollback atau scale-out dengan data yang ada",
                "3. cari akar masalah lewat tabel keputusan di atas",
                "4. tulis postmortem blameless (tanpa mencari kesalahan individu) beserta pencegahannya"
        };
        for (String l : langkah) {
            System.out.println("  " + l);
        }
    }
}

Output yang Dihasilkan:

text
== Tabel keputusan diagnosis ==
gejala                   indikator              bukti yang dikumpulkan           tindakan pertama
latensi p95 menanjak     CPU rendah             thread dump (jcmd Thread.print)  cari thread WAITING/BLOCKED dan pemilik kunci
latensi p95 menanjak     CPU jenuh              rekaman JFR (jfr print)          perbaiki hotspot teratas dari profil
OutOfMemoryError heap    heap terus membesar    heap dump (jcmd GC.heap_dump)    analisis retensi objek terbesar
container mati mendadak  exit code 137          anggaran memori non-heap         perbesar limit atau turunkan MaxRAMPercentage
aplikasi tidak menyala   berhenti saat startup  log startup + tabel konfigurasi  periksa environment variable wajib
koneksi database habis   antrean pool menanjak  metrik pool + thread dump        cari koneksi yang diambil tanpa ditutup

== Tiga insiden melewati tabel ==
Insiden 1: latensi p95 menanjak (CPU rendah)
  bukti   : kumpulkan thread dump (jcmd Thread.print)
  tindakan: cari thread WAITING/BLOCKED dan pemilik kunci

Insiden 2: container mati mendadak (exit code 137)
  bukti   : kumpulkan anggaran memori non-heap
  tindakan: perbesar limit atau turunkan MaxRAMPercentage

Insiden 3: koneksi database habis (antrean pool menanjak)
  bukti   : kumpulkan metrik pool + thread dump
  tindakan: cari koneksi yang diambil tanpa ditutup

== Urutan penanganan insiden ==
  1. amankan bukti: thread dump, heap dump, JFR, sebelum restart apa pun
  2. pulihkan layanan: rollback atau scale-out dengan data yang ada
  3. cari akar masalah lewat tabel keputusan di atas
  4. tulis postmortem blameless (tanpa mencari kesalahan individu) beserta pencegahannya

Mari kita bedah struktur tabelnya:

  1. record Baris(gejala, indikator, bukti, tindakan): satu baris tabel keputusan sebagai data — gejala dan indikator kunci pencarian, bukti dan tindakan hasilnya. Tabel sebagai data (bukan if-else berantai) bisa direview dan ditambah tanpa menyentuh logika.
  2. Dua baris “latensi p95 menanjak”: gejala sama, indikator berbeda, bukti berbeda — CPU rendah berarti thread menunggu, CPU jenuh berarti komputasi. Indikator memangkas ruang dugaan; diagnosis(...) mencari baris dengan pasangan kunci yang cocok.
  3. Baris exit code 137: akar masalahnya di luar JVM — kernel menghentikan container karena batas cgroup terlampaui (anggaran non-heap di chapter Containerizing Aplikasi Java). Memeriksa heap tidak menolong bila yang habis justru komponen non-heap.
  4. Urutan empat langkah: bukti → pulih → akar masalah → postmortem. Postmortem blameless mencatat kejadian beserta pencegahannya, bukan siapa yang keliru — tim yang takut disalahkan menyembunyikan kejadian berikutnya.

Alat Diagnosis pada Proses Hidup #

jcmd adalah pintu utama: satu alat yang mengirim perintah diagnosis ke proses Java mana pun di mesin yang sama, tanpa agent tambahan. Tanpa argumen (atau -l), jcmd mencantumkan proses Java yang berjalan beserta PID-nya. Sesi berikut menargetkan ServerTidur:

bash
jcmd -l

Output yang Dihasilkan:

text
8837 jdk.jcmd/sun.tools.jcmd.JCmd -l
8814 ServerTidur

Baris kedua adalah target kita — PID 8814. Saat insiden latensi, perintah yang paling sering dipakai adalah Thread.print; potongan berikut menampilkan blok thread utama:

bash
jcmd 8814 Thread.print

Output yang Dihasilkan (potongan thread main):

text
8814:
2026-10-16 03:09:22
"main" #3 [8815] prio=5 os_prio=0 cpu=21.79ms elapsed=2.38s tid=0x00007f488002acb0 nid=8815 waiting on condition  [0x00007f4885302000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleepNanos0([email protected]/Native Method)
        at java.lang.Thread.sleepNanos([email protected]/Thread.java:509)
        at java.lang.Thread.sleep([email protected]/Thread.java:540)

PID, tid/nid, cpu, dan elapsed berbeda tiap proses — yang stabil dan diagnostik adalah nama thread, Thread.State, dan isi stack. Thread main di contoh berstatus TIMED_WAITING (sleeping) karena memang tidur; pada aplikasi sakit, cari thread WAITING/BLOCKED yang lama lalu baca stack-nya. Alat pendampingnya:

PerintahBukti yang dihasilkanPertanyaan yang dijawab
jcmd <pid> Thread.printthread dump seluruh threadthread apa yang menunggu atau macet?
jcmd <pid> GC.heap_inforingkasan heapseberapa penuh heap?
jcmd <pid> GC.heap_dumpberkas .hprof snapshot heapobjek apa yang memakan memori?
jcmd <pid> VM.flagsseluruh flag JVM aktifkonfigurasi runtime mana yang berlaku?
jfr print / jfr summaryevent dari rekaman .jfrdi mana waktu prosesor dibelanjakan?

Jebakan Umum #

Tiga kekeliruan klasik: menyamakan OOMKill dengan OutOfMemoryError (exit code 137 berarti kernel menghentikan container karena batas cgroup — heap JVM bisa jadi masih sehat; yang bocor justru metaspace, thread stack, atau direct buffer); klaim “berhasil di laptop, gagal di produksi” yang biasanya selesai pada tabel konfigurasi startup (chapter Externalized Configuration) — bandingkan baris demi baris; dan PID yang diperiksa ternyata sisa pod lama, bukan proses bermasalah.

Diagnosis yang tertata memberi akar masalah — dan perbaikannya harus menyeberang ke produksi dengan aman. Chapter penutup section ini mengatur penyeberangan itu: versi rilis yang berdisiplin, changelog yang bisa diaudit, dan strategi rollout yang selalu punya jalur mundur.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

PetaDiagnosis.java

Unduh berkas
java
import java.util.List;

/** Tabel keputusan diagnosis produksi: gejala -> sumber bukti -> tindakan pertama. */
public class PetaDiagnosis {

    /** Satu baris tabel keputusan: gejala, indikator pendukung, bukti yang dikumpulkan, tindakan. */
    record Baris(String gejala, String indikator, String bukti, String tindakan) {}

    /** Tabel keputusan insiden — tiap baris adalah satu jalur diagnosis. */
    static final List<Baris> TABEL = List.of(
            new Baris("latensi p95 menanjak", "CPU rendah",
                    "thread dump (jcmd Thread.print)", "cari thread WAITING/BLOCKED dan pemilik kunci"),
            new Baris("latensi p95 menanjak", "CPU jenuh",
                    "rekaman JFR (jfr print)", "perbaiki hotspot teratas dari profil"),
            new Baris("OutOfMemoryError heap", "heap terus membesar",
                    "heap dump (jcmd GC.heap_dump)", "analisis retensi objek terbesar"),
            new Baris("container mati mendadak", "exit code 137",
                    "anggaran memori non-heap", "perbesar limit atau turunkan MaxRAMPercentage"),
            new Baris("aplikasi tidak menyala", "berhenti saat startup",
                    "log startup + tabel konfigurasi", "periksa environment variable wajib"),
            new Baris("koneksi database habis", "antrean pool menanjak",
                    "metrik pool + thread dump", "cari koneksi yang diambil tanpa ditutup"));

    /** Jalankan satu insiden lewat tabel: temukan baris yang cocok, cetak jalurnya. */
    static void diagnosis(int nomor, String gejala, String indikator) {
        System.out.println("Insiden " + nomor + ": " + gejala + " (" + indikator + ")");
        Baris cocok = TABEL.stream()
                .filter(b -> b.gejala().equals(gejala) && b.indikator().equals(indikator))
                .findFirst()
                .orElseThrow();
        System.out.println("  bukti   : kumpulkan " + cocok.bukti());
        System.out.println("  tindakan: " + cocok.tindakan());
        System.out.println();
    }

    public static void main(String[] args) {
        System.out.println("== Tabel keputusan diagnosis ==");
        System.out.printf("%-24s %-22s %-32s %s%n",
                "gejala", "indikator", "bukti yang dikumpulkan", "tindakan pertama");
        for (Baris b : TABEL) {
            System.out.printf("%-24s %-22s %-32s %s%n",
                    b.gejala(), b.indikator(), b.bukti(), b.tindakan());
        }
        System.out.println();

        System.out.println("== Tiga insiden melewati tabel ==");
        diagnosis(1, "latensi p95 menanjak", "CPU rendah");
        diagnosis(2, "container mati mendadak", "exit code 137");
        diagnosis(3, "koneksi database habis", "antrean pool menanjak");

        System.out.println("== Urutan penanganan insiden ==");
        String[] langkah = {
                "1. amankan bukti: thread dump, heap dump, JFR, sebelum restart apa pun",
                "2. pulihkan layanan: rollback atau scale-out dengan data yang ada",
                "3. cari akar masalah lewat tabel keputusan di atas",
                "4. tulis postmortem blameless (tanpa mencari kesalahan individu) beserta pencegahannya"
        };
        for (String l : langkah) {
            System.out.println("  " + l);
        }
    }
}