Load balancer dan orchestrator tidak bisa membaca kode untuk menilai aplikasi kita; keduanya butuh satu kontrak sederhana yang menjawab “apakah proses ini hidup?” dan “apakah proses ini siap menerima trafik?”. Endpoint health check adalah kontrak tersebut, biasanya berupa URL yang mengembalikan kode HTTP 200 bila sehat dan 503 bila tidak. Pada chapter ini kita rancang endpoint /healthz dari nol, pelajari perbedaan krusial antara liveness dan readiness, tentukan dependensi apa yang layak diperiksa, dan lihat bagaimana load balancer serta alerting mengonsumsi hasilnya.
Liveness versus Readiness #
Dua pertanyaan pembuka tadi tidak bisa dijawab dengan satu pemeriksaan yang sama. Liveness bertanya pada level proses: apakah aplikasi masih merespons sama sekali. Readiness bertanya pada level kapasitas: apakah semua prasyarat untuk melayani request sudah tersedia. Aplikasi yang kehilangan koneksi database tetap hidup — prosesnya jalan, hanya sedang tidak bisa melayani — sehingga jawaban untuk kedua pertanyaan itu berbeda.
| Aspek | Liveness | Readiness |
|---|---|---|
| Pertanyaan | “Apakah proses hidup dan merespons?” | “Apakah siap menerima trafik baru?” |
| Contoh pemeriksaan | Handler merespons, tidak deadlock | Database bisa di-query, storage cukup |
| Jika gagal | Proses direstart | Dikeluarkan dari pool load balancer |
| Dependensi eksternal down | Tidak boleh ikut gagal | Harus melaporkan not-ready |
Kolom terakhir adalah sumber kesalahan paling umum: menjadikan koneksi database bagian dari liveness. Bila database sempat lambat dan liveness ikut gagal, orchestrator me-restart seluruh instance aplikasi — padahal restart tidak memperbaiki database, dan aplikasi yang tadinya siap menyala kembali begitu database pulih malah dibunuh berulang. Kegagalan dependensi adalah masalah readiness, bukan liveness.
Merancang Endpoint /healthz #
Konvensi yang umum dipakai adalah path /healthz (beberapa sistem memakai /livez dan /readyz terpisah). Endpoint ini mengembalikan JSON ringkas berisi status keseluruhan dan rincian tiap pemeriksaan, dengan kode HTTP yang mencerminkan statusnya. Mari kita tulis handler-nya sebagai fungsi murni agar bisa diuji langsung tanpa web server:
<?php
function healthResponse(array $checks): array
{
$semuaSehat = true;
$detail = [];
foreach ($checks as $nama => $hasil) {
$sehat = $hasil === true;
$detail[] = [
"name" => $nama,
"status" => $sehat ? "up" : "down",
];
if (!$sehat) {
$semuaSehat = false;
}
}
return [
"status" => $semuaSehat ? "ok" : "unhealthy",
"httpCode" => $semuaSehat ? 200 : 503,
"checks" => $detail,
];
}
function cekDatabase(): bool
{
try {
$pdo = new PDO("sqlite::memory:");
$pdo->exec("SELECT 1");
return true;
} catch (PDOException $e) {
return false;
}
}
function cekRuangDisk(float $bebasByte, float $ambangByte): bool
{
return $bebasByte >= $ambangByte;
}
$sehat = healthResponse([
"database" => cekDatabase(),
"storage" => cekRuangDisk(2.5 * 1024 ** 3, 1.0 * 1024 ** 3),
]);
$rusak = healthResponse([
"database" => cekDatabase(),
"storage" => cekRuangDisk(0.4 * 1024 ** 3, 1.0 * 1024 ** 3),
]);
echo json_encode($sehat, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
echo "HTTP " . $rusak["httpCode"] . " " . $rusak["status"] . "\n";Output yang Dihasilkan:
{
"status": "ok",
"httpCode": 200,
"checks": [
{
"name": "database",
"status": "up"
},
{
"name": "storage",
"status": "up"
}
]
}
HTTP 503 unhealthyMari kita bedah rancangannya. healthResponse() menerima map nama-pemeriksaan menuju nilai boolean, membangun array checks berisi status per item, dan menurunkan status keseluruhan menjadi unhealthy begitu satu saja pemeriksaan gagal — skenario $rusak memperlihatkan storage yang hanya menyisakan 0,4 GiB dari ambang 1 GiB. Kode HTTP disertakan di dalam array agar pemanggil tinggal meneruskannya; 503 Service Unavailable adalah sinyal standar bagi load balancer untuk berhenti mengirim trafik. cekDatabase() melakukan ping sungguhan — SELECT 1 pada koneksi SQLite in-memory di lingkungan uji; di produksi baris yang sama memakai DSN database asli dan query yang sama karena SELECT 1 adalah cara termurah memastikan koneksi hidup.
Menyambungkan handler ini ke HTTP hanya butuh beberapa baris. Blok berikut memperlihatkan bentuknya di berkas public/healthz.php — diberi penanda skip karena membutuhkan web server dan konfigurasi nyata:
<?php
// public/healthz.php - dipanggil load balancer setiap beberapa detik
require __DIR__ . "/../src/health.php";
$response = healthResponse([
"database" => cekDatabase(),
"storage" => cekRuangDisk(
(float) disk_free_space("/var/www/toko/storage"),
1.0 * 1024 ** 3
),
]);
http_response_code($response["httpCode"]);
header("Content-Type: application/json");
echo json_encode($response, JSON_UNESCAPED_SLASHES);Perbedaannya hanya pada sumber data: disk_free_space() membaca kondisi filesystem sungguhan, lalu http_response_code() dan header() mengirim kode status serta tipe konten sebelum body JSON dicetak. Semua logika keputusan tetap berada di fungsi murni yang sudah teruji di atas.
Memeriksa Dependensi #
Pilihan dependensi yang diperiksa menentukan seberapa berguna sinyal readiness. Aturan praktisnya: periksa dependensi yang aplikasi selalu butuh untuk melayani request — biasanya database utama dan storage untuk penulisan berkas. Dependensi opsional (layanan email, CDN) tidak layak membalikkan readiness, karena aplikasi masih bisa melayani sebagian besar fitur tanpanya; cukup catat statusnya di log atau metrik.
Pemeriksaan database sebaiknya seringan mungkin: SELECT 1 cukup, bukan query yang menyentuh tabel besar. Untuk storage, bandingkan ruang bebas terhadap ambang yang masuk akal — contoh kita memakai 1 GiB untuk partisi penyimpanan unggahan. Ambang yang terlalu tinggi membuat aplikasi not-ready padahal masih bisa bekerja; terlalu rendah membuat disk penuh terdeteksi setelah terlambat.
Menghindari Pemeriksaan Terlalu Dalam #
Load balancer bisa memanggil /healthz setiap lima detik dari selusin instance — artinya pemeriksaan yang mengeksekusi query nyata dijalankan ratusan kali per menit. Bila database mulai lambat, health check justru menambah beban tepat saat database kesulitan, dan ketika semua instance ikut melaporkan not-ready, insiden kecil berubah menjadi pemadaman total. Inilah yang disebut cascading failure.
Solusinya: jalankan pemeriksaan dalam hanya sesekali dan layani sisanya dari cache. Simulasi berikut memperlihatkan polanya dengan state tetap agar deterministik:
<?php
function jawabPoll(array $cache, int $pollSaatIni, int $intervalDeep): array
{
$umurCache = $pollSaatIni - $cache["diperbaruiPadaPoll"];
if ($umurCache >= $intervalDeep) {
return [
"sumber" => "deep-check",
"status" => $cache["status"],
"diperbaruiPadaPoll" => $pollSaatIni,
];
}
return [
"sumber" => "cache",
"status" => $cache["status"],
"diperbaruiPadaPoll" => $cache["diperbaruiPadaPoll"],
];
}
$cache = ["status" => "ok", "diperbaruiPadaPoll" => 0];
foreach ([1, 2, 3, 6, 7] as $poll) {
$hasil = jawabPoll($cache, $poll, 5);
echo "poll #{$poll}: sumber={$hasil['sumber']} status={$hasil['status']}\n";
$cache = $hasil;
}Output yang Dihasilkan:
poll #1: sumber=cache status=ok
poll #2: sumber=cache status=ok
poll #3: sumber=cache status=ok
poll #6: sumber=deep-check status=ok
poll #7: sumber=cache status=okPoll 1 sampai 3 terlayani dari cache karena usianya masih di bawah interval lima detik; poll 6 menemukan cache berumur enam dan menjalankan pemeriksaan dalam, sekaligus memperbarui stempel waktu; poll 7 kembali memakai cache. Pada implementasi nyata, worker latar belakang menjalankan pemeriksaan dalam setiap beberapa detik dan menulis hasilnya ke variabel bersama (misalnya APCu atau berkas kecil), sementara handler /healthz hanya membacanya — pemeriksaan mahal tidak pernah dieksekusi di jalur request.
Integrasi dengan Load Balancer #
Load balancer mengonsumsi health check melalui tiga parameter: interval polling, timeout per permintaan, dan ambang kegagalan. Interval 5-10 detik adalah titik seimbang — lebih cepat mendeteksi kegagalan lebih awal tetapi menambah beban; timeout harus pendek (1-2 detik) karena health check yang menggantung sama saja dengan kegagalan; ambang kegagalan (misalnya tiga kali berturut-turut) mencegah gangguan sesaat mengeluarkan server dari pool. Nginx mendefinisikan perilaku ini langsung pada blok upstream:
upstream php_app {
server 10.0.0.11:8080 max_fails=3 fail_timeout=15s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=15s;
}Konfigurasi di atas memakai passive health check: Nginx menghitung kegagalan koneksi nyata ke setiap server, dan setelah tiga kegagalan dalam 15 detik server tersebut dilewati selama 15 detik berikutnya. Pendekatan aktif — load balancer yang secara khusus memanggil /healthz setiap interval — menawarkan deteksi lebih awal dan dipakai HAProxy, Consul, atau orchestrator seperti Kubernetes. Terlepas dari mekanismenya, kontraknya sama: respons non-200 berarti jangan kirim trafik.
Alerting saat Health Gagal #
Sinyal health yang tidak ditindaklanjuti hanya dekorasi. Pemetaan yang sehat mengikuti jenis kegagalannya: liveness gagal berarti proses mati dan berdampak segera, jadi layak membangunkan on-call; readiness gagal berarti kapasitas berkurang tetapi service mungkin masih melayani sebagian, cukup notifikasi prioritas rendah atau tiket. Sertakan durasi kegagalan pada pesan alert — “readiness gagal selama 20 menit” membawa informasi yang jauh berbeda dari satu kegagalan sesaat.
Untuk mencegah badai alert saat satu komponen membuat banyak instance gagal bersamaan, deduplikasi alert berdasarkan penyebabnya: satu alert “database tidak dapat dijangkau” lebih berguna daripada empat puluh alert “instance tidak ready”. Pola ini sekaligus mengingatkan kita bahwa health check per-instance adalah gejala, dan diagnosis akarnya tetap tugas manusia dengan akses ke log serta metrik.
Ringkasan #
Health check adalah kontrak antara aplikasi dan infrastruktur: liveness menjawab proses hidup atau tidak, readiness menjawab layak menerima trafik atau tidak, dan keduanya tidak boleh dicampur agar kegagalan dependensi tidak memicu restart massal. Endpoint /healthz mengembalikan JSON dengan kode 200 atau 503, pemeriksaan dalam dijalankan sesekali dan dilayani dari cache untuk mencegah cascading failure, dan load balancer mengubah kode status menjadi keputusan routing. Kontrak ini juga menjadi fondasi saat aplikasi dimatikan dengan anggun: langkah pertama graceful shutdown adalah membalik readiness sebelum berhenti — tekniknya kita bahas pada chapter Graceful Shutdown
.