Log dari chapter sebelumnya menceritakan masa lalu; health check menjawab detik ini. Pertanyaannya sederhana — “apakah aplikasi masih mampu melayani request?” — tetapi penanya bukan manusia melainkan mesin: load balancer yang memutuskan ke server mana trafik diarahkan, dan orchestrator yang memutuskan restart atau tidak. Jawabannya harus tersedia sebagai endpoint HTTP yang murah dipanggil, cepat menjawab, dan jujur soal kondisi. Kesalahan desain health check punya konsekuensi nyata: endpoint yang terlalu optimistis membuat trafik terus mengalir ke aplikasi sekarat, sedangkan endpoint yang terlalu pesimistis memicu gelombang restart yang justru memperburuk keadaan.

Endpoint Kesehatan dan Penggunanya #

Endpoint kesehatan standar, biasanya GET /health, menjawab kode status HTTP: 200 berarti sehat, 503 berarti tidak layak menerima trafik. Tiga konsumen utamanya mengajukan pertanyaan berbeda:

KonsumenPertanyaanTindakan saat jawaban buruk
Load balancerserver ini siap menerima trafik?keluarkan dari rotasi
Orchestrator (Kubernetes)proses ini hidup? siap?restart pod / tahan trafik
Monitoringaplikasi mana yang bermasalah?kirim peringatan ke tim

Karena konsumen berbeda kebutuhan, satu endpoint sering dipecah dua — kita bahas setelah agregasinya paham.

Agregasi Status Terburuk #

Aplikasi jarang punya satu kesehatan tunggal: database, cache, dan ruang disk masing-masing punya kondisi sendiri. Praktik umum: daftarkan health indicator per komponen, lalu agregasikan dengan aturan worst-of — status terburuk menang. Program berikut merakit registri lengkapnya:

import java.util.List;
import java.util.Map;

/** Registri health check: agregasi status terburuk dan pemisahan liveness/readiness. */
public class PemeriksaKesehatan {

    /** Urutan deklarasi = urutan keparahan (worst-of memilih yang paling depan). */
    enum Status { DOWN, OUT_OF_SERVICE, UP, UNKNOWN }

    /** Satu pemeriksaan kesehatan: nama komponen, status, dan keterangan. */
    record Indikator(String nama, Status status, String keterangan) {}

    /** Pilih status paling parah dari kumpulan indikator. */
    static Status agregasi(List<Indikator> indikator) {
        Status terburuk = Status.UP;
        for (Indikator i : indikator) {
            if (i.status().ordinal() < terburuk.ordinal()) {
                terburuk = i.status();
            }
        }
        return terburuk;
    }

    /** Kode HTTP untuk laporan kesehatan keseluruhan. */
    static int httpStatus(Status s) {
        return (s == Status.UP) ? 200 : 503;
    }

    static String json(List<Indikator> indikator) {
        StringBuilder sb = new StringBuilder();
        sb.append("{\"status\":\"").append(agregasi(indikator)).append("\",\"indikator\":{");
        for (int i = 0; i < indikator.size(); i++) {
            Indikator in = indikator.get(i);
            if (i > 0) {
                sb.append(",");
            }
            sb.append("\"").append(in.nama()).append("\":\"").append(in.status()).append("\"");
        }
        sb.append("}}");
        return sb.toString();
    }

    static void laporan(String judul, List<Indikator> indikator) {
        System.out.println("== " + judul + " ==");
        for (Indikator i : indikator) {
            System.out.printf("  %-10s %-15s %s%n", i.nama(), i.status(), i.keterangan());
        }
        Status total = agregasi(indikator);
        System.out.println("  agregat    " + total + "  -> GET /health " + httpStatus(total)
                + " " + json(indikator));
        System.out.println();
    }

    public static void main(String[] args) {
        laporan("Skenario normal: seluruh komponen siap", List.of(
                new Indikator("database", Status.UP, "koneksi 20/20"),
                new Indikator("cache", Status.UP, "hit-ratio 96%"),
                new Indikator("disk", Status.UP, "sisa 41 GB")));

        laporan("Skenario database terganggu", List.of(
                new Indikator("database", Status.DOWN, "koneksi timeout"),
                new Indikator("cache", Status.UP, "hit-ratio 96%"),
                new Indikator("disk", Status.OUT_OF_SERVICE, "sisa 2 GB")));

        System.out.println("== Liveness vs readiness saat database terganggu ==");
        System.out.println("  GET /health/liveness   -> 200 {\"status\":\"UP\"}"
                + "   (proses masih hidup, jangan restart)");
        System.out.println("  GET /health/readiness  -> 503 {\"status\":\"DOWN\"}"
                + "   (tidak siap terima request, keluarkan dari rotasi)");
    }
}

Output yang Dihasilkan:

text
== Skenario normal: seluruh komponen siap ==
  database   UP              koneksi 20/20
  cache      UP              hit-ratio 96%
  disk       UP              sisa 41 GB
  agregat    UP  -> GET /health 200 {"status":"UP","indikator":{"database":"UP","cache":"UP","disk":"UP"}}

== Skenario database terganggu ==
  database   DOWN            koneksi timeout
  cache      UP              hit-ratio 96%
  disk       OUT_OF_SERVICE  sisa 2 GB
  agregat    DOWN  -> GET /health 503 {"status":"DOWN","indikator":{"database":"DOWN","cache":"UP","disk":"OUT_OF_SERVICE"}}

== Liveness vs readiness saat database terganggu ==
  GET /health/liveness   -> 200 {"status":"UP"}   (proses masih hidup, jangan restart)
  GET /health/readiness  -> 503 {"status":"DOWN"}   (tidak siap terima request, keluarkan dari rotasi)

Mari kita bedah mesinnya:

  1. enum Status: urutan deklarasi menetapkan skala keparahan — DOWN paling parah, UNKNOWN paling ringan. Enum di Java menyimpan urutan ini sebagai ordinal(), jadi agregasi tinggal membandingkan angka.
  2. agregasi(...): memilih indikator dengan ordinal() terkecil. Skenario kedua memperlihatkan aturannya: database DOWN + disk OUT_OF_SERVICE + cache UP → agregat DOWN, karena tidak ada yang lebih parah dari komponen mati total.
  3. httpStatus(...): pemetaan dua nilai — UP menjadi 200, sisanya 503. Load balancer umumnya hanya membaca kode status; badan JSON untuk manusia dan monitoring.
  4. json(...): laporan lengkap per komponen. Format dua lapis ini penting saat diagnosis: agregat DOWN saja tidak memberi tahu komponen mana yang bermasalah.
  5. Skenario kedua: OUT_OF_SERVICE pada disk dengan keterangan “sisa 2 GB” — contoh status abu-abu: komponen hidup tetapi di ambang; tidak mematikan aplikasi, namun layak jadi peringatan dini.

Liveness vs Readiness #

Kubernetes memetakan dua pertanyaan menjadi dua probe yang berbeda nasib:

ProbePertanyaanGagal berartiTindakan orchestrator
Livenessapakah proses macet/mat total?deadlock, kebocoran fatalrestart pod
Readinessapakah siap menerima request?dependency sibuk, pemanasantahan trafik, tanpa restart

Bagian akhir program menunjukkan pemisahan yang benar saat database terganggu: liveness tetap 200 karena proses Java sendiri masih sehat — me-restart tidak akan menyalakan ulang database orang lain — sedangkan readiness 503 supaya load balancer mengalihkan trafik sementara.

HEALTHCHECK di Docker #

Dockerfile memiliki instruksinya sendiri untuk container yang berjalan tanpa orchestrator:

dockerfile Instruksi HEALTHCHECK pada Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
  CMD curl -fsS http://localhost:8080/health/liveness || exit 1

Nilai --start-period=40s memberi aplikasi Java waktu pemanasan sebelum kegagalan dihitung, dan kode exit 1 menandai container unhealthy — status yang terbaca lewat docker ps dan bisa dipakai skrip operasi. Spring Boot menyediakan endpoint siap pakai untuk semua konsep ini lewat Spring Actuator (/actuator/health/liveness dan /actuator/health/readiness).

Health check menjawab “sehat atau tidak” dengan ya/tidak. Tetapi pertanyaan operasional yang lebih kaya — berapa latensi sekarang, berapa request per detik, seberapa dalam antrean — membutuhkan angka yang berubah sepanjang waktu. Chapter berikutnya membahas metrik dan monitoring.

Referensi #

Berkas Sumber

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

PemeriksaKesehatan.java

Unduh berkas
java
import java.util.List;
import java.util.Map;

/** Registri health check: agregasi status terburuk dan pemisahan liveness/readiness. */
public class PemeriksaKesehatan {

    /** Urutan deklarasi = urutan keparahan (worst-of memilih yang paling depan). */
    enum Status { DOWN, OUT_OF_SERVICE, UP, UNKNOWN }

    /** Satu pemeriksaan kesehatan: nama komponen, status, dan keterangan. */
    record Indikator(String nama, Status status, String keterangan) {}

    /** Pilih status paling parah dari kumpulan indikator. */
    static Status agregasi(List<Indikator> indikator) {
        Status terburuk = Status.UP;
        for (Indikator i : indikator) {
            if (i.status().ordinal() < terburuk.ordinal()) {
                terburuk = i.status();
            }
        }
        return terburuk;
    }

    /** Kode HTTP untuk laporan kesehatan keseluruhan. */
    static int httpStatus(Status s) {
        return (s == Status.UP) ? 200 : 503;
    }

    static String json(List<Indikator> indikator) {
        StringBuilder sb = new StringBuilder();
        sb.append("{\"status\":\"").append(agregasi(indikator)).append("\",\"indikator\":{");
        for (int i = 0; i < indikator.size(); i++) {
            Indikator in = indikator.get(i);
            if (i > 0) {
                sb.append(",");
            }
            sb.append("\"").append(in.nama()).append("\":\"").append(in.status()).append("\"");
        }
        sb.append("}}");
        return sb.toString();
    }

    static void laporan(String judul, List<Indikator> indikator) {
        System.out.println("== " + judul + " ==");
        for (Indikator i : indikator) {
            System.out.printf("  %-10s %-15s %s%n", i.nama(), i.status(), i.keterangan());
        }
        Status total = agregasi(indikator);
        System.out.println("  agregat    " + total + "  -> GET /health " + httpStatus(total)
                + " " + json(indikator));
        System.out.println();
    }

    public static void main(String[] args) {
        laporan("Skenario normal: seluruh komponen siap", List.of(
                new Indikator("database", Status.UP, "koneksi 20/20"),
                new Indikator("cache", Status.UP, "hit-ratio 96%"),
                new Indikator("disk", Status.UP, "sisa 41 GB")));

        laporan("Skenario database terganggu", List.of(
                new Indikator("database", Status.DOWN, "koneksi timeout"),
                new Indikator("cache", Status.UP, "hit-ratio 96%"),
                new Indikator("disk", Status.OUT_OF_SERVICE, "sisa 2 GB")));

        System.out.println("== Liveness vs readiness saat database terganggu ==");
        System.out.println("  GET /health/liveness   -> 200 {\"status\":\"UP\"}"
                + "   (proses masih hidup, jangan restart)");
        System.out.println("  GET /health/readiness  -> 503 {\"status\":\"DOWN\"}"
                + "   (tidak siap terima request, keluarkan dari rotasi)");
    }
}