Представьте, 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)
- Двигается от симптома к гипотезам и проверкам
- Отличает баг инструмента от ошибки использования или окружения