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 #

ProbePertanyaanAksi orkestrator saat gagalEndpoint lazim
Livenessapakah proses hidup?restart container/healthz
Readinesssiap menerima traffic?keluarkan dari daftar pod penerima load/readyz
Startupsudah 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.

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

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

  1. Baris 12-30 (StatusDB): dependency tiruan dengan Ping dan SetSiap yang dilindungi sync.Mutex — menggantikan koneksi database sungguhan yang tidak tersedia di bundle.
  2. Baris 32-36 (responsSiap): struct ber-tag JSON menjadi bentuk respons kegagalan; urutan field mengikuti deklarasi, bukan urutan map.
  3. Baris 43-48 (handlerHealthz): liveness menulis 200 dan selesai — tanpa satu pun pemeriksaan dependency, sesuai prinsip ringan.
  4. Baris 50-64 (handlerReadyz): db.Ping() menentukan jalur; kegagalan mengembalikan 503 dengan JSON berisi penyebab, sukses mengembalikan 200.
  5. Baris 70-75: wiring mux memakai pola GET /healthz dan GET /readyz — method selain GET tertolak otomatis oleh http.ServeMux; httptest.NewServer membungkus http.Server sungguhan.
  6. Baris 77-95: dua skenario berjalan berurutan; db.SetSiap mengubah kondisi antar skenario, dan hasil GET dicetak 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:

yaml
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: 5

Dengan 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 #

Berkas Sumber

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

health-demo.go

Unduh berkas
go
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))
		}
	}
}