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
AspekPassive (Open-Source)Active (Nginx Plus)
Deteksi kegagalanSetelah request klien gagalSebelum ada klien yang terdampak
Dampak ke klienBeberapa klien kena error duluNol — deteksi proaktif
Konfigurasimax_fails + fail_timeouthealth_check directive
Probe endpointTidak adaGET /health atau custom
BiayaGratis (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 aktif
  • nginx_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=30s adalah titik awal yang wajar; default max_fails=1 fail_timeout=10s terlalu 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 zone directive 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.

← Sebelumnya: Weighted   Berikutnya: Konsep SSL/TLS →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact