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:
| Konsumen | Pertanyaan | Tindakan saat jawaban buruk |
|---|---|---|
| Load balancer | server ini siap menerima trafik? | keluarkan dari rotasi |
| Orchestrator (Kubernetes) | proses ini hidup? siap? | restart pod / tahan trafik |
| Monitoring | aplikasi 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:
== 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:
enum Status: urutan deklarasi menetapkan skala keparahan —DOWNpaling parah,UNKNOWNpaling ringan. Enum di Java menyimpan urutan ini sebagaiordinal(), jadi agregasi tinggal membandingkan angka.agregasi(...): memilih indikator denganordinal()terkecil. Skenario kedua memperlihatkan aturannya:database DOWN+disk OUT_OF_SERVICE+cache UP→ agregatDOWN, karena tidak ada yang lebih parah dari komponen mati total.httpStatus(...): pemetaan dua nilai —UPmenjadi200, sisanya503. Load balancer umumnya hanya membaca kode status; badan JSON untuk manusia dan monitoring.json(...): laporan lengkap per komponen. Format dua lapis ini penting saat diagnosis: agregatDOWNsaja tidak memberi tahu komponen mana yang bermasalah.- Skenario kedua:
OUT_OF_SERVICEpada 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:
| Probe | Pertanyaan | Gagal berarti | Tindakan orchestrator |
|---|---|---|---|
| Liveness | apakah proses macet/mat total? | deadlock, kebocoran fatal | restart pod |
| Readiness | apakah siap menerima request? | dependency sibuk, pemanasan | tahan 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:
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -fsS http://localhost:8080/health/liveness || exit 1Nilai --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 #
- Kubernetes: Configure Liveness, Readiness and Startup Probes
- Docker: Dockerfile Reference — HEALTHCHECK
- Spring Boot: Actuator Endpoints
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PemeriksaKesehatan.java
Unduh berkasimport 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)");
}
}