Orkestrator seperti Kubernetes mengawasi tiap container lewat dua pertanyaan yang berbeda: apakah prosesnya masih hidup, dan apakah layanannya siap menerima traffic? Kedua jawaban itu tidak boleh dibuang menjadi satu endpoint — kegagalan database seharusnya menghentikan arus request tanpa memicu restart, sedangkan proses yang macat total justru perlu direstart. Pada chapter penutup section ini kita memisahkan keduanya menjadi /healthz dan /readyz, mengujinya lewat httptest, lalu merangkai konfigurasi probe Kubernetes untuk kedua endpoint tersebut.
Tiga Jenis Probe #
| Probe | Pertanyaan | Aksi orkestrator saat gagal | Endpoint lazim |
|---|---|---|---|
| Liveness | apakah proses hidup? | restart container | /healthz |
| Readiness | siap menerima traffic? | keluarkan dari daftar pod penerima load | /readyz |
| Startup | sudah selesai menyala? | tunda kedua probe lain sampai sukses | /healthz atau khusus |
Konsekuensi kolom ketiga adalah inti desain ini. Readiness gagal membuat pod dihapus sementara dari Service — traffic dialihkan ke replika lain tanpa restart. Liveness gagal membuat container dibunuh lalu dibuat ulang — cocok untuk deadlock atau kebocoran memori yang hanya bisa dipulihkan dengan restart. Startup probe menutup celah khusus aplikasi lambat menyala: selama inisialisasi, liveness belum dijalankan sehingga proses yang butuh waktu dua menit untuk memuat cache tidak dibunuh duluan.
Liveness Harus Ringan #
Endpoint liveness harus menjawab dalam hitungan milidetik tanpa menyentuh dependency eksternal. Mengecek database di /healthz adalah jebakan klasik: saat database sempat lambat, seluruh pod melaporkan dirinya mati, Kubernetes me-restart semuanya, dan gelombang restart membuat database makin kewalahan — badai restart yang tidak menyelesaikan apa pun. Pemeriksaan dependency adalah tugas readiness; liveness cukup membuktikan event loop masih berjalan.
Program Demo #
Berkas health-demo.go membangun dua handler terpisah dengan StatusDB sebagai dependency tiruan. Skenario pertama berjalan saat database merespons, skenario kedua saat database mati — dan /healthz tetap mengembalikan 200 di keduanya.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"sync"
)
// StatusDB menirukan koneksi database: bisa hidup atau mati.
type StatusDB struct {
mu sync.Mutex
siap bool
}
// Ping melaporkan apakah database saat ini merespons.
func (d *StatusDB) Ping() bool {
d.mu.Lock()
defer d.mu.Unlock()
return d.siap
}
// SetSiap mengubah kondisi database untuk keperluan skenario demo.
func (d *StatusDB) SetSiap(v bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.siap = v
}
// responsSiap adalah bentuk respons JSON saat readiness gagal.
type responsSiap struct {
Status string `json:"status"`
Penyebab string `json:"penyebab"`
}
// Aplikasi memegang dependency yang diperiksa oleh handler readiness.
type Aplikasi struct {
db *StatusDB
}
// handlerHealthz adalah liveness probe: hanya membuktikan proses hidup.
// Tidak ada pemeriksaan dependency berat di sini — sengaja.
func (a *Aplikasi) handlerHealthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "hidup")
}
// handlerReadyz adalah readiness probe: memeriksa dependency yang
// menentukan siap-tidaknya layanan menerima traffic.
func (a *Aplikasi) handlerReadyz(w http.ResponseWriter, r *http.Request) {
if !a.db.Ping() {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(responsSiap{
Status: "tidak siap",
Penyebab: "database tidak merespons",
})
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "siap")
}
func main() {
db := &StatusDB{siap: true}
app := &Aplikasi{db: db}
// Wiring mux: dua endpoint terpisah dengan pola method + path.
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", app.handlerHealthz)
mux.HandleFunc("GET /readyz", app.handlerReadyz)
server := httptest.NewServer(mux)
defer server.Close()
for _, skenario := range []struct {
nama string
siap bool
}{
{"database merespons", true},
{"database mati", false},
} {
fmt.Printf("--- %s ---\n", skenario.nama)
db.SetSiap(skenario.siap)
for _, endpoint := range []string{"/healthz", "/readyz"} {
resp, err := http.Get(server.URL + endpoint)
if err != nil {
panic(err)
}
isi, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("GET %-8s -> %d %s", endpoint, resp.StatusCode, string(isi))
}
}
}Output yang Dihasilkan:
--- database merespons ---
GET /healthz -> 200 hidup
GET /readyz -> 200 siap
--- database mati ---
GET /healthz -> 200 hidup
GET /readyz -> 503 {"status":"tidak siap","penyebab":"database tidak merespons"}Mari kita telaah baris demi baris program di atas:
- Baris 12-30 (
StatusDB): dependency tiruan denganPingdanSetSiapyang dilindungisync.Mutex— menggantikan koneksi database sungguhan yang tidak tersedia di bundle. - Baris 32-36 (
responsSiap): struct ber-tag JSON menjadi bentuk respons kegagalan; urutan field mengikuti deklarasi, bukan urutan map. - Baris 43-48 (
handlerHealthz): liveness menulis 200 dan selesai — tanpa satu pun pemeriksaan dependency, sesuai prinsip ringan. - Baris 50-64 (
handlerReadyz):db.Ping()menentukan jalur; kegagalan mengembalikan 503 dengan JSON berisipenyebab, sukses mengembalikan 200. - Baris 70-75: wiring mux memakai pola
GET /healthzdanGET /readyz— method selain GET tertolak otomatis olehhttp.ServeMux;httptest.NewServermembungkushttp.Serversungguhan. - Baris 77-95: dua skenario berjalan berurutan;
db.SetSiapmengubah kondisi antar skenario, dan hasilGETdicetak berikut kode status dan isi responsnya.
Baris terakhir output adalah bukti pemisahan yang kita cari: pada skenario database mati, /healthz tetap 200 hidup (tidak ada restart) sementara /readyz menjawab 503 (pod dikeluarkan dari rotasi traffic sampai database pulih).
Konfigurasi Probe di Kubernetes #
Dua endpoint tersebut dipasangkan ke Deployment sebagai probe:
apiVersion: apps/v1
kind: Deployment
metadata:
name: layanan-pesanan
spec:
template:
spec:
containers:
- name: api
image: layanan-pesanan:1.4.0
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 2
failureThreshold: 30
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8080
periodSeconds: 5Dengan konfigurasi di atas, startup memberi waktu maksimal 60 detik untuk menyala (30 percobaan × 2 detik) sebelum kedua probe lain diaktifkan, liveness diperiksa setiap 10 detik, dan readiness setiap 5 detik.
Meredam Flapping #
Dependency yang lambat membuat readiness berkedip: siap, tidak siap, siap lagi — setiap kedip mengganggu load balancer. Meredamnya bisa dari dua sisi. Sisi Kubernetes memakai failureThreshold (butuh N kegagalan berurutan sebelum dinyatakan tidak siap) dan initialDelaySeconds. Sisi aplikasi memakai debouncing sederhana: tandai tidak siap hanya setelah kegagalan bertahan beberapa detik, atau lewati pemeriksaan yang baru saja gagal dalam jendela waktu singkat. Prinsipnya sama dengan menstabilkan sinyal sebelum sinyal itu menggerakkan tindakan besar.
Section Logging dan Observability ditutup di sini: log terstruktur menjadi data yang dapat dicari, level menjaga sinyal tetap terdengar, request ID dan trace menautkan peristiwa lintas layanan, metrik mengukur perilaku agregat, dan probe kesehatan menjadi pintu terakhir yang menentukan siapa yang boleh mengirim traffic. Konvensi ini akan kita pakai utuh saat menyusun aplikasi nyata pada section Proyek.
Referensi #
- Dokumentasi package net/http
—
ServeMuxpola method+path dan handler. - Package net/http/httptest — server lokal untuk menguji kedua endpoint.
- Dokumentasi package encoding/json — respons JSON kegagalan readiness.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
health-demo.go
Unduh berkaspackage main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"sync"
)
// StatusDB menirukan koneksi database: bisa hidup atau mati.
type StatusDB struct {
mu sync.Mutex
siap bool
}
// Ping melaporkan apakah database saat ini merespons.
func (d *StatusDB) Ping() bool {
d.mu.Lock()
defer d.mu.Unlock()
return d.siap
}
// SetSiap mengubah kondisi database untuk keperluan skenario demo.
func (d *StatusDB) SetSiap(v bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.siap = v
}
// responsSiap adalah bentuk respons JSON saat readiness gagal.
type responsSiap struct {
Status string `json:"status"`
Penyebab string `json:"penyebab"`
}
// Aplikasi memegang dependency yang diperiksa oleh handler readiness.
type Aplikasi struct {
db *StatusDB
}
// handlerHealthz adalah liveness probe: hanya membuktikan proses hidup.
// Tidak ada pemeriksaan dependency berat di sini — sengaja.
func (a *Aplikasi) handlerHealthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "hidup")
}
// handlerReadyz adalah readiness probe: memeriksa dependency yang
// menentukan siap-tidaknya layanan menerima traffic.
func (a *Aplikasi) handlerReadyz(w http.ResponseWriter, r *http.Request) {
if !a.db.Ping() {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(responsSiap{
Status: "tidak siap",
Penyebab: "database tidak merespons",
})
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "siap")
}
func main() {
db := &StatusDB{siap: true}
app := &Aplikasi{db: db}
// Wiring mux: dua endpoint terpisah dengan pola method + path.
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", app.handlerHealthz)
mux.HandleFunc("GET /readyz", app.handlerReadyz)
server := httptest.NewServer(mux)
defer server.Close()
for _, skenario := range []struct {
nama string
siap bool
}{
{"database merespons", true},
{"database mati", false},
} {
fmt.Printf("--- %s ---\n", skenario.nama)
db.SetSiap(skenario.siap)
for _, endpoint := range []string{"/healthz", "/readyz"} {
resp, err := http.Get(server.URL + endpoint)
if err != nil {
panic(err)
}
isi, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("GET %-8s -> %d %s", endpoint, resp.StatusCode, string(isi))
}
}
}