Aplikasi web berbasis Rust yang memanfaatkan framework asynchronous seperti Topcoat di atas Tokio runtime menawarkan performa tinggi, namun rentan mengalami penurunan throughput drastis jika terjadi thread starvation atau blocking tasks. Artikel ini membahas implementasi zero-downtime canary deployment, deteksi stall pada Tokio runtime, skrip automated rollback, dan mitigasi konfigurasi thread.

1. Endpoint Health Check dan Observabilitas Tokio Runtime

Liveness dan readiness probe tidak boleh sekadar mengembalikan respons statis HTTP 200. Health check harus merefleksikan kemampuan Tokio executor memproses asynchronous tasks tanpa terblokir. Gunakan tokio-metrics untuk mengekspos metrik runtime seperti worker thread stall dan queue depth.

use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use axum::{routing::get, Router, response::IntoResponse, http::StatusCode};

static READY: AtomicBool = AtomicBool::new(true);

async fn readiness() -> impl IntoResponse {
    if READY.load(Ordering::Relaxed) {
        // Verifikasi runtime tidak mengalami starvation
        match tokio::time::timeout(Duration::from_millis(150), async { 1 + 1 }).await {
            Ok(_) => (StatusCode::OK, "READY"),
            Err(_) => (StatusCode::SERVICE_UNAVAILABLE, "RUNTIME_STARVED"),
        }
    } else {
        (StatusCode::SERVICE_UNAVAILABLE, "DRAINING")
    }
}

#[tokio::main]
async fn main() {
    let handle = tokio_metrics::RuntimeMonitor::builder()
        .build(tokio::runtime::Handle::current());

    tokio::spawn(async move {
        for interval in handle.intervals() {
            if interval.total_noop_count > 0 {
                // Emit ke Prometheus atau logger
            }
            tokio::time::sleep(Duration::from_secs(5)).await;
        }
    });

    let app = Router::new().route("/readyz", get(readiness));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

2. Konfigurasi Kubernetes Canary Deployment

Gunakan deployment terpisah untuk versi stabil dan canary, lalu atur proporsi trafik melalui penyesuaian jumlah replika pod atau ingress weight. Konfigurasi probe berikut memastikan pod canary dilepas dari routing jika terjadi task stall.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: topcoat-app-canary
  labels:
    app: topcoat-app
    track: canary
spec:
  replicas: 1
  selector:
    matchLabels:
      app: topcoat-app
      track: canary
  template:
    metadata:
      labels:
        app: topcoat-app
        track: canary
    spec:
      containers:
      - name: web
        image: registry.internal/topcoat-app:v2.1.0
        ports:
        - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8080
          initialDelaySeconds: 2
          periodSeconds: 3
          failureThreshold: 2
          timeoutSeconds: 1
        resources:
          limits:
            cpu: "2"
            memory: 512Mi
          requests:
            cpu: "500m"
            memory: 128Mi

3. Skenario Insiden: Tokio Task Stall dan Auto-Rollback

Kesalahan fatal yang sering terjadi pada Rust asynchronous server adalah eksekusi synchronous blocking operations (seperti enkripsi CPU-bound intensif atau synchronous file I/O) di dalam async task tanpa dialihkan ke tokio::task::spawn_blocking. Hal ini mengunci worker threads, menyebabkan global queue saturation dan request timeouts.

Skrip Deteksi dan Automated Rollback

Jalankan skrip monitoring canary di pipeline CD. Jika endpoint mengembalikan status non-200 atau latensi melebihi batas toleransi, skrip secara otomatis menurunkan replika canary ke nol.

#!/usr/bin/env bash
set -euo pipefail

CANARY_URL="http://canary.internal.domain/readyz"
DEPLOYMENT_NAME="topcoat-app-canary"
NAMESPACE="production"
MAX_RETRIES=5
FAILURE_COUNT=0

echo "Starting Canary health evaluation..."

for i in $(seq 1 $MAX_RETRIES); do
  STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 1 "$CANARY_URL" || echo "000")
  
  if [ "$STATUS_CODE" != "200" ]; then
    FAILURE_COUNT=$((FAILURE_COUNT + 1))
    echo "Warning: Canary probe returned $STATUS_CODE (attempt $i)"
  fi
  sleep 2
done

if [ "$FAILURE_COUNT" -ge 2 ]; then
  echo "Threshold breached. Triggering automatic canary rollback..."
  kubectl scale deployment "$DEPLOYMENT_NAME" --replicas=0 -n "$NAMESPACE"
  exit 1
fi

echo "Canary evaluation successful. Safe to promote."
exit 0

4. Analisis Postmortem dan Konfigurasi Tokio Mitigation

Root Cause

Trace log menunjukkan task sinkronus memonopoli pool default tokio::runtime. Default worker threads sama dengan jumlah logical core CPU sistem. Di lingkungan container terbatas (misal 2 vCPU), dua operasi blocking yang berjalan bersamaan akan menghentikan seluruh pemrosesan event I/O (epoll/kqueue).

Mitigasi Kode dan Konfigurasi

1. Pisahkan komputasi blocking ke threadpool terpisah:

// SALAH: Memblokir Tokio worker thread
let hash = bcrypt::hash(password, 12)?;

// BENAR: Menggunakan threadpool khusus blocking
let hash = tokio::task::spawn_blocking(move || {
    bcrypt::hash(password, 12)
}).await??;

2. Konfigurasi runtime secara eksplisit saat inisialisasi pada container lingkungan produksi:

fn main() {
    tokio::runtime::Builder::new_multi_thread()
        .worker_threads(4) // Tentukan explisit alih-alih fallback ke core node
        .max_blocking_threads(512) // Batas untuk spawn_blocking
        .thread_keep_alive(Duration::from_secs(60))
        .enable_all()
        .build()
        .unwrap()
        .block_on(async_main());
}

Konfigurasi eksplisit ini mencegah under-allocation threads ketika aplikasi berjalan di bawah cgroups container Kubernetes dan mengisolasi overhead background workers dari alur request I/O utama.