Kami menghadapi layanan API Rust berbasis Tokio yang mendadak mengalami latensi ekstrem dan timeout request. Kondisi ini muncul saat shutdown terjadwal, terlihat dari tracing yang memperlihatkan gorong-gorong (task) hang pada operasi I/O. Artikel ini menunjukkan langkah debugging dan solusi konkrit agar deadlock I/O tidak mengganggu layanan.
1. Gejala lapangan dan metrik observasi
Awalnya alarm datang dari latency dashboard: p95 HTTP POST ke /orders melejit dari 120 ms menjadi >3 detik, sementara p99 bahkan mencapai timeout gateway. Log tracing mencatat banyak span async yang tidak pernah selesai, dan metric tokio_task_running tetap tinggi meskipun traffic rendah.
Grafik latency terkait shutdown window—short-lived deployment—menunjukkan bahwa gejala muncul setelah signal SIGTERM diterima. Saat shutdown berjalan, request baru tetap diterima dan akhirnya timeout karena worker tidak pernah bisa menyelesaikan response. Ini menandakan deadlock di jalur I/O yang menunggu resource internal (mutex/receiver).
2. Langkah reproduksi dan data observability
2.1 Reproduksi
Reproduksi dilakukan di staging:
- Mulai layanan dengan
cargo rundan load testing sederhana (wrk 5s). - Kirim SIGTERM, tunggu sampai shutdown dimulai.
- Lanjutkan request POST selama shutdown.
- Lihat sistem tidak menutup koneksi client dan request timeout.
2.2 Data tracing/log
Tracing span dengan tokio::instrument menunjukkan semua request tergantung di blok process_request yang memanggil:
let response = channel.send(req).await?;
Log tambahan mengindikasikan mpsc sender ditutup karena worker utama sudah keluar, sehingga send().await menunggu forever jika receiver juga sedang menunggu shutdown.
3. Analisis root cause
Stack trace dari trakcing:
tokio::sync::mpsc::Sender::send
service::api::handle_request
tokio::task::harness::poll
Sentral masalah: future yang mengirim request ke worker I/O tidak pernah selesai saat worker menunggu channel close. Worker sendiri menunggu mutex guard untuk menyelesaikan shutdown graceful, menyebabkan deadlock saat SIGTERM. Ada dua faktor utama:
- Awaiting send tanpa timeout saat receiver ditutup, sehingga sender menunggu sinyal selesai dari receiver.
- Worker menunggu Mutex yang di-hold oleh task lain yang juga menunggu channel selesai.
Perhatikan bahwa Tokio tidak membatalkan send().await walau receiver drop; ia hanya mengembalikan error setelah selesai. Jika receiver diblock lagi karena mutex, maka send pendukung pun deadlock.
4. Perilaku sebelum dan sesudah perbaikan
4.1 Sebelum: send await tanpa batas
async fn handle_request(...) {
let request = RequestData::from_body(body);
// send akan menunggu hingga receiver menerima, tapi receiver hang saat shutdown
channel.send(request).await.map_err(|e| ...)?;
}
Dengan kode ini, request pending terus hingga timeout client, dan worker tidak bisa menyelesaikan shutdown karena receiver masih menunggu.
4.2 Setelah: bounded timeout dan refactor future
Solusi yang diterapkan:
- Gunakan
tokio::time::timeoutpadasend().awaitsehingga send gagal cepat saat receiver tidak merespons. - Status shutdown ditandai
AtomicBoolsehingga request baru menolak lebih awal. - Refactor worker agar tidak menunggu mutex utama saat sedang shutdown—gunakan
tokio::select!dengan channel shutdown.
Contoh perbaikan:
async fn handle_request(...) -> Result {
if SHUTTING_DOWN.load(Ordering::SeqCst) {
return Err(Error::service_unavailable());
}
let send_future = channel.send(request);
let response = tokio::time::timeout(Duration::from_millis(200), send_future)
.await
.map_err(|_| Error::timeout())?
.map_err(|_| Error::service_unavailable())?;
Ok(response)
}
Di sisi worker, shutdown channel digunakan untuk memutus loop tanpa menunggu mutex lain:
loop {
tokio::select! {
biased;
_ = shutdown.recv() => break,
Some(req) = receiver.recv() => process(req),
}
}
5. Langkah perbaikan praktis setelah debugging
- Refactor future agar tidak menunggu resource yang juga tergantung dari future lain—gunakan
tokio::select!atautokio::time::timeout. - Durasi timeout wajar pada channel send/receive agar service cepat mengembalikan error saat shutdown.
- Observability: tambahkan metric untuk
channel_send_latencydan countershutdown_rejected_requests. - Graceful shutdown: gunakan atomic flag supaya entry baru bisa ditolak saat service sedang menutup, sehingga tidak menambah beban I/O.
- Review dependency locking mencari mutex yang bisa blocking lalu diringankan atau diganti dengan struktur non-blocking.
Setelah langkah ini, latency kembali normal saat deployment, p95 tetap di bawah 200 ms, dan metric tracing menunjukkan send/receive tidak pernah pending lama. Debugging deadlock I/O membutuhkan observability yang lengkap serta refactor future agar tidak saling menunggu resource yang sama.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!