Lonjakan status HTTP 503 Service Unavailable saat proses rolling update di Kubernetes umumnya bersumber dari dua kesalahan konfigurasi: traffic dialihkan ke pod sebelum pool koneksi database siap menerima query, atau Kubelet menghentikan pod sehat karena liveness probe mengalami timeout saat beban kerja tinggi.

Panduan ini membahas arsitektur health check yang tepat pada aplikasi Rust berbasis Actix Web, memisahkan logika liveness dan readiness secara ketat, serta sinkronisasi lifecycle pod dengan Kubernetes Endpoints controller.

Postmortem: Mengapa HTTP 503 Muncul Saat Deployment?

Saat Deployment menjalankan pembaruan pod via strategi RollingUpdate, dua masalah race condition sering terjadi secara bersamaan:

  1. Traffic mendahului inisialisasi: Container Actix Web aktif dan port HTTP terbuka, tetapi inisialisasi pool database (misalnya SQLx atau Deadpool) masih berlangsung atau menunggu alokasi koneksi awal. Kubelet menganggap pod siap dan Endpoint Controller langsung mendaftarkan IP pod ke Service iptables/IPVS. Request pertama dari klien gagal dengan status 500 atau 503.
  2. Anti-Pattern Liveness Probe dengan DB Ping: Endpoint liveness mengecek koneksi database via query SELECT 1. Ketika database mengalami lonjakan beban sesaat, latency query meningkat melampaui timeoutSeconds liveness probe. Kubelet membunuh worker pod yang sebenarnya masih sehat dan sedang memproses request. Ini memicu kaskade pod restart berulang (crash loop semu).
  3. SIGTERM mendahului deregistrasi endpoint: Kubelet mengirim sinyal SIGTERM ke pod lama pada saat yang hampir sama dengan pengiriman sinyal update IP ke kube-proxy di node-node lain. Traffic masih dikirim ke pod lama yang sudah mulai mematikan server socket-nya.

Pemisahan Tanggung Jawab: /livez vs /readyz

Dua probe Kubernetes memiliki tujuan operasional yang berbeda mendasar:

  • Liveness Probe (/livez): Bertujuan mendeteksi deadlocks atau event loop yang macet total. Sifat operasi ini harus zero-I/O. Jangan pernah menyertakan pengecekan database, cache, atau network hop lain di sini. Jika endpoint ini gagal, Kubelet akan me-restart container.
  • Readiness Probe (/readyz): Bertujuan memvalidasi apakah aplikasi sanggup melayani traffic klien. Probe ini memeriksa status koneksi downstream (seperti database SQLx) dengan batas timeout yang ketat. Jika endpoint ini gagal, Kubelet hanya mencopot IP pod dari Service Endpoint. Container tetap hidup dan diberi kesempatan untuk memulihkan koneksi atau menyelesaikan inisialisasi.

Implementasi Handler pada Actix Web

Berikut implementasi endpoint /livez dan /readyz menggunakan Actix Web dan SQLx PostgreSQL. Cek koneksi di-wrap menggunakan tokio::time::timeout untuk mencegah probe worker menggantung jika pool kehabisan slot.

use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
use sqlx::PgPool;
use std::time::Duration;

struct AppState {
    db: PgPool,
}

// Zero I/O: hanya memvalidasi event loop Actix masih responsif
#[get("/livez")]
async fn liveness_handler() -> impl Responder {
    HttpResponse::Ok().body("OK")
}

// Non-blocking timeout check terhadap database pool
#[get("/readyz")]
async fn readiness_handler(data: web::Data<AppState>) -> impl Responder {
    let timeout_duration = Duration::from_millis(800);

    let db_check = tokio::time::timeout(
        timeout_duration,
        sqlx::query("SELECT 1").execute(&data.db),
    )
    .await;

    match db_check {
        Ok(Ok(_)) => HttpResponse::Ok().body("READY"),
        Ok(Err(e)) => {
            eprintln!("Readiness DB query failed: {:?}", e);
            HttpResponse::ServiceUnavailable().body("DB Unreachable")
        }
        Err(_) => {
            eprintln!("Readiness DB check timed out");
            HttpResponse::ServiceUnavailable().body("DB Check Timeout")
        }
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let database_url = std::env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");

    let pool = sqlx::postgres::PgPoolOptions::new()
        .max_connections(20)
        .acquire_timeout(Duration::from_secs(3))
        .connect(&database_url)
        .await
        .expect("Failed to create pool");

    let app_state = web::Data::new(AppState { db: pool });

    HttpServer::new(move || {
        App::new()
            .app_data(app_state.clone())
            .service(liveness_handler)
            .service(readiness_handler)
    })
    .bind(("0.0.0.0", 8080))?
    .run()
    .await
}
Catatan: Parameter timeout_duration pada tokio::time::timeout harus lebih rendah daripada timeoutSeconds yang ditentukan di Kubernetes spec untuk menghindari socket hang pada Kubelet.

Konfigurasi Kubernetes Deployment Spec

Konfigurasikan pod probe beserta preStop hook. Hook preStop memberi jeda waktu agar kube-proxy di seluruh worker node sempat mencopot IP pod dari tabel routing sebelum server Actix Web menerima sinyal shutdown (SIGTERM).

apiVersion: apps/v1
kind: Deployment
metadata:
  name: actix-api
  labels:
    app: actix-api
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: actix-api
  template:
    metadata:
      labels:
        app: actix-api
    spec:
      containers:
      - name: api
        image: my-registry/actix-api:v1.2.0
        ports:
        - containerPort: 8080
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 5"]
        livenessProbe:
          httpGet:
            path: /livez
            port: 8080
          initialDelaySeconds: 2
          periodSeconds: 10
          timeoutSeconds: 1
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8080
          initialDelaySeconds: 3
          periodSeconds: 5
          timeoutSeconds: 1
          failureThreshold: 2

Analisis Parameter Kritis

  • maxUnavailable: 0: Memaksa Kubernetes untuk meluncurkan pod baru dan memastikan statusnya Ready sebelum menghapus pod lama.
  • preStop: sleep 5: Menghentikan eksekusi SIGTERM langsung ke container selama 5 detik. Dalam durasi ini, Pod berstatus Terminating dan Endpoint controller menghapus pod dari daftar target upstream Service/Ingress secara konsisten.
  • initialDelaySeconds pada readiness: Memberi toleransi waktu bootstrapping worker threads Actix Web dan koneksi pool TCP awal sebelum evaluasi dimulai.

Verifikasi Rollout Tanpa Downtime

Untuk memastikan tidak ada 503 selama deployment, lakukan stress testing concurrently saat rolling update berlangsung menggunakan tool seperti hey atau k6.

Jalankan load generator pada endpoint bisnis yang memerlukan query DB:

hey -z 30s -q 50 -c 10 http://api.internal.local/v1/resource

Di terminal terpisah, trigger proses rolling restart:

kubectl rollout restart deployment/actix-api
kubectl rollout status deployment/actix-api

Evaluasi hasil output benchmark. Bila konfigurasi berhasil, seluruh request harus menghasilkan status [200] tanpa kemunculan respon [503] atau connection reset by peer.