Ringkasan Insiden

Saat eksekusi rolling deployment rutin pada layanan backend berbasis Actix Web di kluster Kubernetes, sistem mendadak mengalami lonjakan galat HTTP 500 pada rute yang membutuhkan akses database. Metrik latensi API melonjak drastis dan log aplikasi mencatat kegagalan koneksi ke PostgreSQL:

ERROR sqlx::pool: error returned from database: FATAL: sorry, too many clients already

Insiden ini menyebabkan penurunan ketersediaan API hingga 42% selama 14 menit sebelum mitigasi darurat diberlakukan.

Tindakan Darurat dan Rollback

Langkah pertama adalah membatalkan proses deployment yang sedang berjalan untuk menghentikan pembuatan pod baru:

kubectl rollout undo deployment/auth-service -n production

Setelah rollback dieksekusi, periksa status terminasi pod lama dan pantau penurunan jumlah koneksi aktif langsung dari server PostgreSQL:

SELECT count(*) AS total_connections, state 
FROM pg_stat_activity 
GROUP BY state;

Jika koneksi idle dari pod yang sedang diterminasi masih tertinggal dan memblokir kapasitas, putuskan koneksi yang tidak merespons secara manual:

SELECT pg_terminate_backend(pid) 
FROM pg_stat_activity 
WHERE state = 'idle' 
  AND query_start < now() - interval '2 minutes' 
  AND application_name LIKE '%auth-service%';

Metrik koneksi kembali normal di bawah batas aman (< 65% dari max_connections) dalam waktu 3 menit setelah rollback berhasil.

Analisis Akar Masalah (Root Cause Analysis)

Insiden ini dipicu oleh akumulasi dua faktor: konfigurasi rolling update Kubernetes dan kesalahan arsitektur inisialisasi connection pool SQLx di Actix Web.

1. Efek Penggandaan maxSurge pada Kubernetes

Manifest deployment sebelumnya menggunakan konfigurasi default:

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 25%
    maxUnavailable: 0

Dengan 12 replika eksisting, maxSurge: 25% membuat 3 pod baru sebelum pod lama dimatikan. Ditambah lagi, proses terminasi pod lama menunggu terminationGracePeriodSeconds: 30. Akibatnya, ada rentang waktu di mana 15 pod berjalan paralel dan masing-masing mempertahankan koneksi aktif ke PostgreSQL.

2. Anti-Pattern Inisialisasi Pool pada Actix Web

Kesalahan fatal terjadi pada src/main.rs. Developer menginisialisasi PgPool di dalam closure HttpServer::new alih-alih di luarnya:

// SALAH: Pool dibuat per-worker thread
HttpServer::new(move || {
    let pool = PgPoolOptions::new()
        .max_connections(10)
        .connect_lazy(&db_url)
        .unwrap();

    App::new()
        .app_data(web::Data::new(pool))
        .service(handlers::login)
})

Actix Web menjalankan worker thread sejumlah core CPU (8 worker per node). Kode di atas menginstansiasi connection pool terpisah untuk setiap worker thread. Perhitungannya:

  • 15 Pod × 8 Worker × 10 Max Connections = 1.200 potensi koneksi.
  • Kapasitas PostgreSQL: max_connections = 200.

Batas maksimal koneksi PostgreSQL terlampaui seketika saat pod tahap surge mulai melakukan warm-up.

Solusi Teknis dan Perbaikan

1. Perbaikan Kode Inisialisasi SQLx

Inisialisasi pool satu kali saja di fungsi main runtime Tokio, lalu bungkus dalam web::Data dan salin (clone) referensinya ke dalam closure HttpServer::new. Dengan cara ini, seluruh worker thread pada satu pod berbagi satu pool yang sama.

use actix_web::{web, App, HttpServer};
use sqlx::postgres::PgPoolOptions;
use std::time::Duration;

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

    // Buat SATU pool untuk seluruh instance pod
    let pool = PgPoolOptions::new()
        .max_connections(5) // Cukup untuk beban I/O asinkronus per pod
        .min_connections(2)
        .acquire_timeout(Duration::from_secs(3))
        .idle_timeout(Duration::from_secs(600))
        .connect(&db_url)
        .await
        .expect("Failed to connect to Postgres");

    HttpServer::new(move || {
        App::new()
            // Menggunakan referensi Arc internal dari PgPool
            .app_data(web::Data::new(pool.clone()))
            .route("/healthz", web::get().to(handlers::health_check))
    })
    .bind(("0.0.0.0", 8080))?
    .run()
    .await
}

2. Penyesuaian Strategi Rolling Update dan PreStop Hook

Kendalikan lonjakan pod baru dan percepat pemutusan koneksi saat pod menerima sinyal SIGTERM:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: auth-service
spec:
  replicas: 12
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  template:
    spec:
      containers:
      - name: api
        image: auth-service:v2.1.0
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 5"]
        readinessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 2
          periodSeconds: 5

Dengan maxSurge: 1 dan maxUnavailable: 1, jumlah pod aktif tidak akan melebihi 13 pod. Total koneksi maksimal adalah: 13 pod × 5 koneksi = 65 koneksi, jauh di bawah batas server PostgreSQL.

Tindakan Pencegahan

Prometheus Alert Rule

Tambahkan deteksi dini saturasi koneksi pada Prometheus untuk mencegah cascading failure:

groups:
- name: postgresql-alerts
  rules:
  - alert: PostgresqlConnectionSaturation
    expr: sum(pg_stat_activity_count) / sum(pg_settings_max_connections) * 100 > 80
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: "PostgreSQL connection utilization above 80%"
      description: "Current utilization is {{ $value }}%. Check for deployment spikes or leaking connection pools."

Checklist Kapasitas Rilis Produksi

  1. Rumus Kapasitas: Pastikan (Replicas + maxSurge) × PoolSize < (Postgres max_connections - 15 reserved) terpenuhi sebelum mengubah replika.
  2. Pooling Proxy: Jika skala pod melebihi puluhan replika, implementasikan PgBouncer di mode transaction pooling di antara pod dan server PostgreSQL.
  3. Graceful Shutdown: Pastikan Actix Web menangani sinyal terminasi dengan benar agar koneksi TCP database ditutup secara bersih dan tidak tertahan dalam status idle.