Runtime asynchronous Rust seperti Tokio mengandalkan model cooperative scheduling. Task yang berjalan di dalam worker thread diasumsikan melepaskan kontrol secara sukarela melalui .await. Ketika komputasi CPU-heavy dieksekusi langsung di dalam async task tanpa titik yield, worker thread runtime akan terblokir. Kondisi ini memicu runtime starvation: task I/O lain pada thread yang sama tertunda, queue epoll/kqueue lambat dikosongkan, dan latency P99 melonjak drastis.
Akar Masalah: Concurrency vs Parallelism di Rust
Concurrency berkaitan dengan struktur aplikasi dalam menangani banyak hal sekaligus (misal: menangani ribuan koneksi TCP idle via non-blocking I/O). Parallelism berfokus pada eksekusi komputasi secara bersamaan di banyak core hardware fisik (misal: hashing kriptografi atau image processing).
Tokio didesain untuk concurrency I/O throughput tinggi dengan thread pool berukuran kecil (standarnya sama dengan jumlah core CPU logis). Menjalankan kalkulasi paralel atau CPU-bound langsung di thread pool Tokio mencampuradukkan abstraksi ini. Jika runtime memiliki 8 worker thread dan ada 8 task melakukan parsing JSON masif atau enkripsi secara sinkron, seluruh event loop lumpuh total.
Anti-Pattern: Memanggil CPU-Intensive Code di Tokio Worker
Contoh berikut mendemonstrasikan kesalahan umum: mengeksekusi fungsi komputasi berat di dalam async handler HTTP atau event loop utama.
use axum::{routing::get, Router};
use std::net::SocketAddr;
fn cpu_heavy_hashing(payload: Vec<u8>) -> [u8; 32] {
// Simulasi kalkulasi intensif yang memakan 100ms CPU time
let mut hash = [0u8; 32];
for _ in 0..5_000_000 {
hash[0] = hash[0].wrapping_add(payload.len() as u8);
}
hash
}
// ANTI-PATTERN: Memblokir worker thread Tokio
async fn bad_handler() -> &'static str {
let dummy_data = vec![1u8; 1024];
let _ = cpu_heavy_hashing(dummy_data); // Worker terblokir di sini!
"Done"
}
Dampak langsung: request I/O ringan (misalnya /healthz) yang masuk ke worker thread yang sama akan tertahan hingga kalkulasi selesai. Throughput total server anjlok dan tail latency melonjak.
Solusi 1: tokio::task::spawn_blocking
Untuk operasi sinkron yang bersifat blocking I/O (seperti membaca disk secara blocking) atau komputasi berdurasi pendek, Tokio menyediakan spawn_blocking. Task ini dieksekusi di thread pool khusus terpisah dari worker event loop.
use tokio::task;
async fn blocking_handler() -> Result<[u8; 32], task::JoinError> {
let dummy_data = vec![1u8; 1024];
// Dipindahkan ke dedicated blocking pool Tokio
let res = task::spawn_blocking(move || {
cpu_heavy_hashing(dummy_data)
}).await?;
Ok(res)
}
Batasan: Thread pool
spawn_blockingTokio secara default dapat bertumbuh hingga 512 thread (bergantung konfigurasi). Jika dibebani throughput komputasi tinggi, overhead context switching kernel OS akan meningkat, mengurangi efisiensi utilisasi CPU cache.
Solusi 2: Menjembatani Rayon via tokio::sync::oneshot
Rayon dirancang khusus untuk komputasi data-paralel dengan arsitektur work-stealing. Ukuran pool Rayon umumnya dipatok tetap sesuai jumlah core fisik. Untuk mengisolasi CPU task dari Tokio tanpa memicu thread-explosion, buat jembatan menggunakan tokio::sync::oneshot.
use tokio::sync::oneshot;
pub async fn run_on_rayon<F, R>(func: F) -> Result<R, oneshot::error::RecvError>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let (tx, rx) = oneshot::channel();
rayon::spawn(move || {
// Eksekusi di pool Rayon terisolasi
let result = func();
// Kirim hasil kembali ke runtime async Tokio
let _ = tx.send(result);
});
rx.await
}
// Handler aman
async fn rayon_handler() -> Result<String, String> {
let dummy_data = vec![1u8; 1024];
let res = run_on_rayon(move || {
cpu_heavy_hashing(dummy_data)
})
.await
.map_err(|_| "Task cancelled or Rayon pool panicked".to_string())?;
Ok(format!("Hash: {:?}", res))
}
Panduan Pemilihan Strategi
- Tetap di Tokio (In-task): Operasi murni non-blocking I/O, kalkulasi CPU trivial (< 10–50 mikrosekon).
- tokio::task::spawn_blocking: Panggilan I/O sinkron (interaksi filesystem OS standar, legacy blocking C libraries).
- Rayon + oneshot bridge: Operasi CPU terberat (kriptografi, machine learning inference, kompresi Zstd/Gzip, pemrosesan citra, sorting data masif via
par_iter).
Dampak Terhadap Karakteristik Latency
Dalam skenario pengujian dengan traffic I/O campuran (90% light I/O request, 10% CPU-heavy payload):
- Unisolated (Anti-pattern): Latency P99 pada light I/O terdegradasi sebanding dengan durasi CPU task (naik dari <5ms ke >120ms) akibat thread starvation.
- Rayon Bridge Isolation: Latency P99 light I/O tetap stabil di rentang sub-10ms karena Tokio worker thread tidak pernah terhalang kalkulasi CPU.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!