Health Check #
Health check adalah mekanisme untuk mendeteksi server backend yang tidak berfungsi dan mengeluarkannya dari rotasi load balancing secara otomatis. Tanpa health check, Nginx terus mengirimkan request ke server yang down hingga klien menerima error — pengalaman yang buruk untuk pengguna dan sulit di-debug.
Memahami perbedaan antara passive dan active health check adalah kunci untuk merancang sistem yang andal.
Passive vs Active: Perbedaan Fundamental #
flowchart TD
subgraph PASSIVE["Passive Health Check\n(Nginx Open-Source)"]
direction TB
P1["Klien → Nginx → Server A\n(request normal)"]
P2["Server A gagal merespons"]
P3["Nginx catat kegagalan\nfail counter +1"]
P4{"fail counter\n≥ max_fails?"}
P5["Server A tetap di rotasi\n(user berikutnya bisa kena)"]
P6["Server A ditandai DOWN\nselama fail_timeout detik"]
P1 --> P2 --> P3 --> P4
P4 -- "Belum" --> P5
P4 -- "Ya" --> P6
end
subgraph ACTIVE["Active Health Check\n(Nginx Plus saja)"]
direction TB
A1["Nginx proactively kirim\nGET /health setiap N detik"]
A2{"Respons\n200 OK?"}
A3["Server tetap di rotasi\n(klien tidak terpengaruh)"]
A4["Server langsung ditandai DOWN\n(SEBELUM ada klien yang kena)"]
A1 --> A2
A2 -- "Ya" --> A3
A2 -- "Tidak" --> A4
end| Aspek | Passive (Open-Source) | Active (Nginx Plus) |
|---|---|---|
| Deteksi kegagalan | Setelah request klien gagal | Sebelum ada klien yang terdampak |
| Dampak ke klien | Beberapa klien kena error dulu | Nol — deteksi proaktif |
| Konfigurasi | max_fails + fail_timeout | health_check directive |
| Probe endpoint | Tidak ada | GET /health atau custom |
| Biaya | Gratis (open-source) | Nginx Plus (berbayar) |
Passive Health Check: Konfigurasi Lengkap #
upstream app_servers {
# Shared memory untuk data health check antar worker process
zone app_upstream 64k;
# max_fails: berapa kali gagal dalam window fail_timeout sebelum server ditandai down
# fail_timeout: durasi window kegagalan DAN durasi server "dikecualikan"
server 10.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.2:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.3:3000 max_fails=3 fail_timeout=30s;
# Server backup — tampil jika semua server di atas down
server 10.0.0.4:8080 backup;
keepalive 32;
}
server {
listen 443 ssl;
server_name example.com;
location / {
proxy_pass http://app_servers;
# Kondisi yang dihitung sebagai "kegagalan" untuk max_fails
# error: koneksi ditolak/timeout
# timeout: proxy_read_timeout terlampaui
# http_500, http_502, http_503, http_504: error dari backend
proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
# Total waktu maksimum untuk mencoba semua server
proxy_next_upstream_timeout 10s;
# Maksimum berapa kali retry ke server lain
proxy_next_upstream_tries 3;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
}
Menyetel Nilai yang Tepat #
Untuk aplikasi web normal:
max_fails=3 fail_timeout=30s
→ Server dikeluarkan setelah 3 kegagalan dalam 30 detik
→ Kembali dicoba setelah 30 detik
→ Toleransi moderat, hindari false positive dari spike sesaat
Untuk aplikasi kritis (fintech, healthcare):
max_fails=2 fail_timeout=15s
→ Lebih agresif, deteksi lebih cepat
→ Pastikan monitoring aktif karena false positive lebih mungkin
Untuk aplikasi yang sesekali GC pause atau slow (Java, Go dengan GC):
max_fails=5 fail_timeout=60s
→ Toleransi tinggi — GC pause yang menyebabkan timeout sesaat tidak
langsung mengeluarkan server dari rotasi
Default jika tidak ditentukan: max_fails=1 fail_timeout=10s
→ TERLALU AGRESIF untuk kebanyakan kasus
→ Satu timeout saja sudah mengeluarkan server selama 10 detik
Health Endpoint: Best Practices di Sisi Aplikasi #
Meskipun Nginx open-source tidak melakukan active health check, kita tetap perlu endpoint /health yang baik di setiap backend — untuk monitoring eksternal, load balancer lain, atau Kubernetes readiness probe:
Node.js #
// Health check yang memeriksa semua dependency kritis
app.get('/health', async (req, res) => {
const checks = {};
let statusCode = 200;
// Cek koneksi database
try {
await db.raw('SELECT 1');
checks.database = { status: 'ok' };
} catch (err) {
checks.database = { status: 'error', message: err.message };
statusCode = 503;
}
// Cek koneksi Redis/cache
try {
await redis.ping();
checks.cache = { status: 'ok' };
} catch (err) {
checks.cache = { status: 'error', message: err.message };
statusCode = 503;
}
// Cek message queue (opsional — tergantung apakah critical)
try {
// Hanya cek koneksi, tidak perlu consume message
checks.queue = { status: 'ok' };
} catch (err) {
checks.queue = { status: 'degraded' };
// Tidak set statusCode ke 503 jika queue bukan critical
}
res.status(statusCode).json({
status: statusCode === 200 ? 'ok' : 'error',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
checks
});
});
Go #
func healthHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
checks := map[string]string{}
statusCode := http.StatusOK
// Database check
if err := db.PingContext(ctx); err != nil {
checks["database"] = "error: " + err.Error()
statusCode = http.StatusServiceUnavailable
} else {
checks["database"] = "ok"
}
// Redis check
if err := rdb.Ping(ctx).Err(); err != nil {
checks["cache"] = "error: " + err.Error()
statusCode = http.StatusServiceUnavailable
} else {
checks["cache"] = "ok"
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": map[int]string{200: "ok", 503: "error"}[statusCode],
"checks": checks,
})
}
Nginx untuk Health Endpoint #
Expose health endpoint di port terpisah agar tidak bercampur dengan traffic production:
# Port 8080: khusus untuk internal health check dan monitoring
server {
listen 8080;
server_name _;
# Batasi akses hanya dari jaringan internal
allow 10.0.0.0/8;
allow 127.0.0.1;
deny all;
# Proxy ke health endpoint aplikasi
location /health {
proxy_pass http://localhost:3000/health;
access_log off; # Jangan log health check request ke access log normal
proxy_read_timeout 5s;
}
# Status Nginx sendiri
location /nginx_status {
stub_status;
}
}
Mensimulasikan Active Health Check dengan OpenResty #
Jika menggunakan OpenResty (Nginx + LuaJIT), bisa menambahkan active health check menggunakan modul lua-resty-upstream-healthcheck:
# Install OpenResty dan modul
opm get openresty/lua-resty-upstream-healthcheck
# nginx.conf (OpenResty)
http {
# Shared memory untuk status health check
lua_shared_dict healthcheck 1m;
# Jalankan health checker di background untuk setiap worker
init_worker_by_lua_block {
local hc = require "resty.upstream.healthcheck"
local ok, err = hc.spawn_checker {
shm = "healthcheck", -- nama shared memory
upstream = "app_servers", -- nama upstream block
type = "http",
http_req = "GET /health HTTP/1.0\r\nHost: localhost\r\n\r\n",
interval = 2000, -- cek setiap 2000ms
timeout = 1000, -- timeout 1000ms
fall = 3, -- 3 kegagalan → tandai DOWN
rise = 2, -- 2 sukses → tandai UP kembali
valid_statuses = {200, 204}, -- status code yang dianggap sehat
concurrency = 10, -- cek semua server secara concurrent
}
if not ok then
ngx.log(ngx.ERR, "Failed to spawn health checker: ", err)
end
}
upstream app_servers {
server 10.0.0.1:3000;
server 10.0.0.2:3000;
server 10.0.0.3:3000;
keepalive 32;
}
server {
# Endpoint untuk melihat status health check saat ini
location /upstream_status {
allow 127.0.0.1;
deny all;
content_by_lua_block {
local hc = require "resty.upstream.healthcheck"
ngx.say(hc.status_page())
}
}
}
}
# Melihat status health check
curl http://localhost/upstream_status
# Output:
# Upstream app_servers
# Primary Peers
# 10.0.0.1:3000 UP
# 10.0.0.2:3000 DOWN # sudah dikeluarkan dari rotasi
# 10.0.0.3:3000 UP
Monitoring Upstream dengan Prometheus #
Untuk monitoring yang lebih komprehensif, gunakan nginx-module-vts atau nginx-prometheus-exporter:
# nginx-prometheus-exporter: expose stub_status sebagai Prometheus metrics
docker run -d \
--name nginx-exporter \
-p 9113:9113 \
nginx/nginx-prometheus-exporter:latest \
-nginx.scrape-uri=http://localhost:8080/nginx_status
# Konfigurasi scrape di Prometheus
# prometheus.yml
scrape_configs:
- job_name: nginx
static_configs:
- targets: ['localhost:9113']
Metrics yang tersedia:
nginx_connections_active— jumlah koneksi aktifnginx_connections_waiting— koneksi idle (keepalive)nginx_http_requests_total— total request
Untuk per-upstream metrics, gunakan nginx-module-vts yang memberikan:
- Request count per backend server
- Response time per backend server
- Error count per backend server
Graceful Failover: Strategi Production Lengkap #
Pendekatan berlapis untuk Nginx open-source di production:
upstream app_servers {
zone app_upstream 64k;
# Layer 1: Primary servers — passive health check
server 10.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.2:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.3:3000 max_fails=3 fail_timeout=30s;
# Layer 2: Backup server — halaman maintenance
server 10.0.0.4:8080 backup;
keepalive 32;
}
server {
location / {
proxy_pass http://app_servers;
# Retry ke server lain saat ada kegagalan
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 10s;
# Intercept error dari backend untuk custom error page
proxy_intercept_errors on;
error_page 502 503 504 = @maintenance;
}
location @maintenance {
# Jika semua backend down termasuk backup
root /var/www;
try_files /maintenance.html =503;
add_header Retry-After "60" always;
add_header Cache-Control "no-store" always;
}
}
Alerting Saat Server Down #
#!/bin/bash
# /usr/local/bin/check-nginx-upstream.sh
# Jalankan dari cron setiap menit
BACKENDS=("10.0.0.1:3000" "10.0.0.2:3000" "10.0.0.3:3000")
SLACK_WEBHOOK="https://hooks.slack.com/services/xxx/yyy/zzz"
for backend in "${BACKENDS[@]}"; do
response=$(curl -s -o /dev/null -w "%{http_code}" \
--max-time 5 "http://$backend/health")
if [ "$response" != "200" ]; then
# Kirim alert ke Slack
curl -s -X POST "$SLACK_WEBHOOK" \
-H 'Content-type: application/json' \
--data "{\"text\":\"⚠️ Backend $backend DOWN (HTTP $response)\"}"
# Log ke file
echo "[$(date)] Backend $backend DOWN (HTTP $response)" >> /var/log/nginx/upstream-health.log
fi
done
# Tambahkan ke crontab
# crontab -e
* * * * * /usr/local/bin/check-nginx-upstream.sh
Integrasi dengan Kubernetes dan Container #
Dalam lingkungan container, health check di Nginx perlu mempertimbangkan readiness probe dan liveness probe dari Kubernetes:
# Kubernetes Deployment: pastikan pod siap menerima traffic sebelum dimasukkan ke pool
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: myapp:v2
ports:
- containerPort: 3000
# Readiness probe: pod hanya terima traffic jika /health mengembalikan 200
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10 # Tunggu 10 detik setelah container start
periodSeconds: 5 # Cek setiap 5 detik
failureThreshold: 3 # Keluarkan dari service setelah 3 gagal
successThreshold: 1 # Masukkan kembali setelah 1 sukses
# Liveness probe: restart container jika deadlock
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 5
Jika Nginx dijalankan sebagai ingress di Kubernetes (misalnya NGINX Ingress Controller), health check ditangani secara native. Tapi jika Nginx berdiri di luar cluster sebagai load balancer eksternal, kita perlu mengkonfigurasi passive health check seperti biasa dan memastikan endpoint /health dari pod tersedia.
# Nginx di luar Kubernetes, load balance ke pod
# Pod dijangkau melalui NodePort atau LoadBalancer Service
upstream k8s_pods {
server k8s-node-1:30000 max_fails=3 fail_timeout=30s; # NodePort
server k8s-node-2:30000 max_fails=3 fail_timeout=30s;
server k8s-node-3:30000 max_fails=3 fail_timeout=30s;
zone k8s_upstream 64k;
keepalive 32;
}
Testing Health Check Endpoint #
Sebelum deploy ke production, pastikan health endpoint bekerja dengan benar:
# Test endpoint health langsung ke backend
curl -v http://10.0.0.1:3000/health
# Expected:
# HTTP/1.1 200 OK
# Content-Type: application/json
# {"status":"ok","checks":{"database":"ok","cache":"ok"}}
# Simulasi kondisi database down
# (matikan database, lalu test)
curl -v http://10.0.0.1:3000/health
# Expected:
# HTTP/1.1 503 Service Unavailable
# {"status":"error","checks":{"database":"error: connection refused","cache":"ok"}}
# Test response time health endpoint (harus cepat, < 500ms)
for i in {1..10}; do
time curl -s http://10.0.0.1:3000/health > /dev/null
done
# Test bahwa health endpoint tidak mengekspos informasi sensitif
curl http://10.0.0.1:3000/health
# Pastikan tidak ada stack trace, version string, atau credential
# Script monitoring sederhana yang berjalan setiap menit (via cron)
#!/bin/bash
# /usr/local/bin/health-monitor.sh
BACKENDS=("10.0.0.1:3000" "10.0.0.2:3000" "10.0.0.3:3000")
LOG="/var/log/nginx/health-monitor.log"
ALERT_EMAIL="[email protected]"
for backend in "${BACKENDS[@]}"; do
result=$(curl -s -o /tmp/health_response -w "%{http_code}" \
--max-time 5 "http://$backend/health")
if [ "$result" = "200" ]; then
echo "[$(date)] OK: $backend" >> $LOG
else
echo "[$(date)] FAIL: $backend (HTTP $result)" >> $LOG
# Kirim email alert
echo "Backend $backend health check FAILED (HTTP $result)" | \
mail -s "[ALERT] Backend Down: $backend" $ALERT_EMAIL
# Atau kirim ke Slack
curl -s -X POST https://hooks.slack.com/services/XXX \
-H 'Content-type: application/json' \
--data "{\"text\":\"Backend $backend DOWN (HTTP $result)\"}" \
>> /dev/null
fi
done
Ringkasan #
- Nginx open-source hanya mendukung passive health check — kegagalan terdeteksi setelah ada request klien yang gagal, bukan sebelumnya.
max_fails=3 fail_timeout=30sadalah titik awal yang wajar; defaultmax_fails=1 fail_timeout=10sterlalu agresif untuk kebanyakan kasus.- Buat health endpoint di setiap backend (
GET /health) yang memeriksa database, cache, dan dependency kritis — respons 200 jika sehat, 503 jika tidak.- Gunakan
zonedirective di upstream agar data passive health check (fail counter) dibagi antar semua worker process Nginx.- Untuk active health check tanpa Nginx Plus: gunakan OpenResty +
lua-resty-upstream-healthcheck, atau script cron eksternal yang melakukan probe ke/health.- Active health check (probing proaktif yang tidak mengorbankan request klien) hanya tersedia di Nginx Plus.
- Lengkapi dengan alerting eksternal (Prometheus, script cron ke Slack/PagerDuty) agar tim langsung tahu saat server down — jangan hanya mengandalkan passive detection.