ActixSeniorExperience

Представьте, endpoint на Actix (Rust) иногда отвечает за секунды вместо миллисекунд. Как вы будете искать причину?

Spike latency в Actix-web: (1) блокирующий код в async handler — исправить через web::block; (2) исчерпание connection pool — добавить acquire_timeout и метрики; (3) tail latency внешних сервисов — явный timeout в reqwest. Воспроизводить через wrk/k6 под нагрузкой.

Классификация intermittent latency

«Иногда секунды вместо миллисекунд» — паттерн, характерный для нескольких классов проблем: resource exhaustion (пул соединений, файловые дескрипторы), блокирующий код в async runtime, GC-подобные паузы (в Rust — аллокатор при большой фрагментации), cold start эффекты или внешние зависимости с высоким tail latency.

Шаг 1 — Измерить, не гадать

Добавьте per-handler гистограмму латентности с actix-web-prom и структурированное логирование с tracing-actix-web. Без этого вы не знаете, в каком хэндлере проблема и на какой процентиль (p95/p99) она проявляется.

use tracing_actix_web::TracingLogger;
use actix_web_prom::PrometheusMetricsBuilder;

let metrics = PrometheusMetricsBuilder::new("svc")
    .endpoint("/metrics")
    .build().unwrap();

App::new()
    .wrap(metrics)
    .wrap(TracingLogger::default())
    .route("/api/orders", web::get().to(get_orders))

Шаг 2 — Проверить блокирующий код

Самая частая причина spike latency в Actix: синхронный blocking call в async handler. Tokio runtime имеет ограниченный пул потоков — один блокирующий вызов голодает остальные задачи.

// ПЛОХО: блокирует Tokio worker thread
async fn handler() -> impl Responder {
    let data = std::fs::read_to_string("/large/file").unwrap(); // блокирует!
    HttpResponse::Ok().body(data)
}

// ХОРОШО: offload в blocking thread pool
async fn handler() -> actix_web::Result<impl Responder> {
    let data = web::block(|| {
        std::fs::read_to_string("/large/file")
    }).await??;
    Ok(HttpResponse::Ok().body(data))
}

Шаг 3 — Connection pool exhaustion

При резком росте трафика пул соединений к БД исчерпывается — запросы ждут освобождения. Диагностика: метрика sqlx_pool_idle_connections или явный лог acquire timeout.

let pool = PgPoolOptions::new()
    .max_connections(20)
    .min_connections(2)
    .acquire_timeout(Duration::from_secs(5))  // явный timeout вместо бесконечного ожидания
    .idle_timeout(Duration::from_secs(600))
    .connect(&url).await?;

Шаг 4 — Внешние HTTP-сервисы

Если handler обращается к внешнему API через reqwest, проблема может быть там. Добавьте явный timeout на каждый исходящий запрос и трассировку через tracing:

let client = reqwest::Client::builder()
    .timeout(Duration::from_secs(2))
    .connect_timeout(Duration::from_secs(1))
    .build()?;

Шаг 5 — Нагрузочный тест для воспроизведения

Intermittent спайки часто не воспроизводятся при единичных запросах. Используйте wrk или k6 для имитации concurrent нагрузки:

wrk -t4 -c100 -d30s http://localhost:8080/api/orders
# смотрим Latency Distribution: если p99 >> p50 — есть tail latency проблема

Подводные камни

  • Actix-web по умолчанию не устанавливает таймаут на чтение тела запроса — медленный клиент держит соединение и worker занятым.
  • Tokio runtime по умолчанию использует числo CPU-ядер для async threads и отдельный пул для spawn_blocking — если web::block-задач слишком много, они тоже очередятся.
  • Аллокатор по умолчанию (system allocator) медленнее jemalloc при фрагментации — для high-throughput сервисов подключают jemallocator.
  • Keep-alive соединения от nginx/load balancer могут накапливаться — настройте HttpServer::keep_alive и client_disconnect_timeout.
  • Логирование само может стать бутылочным горлышком при синхронном appender — используйте tracing-subscriber с non-blocking writer.
  • Spike латентности только на первых запросах после деплоя — cold start: JIT-эффекты отсутствуют, но Linux page cache и TLS handshake вносят вклад.
  • Размер стека Tokio task по умолчанию 2 MB — при глубокой рекурсии или большом async state machine возможен stack overflow, проявляющийся как редкий crash.

What hurts your answer

  • Сразу обвинять Actix (Rust), не проверив соседние слои системы
  • Чинить симптом без минимального воспроизведения и evidence
  • Не учитывать версии, конфигурацию, окружение и recent changes

What they're listening for

  • Умеет локализовать проблему вокруг Actix (Rust)
  • Двигается от симптома к гипотезам и проверкам
  • Отличает баг инструмента от ошибки использования или окружения

Related topics