Menjalankan kode synchronous yang memakan waktu di dalam async handler Actix Web menyebabkan worker thread starvation. Permasalahan ini sering tidak terdeteksi saat pengujian beban rendah, tetapi memicu cascading failure pada trafik produksi tinggi.
Anatomi Masalah: Arsitektur Worker Actix Web
Secara default, Actix Web mengalokasikan satu worker thread per core CPU logis menggunakan tokio::task::LocalSet. Setiap worker menjalankan single-threaded event loop yang menangani koneksi secara kooperatif. Desain ini mengharapkan setiap future menyerahkan kendali kembali ke runtime (melalui .await) dalam durasi mikrodetik.
Ketika handler mengeksekusi operasi synchronous CPU-intensive (seperti kalkulasi hash Argon2/Bcrypt) atau synchronous file/network I/O langsung di dalam async handler, worker thread tersebut ditahan secara eksklusif. Worker tidak dapat memproses .poll() untuk request lain yang sudah antre di thread yang sama.
Gejala Insiden di Lingkungan Produksi
Insiden worker starvation memiliki karakteristik metrik yang khas:
- Latensi p99 dan p999 Melonjak: Endpoint ringan seperti
/healthzmembutuhkan detik hingga puluhan detik untuk merespons jika dialokasikan ke worker yang terblokir. - HTTP 504 Gateway Timeout: Reverse proxy (Nginx, Envoy, atau AWS ALB) memutus koneksi klien karena Actix Web tidak menyelesaikan handshake HTTP dalam batas timeout.
- Penggunaan CPU Tidak Merata: Sistem dengan 16 core bisa saja hanya menunjukkan pemakaian CPU 25-50%, tetapi server menolak atau menunda request konkuren baru. Worker yang terblokir sibuk pada loop komputasi, sementara request menumpuk di antrean TCP backlog.
Metode Investigasi dan Profiling Runtime
Identifikasi worker starvation membutuhkan metrik latensi task scheduler, bukan sekadar durasi total HTTP request.
1. Deteksi Stall dengan Tracing Tokio Console
Integrasikan console-subscriber pada inisialisasi aplikasi untuk memantau waktu polling future:
// Inisialisasi pada main.rs untuk profiling lokal
console_subscriber::init();
Metrik busy_duration yang mendekati total duration tanpa jeda idle mengindikasikan task synchronous menahan runtime tanpa yield.
2. Profiling CPU Flamegraph
Gunakan cargo flamegraph atau perf pada Linux untuk melihat call stack thread worker:
cargo flamegraph --bin my-actix-service -- --bench
Jika visualisasi didominasi oleh fungsi kriptografi (seperti argon2::hash atau bcrypt::hash) langsung di dalam frame pemanggilan actix_net::service, thread pool worker Actix terkonfirmasi mengalami blocking langsung.
Solusi: Isolasi Task Menggunakan actix_web::web::block
Operasi synchronous harus dipindahkan keluar dari worker event loop ke thread pool blocking khusus yang disediakan Tokio runtime menggunakan abstraksi actix_web::web::block.
Implementasi Bermasalah (Anti-Pattern)
use actix_web::{post, web, HttpResponse, Responder};
#[post("/auth/hash")]
async fn hash_password(payload: web::Json<String>) -> impl Responder {
// BUG: Memblokir worker thread selama ~300ms
let hash = bcrypt::hash(payload.as_str(), 12).unwrap();
HttpResponse::Ok().body(hash)
}
Implementasi Perbaikan dengan Error Handling
Fungsi web::block mengeksekusi closure di worker pool blocking terpisah dan mengembalikan future yang menghasilkan Result<T, BlockingError>:
use actix_web::{error, post, web, Error, HttpResponse};
#[post("/auth/hash")]
async fn hash_password(payload: web::Json<String>) -> Result<HttpResponse, Error> {
let raw_password = payload.into_inner();
// Offload komputasi berat ke dedicated blocking thread pool
let hash = web::block(move || {
bcrypt::hash(&raw_password, 12)
})
.await
.map_err(|blocking_err| {
// BlockingError terjadi jika runtime pool dibatalkan atau thread panik
log::error!("Blocking thread pool error: {blocking_err}");
error::ErrorInternalServerError("Task execution failed")
})?
.map_err(|hash_err| {
// Error operasional spesifik dari library bcrypt
log::error!("Bcrypt calculation error: {hash_err}");
error::ErrorBadRequest("Invalid hashing payload")
})?;
Ok(HttpResponse::Ok().body(hash))
}
Trade-off dan Limitasi web::block
Meskipun web::block menyelesaikan starvation pada HTTP worker thread, ada batas kapasitas yang perlu diperhatikan:
- Kapasitas Thread Pool Blocking: Tokio blocking pool memiliki batas maksimum default (512 thread). Memindahkan ribuan kalkulasi berat ke
web::blocksecara bersamaan tetap memicu antrean pada thread pool blocking, menyebabkan lonjakan penggunaan memori (RAM) akibat stack thread OS. - CPU Saturation Tetap Terjadi: Jika beban komputasi murni melampaui jumlah core fisik, OS context switching antar-thread OS tetap akan meningkatkan waktu eksekusi total.
- Kapan Harus Menggunakan Task Queue: Gunakan
web::blockuntuk kalkulasi CPU jangka pendek (di bawah 500ms) seperti hashing kredensial atau kompresi payload kecil. Untuk komputasi di atas 1 detik (rendering file, machine learning inference, pengolahan video), delegasikan beban ke antrean terpisah via broker pesan (Redis Streams, RabbitMQ, atau Kafka).
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!