Galat 502 Bad Gateway saat proses rolling deployment umumnya terjadi karena proses aplikasi mati mendadak sebelum koneksi aktif (in-flight requests) selesai diproses. Masalah ini diperparah ketika reverse proxy atau load balancer (seperti Nginx atau Kubernetes Ingress) masih merutekan lalu lintas baru ke instance yang sedang dalam proses terminasi.
Untuk mencapai zero-downtime deployment, Actix Web perlu dikonfigurasi agar menangani sinyal terminasi sistem operasi, menolak lalu lintas baru melalui readiness probe, menyelesaikan request yang sedang berjalan, dan menutup koneksi database secara terkontrol.
Mekanisme Shutdown Default Actix Web
Secara default, HttpServer::run() menangani sinyal SIGINT (Ctrl+C) dan SIGTERM. Saat sinyal diterima, runtime Actix Web melakukan langkah berikut:
- Berhenti menerima koneksi TCP baru pada port yang di-bind.
- Mengirim notifikasi ke semua worker thread.
- Menunggu worker menyelesaikan in-flight requests hingga batas waktu yang ditentukan.
- Memutus koneksi paksa jika batas waktu terlampaui.
Batas waktu default ini dikendalikan oleh method shutdown_timeout() yang bernilai 30 detik secara default. Jika aplikasi Anda menjalankan background task atau query database yang membutuhkan waktu lebih lama, nilai ini wajib disesuaikan:
HttpServer::new(|| { /* ... */ })
.shutdown_timeout(60) // Durasi dalam detik
.bind(("0.0.0.0", 8080))?
.run()
.await?;Mengapa Default Shutdown Masih Menghasilkan Galat 502?
Pada arsitektur kontainer (seperti Kubernetes), penghapusan Pod dan pembaruan aturan routing iptables/IPVS berjalan secara asinkron. Siklusnya adalah sebagai berikut:
- Kubelet mengirim sinyal
SIGTERMke kontainer. - Secara paralel, Endpoint controller menghapus IP Pod dari service endpoints.
Jika aplikasi langsung mematikan socket penerima (perilaku default run()), request baru yang dikirim load balancer dalam rentang jeda propagasi IP (biasanya 1–5 detik) akan menerima respons ECONNREFUSED atau 502 Bad Gateway.
Solusinya adalah mengabaikan penanganan sinyal bawaan Actix Web menggunakan disable_signals(), lalu menangani siklus hidup aplikasi secara manual: set status readiness probe menjadi gagal (503), tunggu jeda propagasi proxy, panggil graceful stop pada server, dan tutup resource eksternal seperti SQLx pool.
Implementasi Kode: Manual Signal Handling dan Resource Cleanup
Contoh berikut mendemonstrasikan implementasi manual signal handling menggunakan tokio::signal, readiness state via AtomicBool, dan penutupan sqlx::PgPool.
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
struct AppState {
is_ready: Arc<AtomicBool>,
db: PgPool,
}
async fn readiness_probe(data: web::Data<AppState>) -> impl Responder {
if data.is_ready.load(Ordering::Relaxed) {
HttpResponse::Ok().body("READY")
} else {
HttpResponse::ServiceUnavailable().body("DRAINING")
}
}
async fn long_running_task(data: web::Data<AppState>) -> impl Responder {
// Simulasi in-flight request yang memakan waktu
let _ = sqlx::query("SELECT 1").execute(&data.db).await;
sleep(Duration::from_secs(5)).await;
HttpResponse::Ok().body("Task Completed")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let is_ready = Arc::new(AtomicBool::new(true));
// Inisialisasi DB pool minimal (gunakan connection string valid di environment Anda)
let db_pool = PgPoolOptions::new()
.max_connections(5)
.connect_lazy("postgres://postgres:[email protected]:5432/app")
.expect("Failed to create pool");
let app_state = web::Data::new(AppState {
is_ready: is_ready.clone(),
db: db_pool.clone(),
});
let server = HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
.route("/ready", web::get().to(readiness_probe))
.route("/work", web::get().to(long_running_task))
})
.disable_signals() // Serahkan kontrol sinyal ke Tokio
.shutdown_timeout(15)
.bind(("0.0.0.0", 8080))?
.run();
let server_handle = server.handle();
// Task terpisah untuk mendengarkan sinyal OS
let is_ready_flag = is_ready.clone();
tokio::spawn(async move {
# [cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM");
let mut sigint = signal(SignalKind::interrupt()).expect("Failed to register SIGINT");
tokio::select! {
_ = sigterm.recv() => {},
_ = sigint.recv() => {},
};
}
# [cfg(not(unix))]
{
tokio::signal::ctrl_c().await.expect("Failed to listen for ctrl_c");
}
// 1. Ubah probe ke 503 agar Load Balancer berhenti mengirim traffic baru
is_ready_flag.store(false, Ordering::SeqCst);
// 2. Beri jeda propagasi network/iptables (2-5 detik di K8s)
sleep(Duration::from_secs(3)).await;
// 3. Trigger graceful stop Actix Web (true = hentikan worker secara graceful)
server_handle.stop(true).await;
});
// Tunggu server berhenti sepenuhnya
server.await?;
// 4. Tutup pool koneksi database setelah server berhenti menerima/memproses request
db_pool.close().await;
Ok(())
}Uji Verifikasi Request Draining
Uji apakah implementasi berfungsi tanpa memutuskan koneksi aktif saat proses dihentikan:
- Jalankan aplikasi:
cargo run - Buka terminal kedua, kirim request yang membutuhkan waktu eksekusi (5 detik):
curl -i http://127.0.0.1:8080/work - Segera kirim sinyal
SIGTERMke proses aplikasi dari terminal ketiga:kill -15 $(pgrep <binary_name>) - Periksa status probe pada terminal ketiga:
Endpointcurl -i http://127.0.0.1:8080/ready/readyakan mengembalikan status503 Service Unavailable. - Lihat kembali terminal kedua: request
/worktetap selesai dengan respons200 OKtanpa terputus di tengah jalan.
Ringkasan Konfigurasi Produksi
Saat men-deploy konfigurasi ini ke Kubernetes, pastikan parameter berikut sinkron:
- readinessProbe: Arahkan ke endpoint
/readydengan interval kecil (misalperiodSeconds: 2). - initialDelay / sleep: Durasi
sleepsebelumserver_handle.stop(true)harus cukup memberi waktu bagi Kubelet menghapus Pod dari kube-proxy. - terminationGracePeriodSeconds: Set di spec Pod lebih besar dari total durasi (sleep propagasi +
shutdown_timeout+ waktu close resource), misalnya 45-60 detik, guna menghindariSIGKILLprematur.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!