Представьте, endpoint на Axum (Rust) иногда отвечает за секунды вместо миллисекунд. Как вы будете искать причину?
Иногда медленный endpoint — сигнал периодической проблемы: connection pool exhaustion, slow query на определённых данных, блокирующий код или GC-пауза. Алгоритм: воспроизвести, добавить трейсинг по слоям, найти outlier, изолировать.
Почему «иногда» — самый сложный сценарий
Постоянно медленный endpoint диагностировать просто. «Иногда медленный» означает, что причина периодическая: высокая нагрузка, определённые данные, внешняя зависимость или состояние гонки. Алгоритм диагностики отличается.
Шаг 1 — собрать данные без изменения кода
Сначала убедитесь, что у вас есть histogramm latency (p50/p95/p99), а не только average. Average скрывает outliers. Включите TraceLayer с логированием времени ответа:
use tower_http::trace::{TraceLayer, DefaultMakeSpan, DefaultOnResponse};
use tracing::Level;
let app = Router::new()
.route("/api/items", get(list_items))
.layer(
TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::new().level(Level::INFO))
.on_response(DefaultOnResponse::new().level(Level::INFO))
);
// Теперь каждый запрос логирует: method, path, status, latency
Шаг 2 — выдвинуть гипотезы
Типичные причины периодических задержек в Axum-сервисах:
- Connection pool exhaustion — все соединения с БД заняты, хендлер ждёт освобождения.
- Slow query на определённых данных — запрос без индекса быстр на маленьких таблицах, медленен после роста данных.
- Блокирующий код в async — CPU-heavy операция или sync IO блокирует Tokio worker thread.
- Внешний HTTP-запрос без timeout — сторонний API периодически медленно отвечает.
- Memory pressure / allocator — jemalloc vs system allocator заметен при частых аллокациях.
Шаг 3 — инструментировать хендлер
use axum::{extract::State, Json};
use std::sync::Arc;
use tracing::instrument;
#[instrument(skip(state), fields(user_id))] // создаёт span для каждого вызова
async fn list_items(
State(state): State<Arc<AppState>>,
) -> Json<Vec<Item>> {
let t0 = std::time::Instant::now();
let pool_size = state.db.size(); // сколько соединений используется
let idle = state.db.num_idle(); // сколько свободных
tracing::debug!(pool_size, idle, "db pool stats");
let items = state.db.fetch_items().await; // <-- здесь может быть задержка
tracing::info!(elapsed_ms = t0.elapsed().as_millis(), "db query done");
Json(items.unwrap())
}
Шаг 4 — проверить connection pool
// sqlx: настройка pool с таймаутами и мониторингом
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(20)
.acquire_timeout(std::time::Duration::from_secs(3)) // не ждать вечно
.connect(&database_url)
.await?;
// В хендлере — логировать статус пула при медленных запросах
if pool.size() == pool.options().get_max_connections() {
tracing::warn!("connection pool exhausted");
}
Шаг 5 — проверить внешние зависимости
// Всегда используйте timeout для внешних HTTP-запросов
use std::time::Duration;
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(2)) // без этого один медленный upstream = зависший хендлер
.build()?;
let resp = client.get("https://api.example.com/data").send().await?;
Шаг 6 — load test для воспроизведения
Используйте drill или oha для нагрузочного теста: запустите 100 concurrent requests и посмотрите, при каком уровне concurrency появляются outliers. Это укажет на ресурс под давлением.
# oha — простой HTTP benchmarking tool на Rust
oha -n 1000 -c 50 http://localhost:3000/api/items
# Смотрим: p99 latency и distribution графиком
Подводные камни
- Average latency скрывает проблему: всегда смотрите p99/p999 — один медленный запрос из 100 не виден в average, но убивает пользовательский опыт.
- Tokio runtime не имеет вытесняющей многозадачности: одна задача, захватившая CPU на 100ms, задерживает все остальные на том же thread — профилируйте с tokio-console.
- sqlx acquire_timeout по умолчанию 30 секунд: если не переопределить, исчерпание пула выглядит как очень медленный ответ, а не ошибка.
- reqwest без timeout: отсутствие таймаута на внешний запрос — самая частая причина периодических зависаний в production.
- Трейсинг добавляет overhead: включение подробного трейсинга в production может само по себе замедлить сервис — используйте sampling (например, 1% запросов).
- Не коррелировать с деплоями: периодическая деградация после деплоя — первый признак регрессии производительности; сравните метрики до и после.
- Игнорировать allocator: Rust по умолчанию использует system allocator; для высоконагруженных сервисов jemalloc может дать 10–20% прирост и снизить latency spikes.
- Flame graph только в idle: запускайте профилировщик под нагрузкой, иначе hot path не проявится — используйте cargo-flamegraph с параллельными нагрузочными тестами.
What hurts your answer
- Сразу обвинять Axum (Rust), не проверив соседние слои системы
- Чинить симптом без минимального воспроизведения и evidence
- Не учитывать версии, конфигурацию, окружение и recent changes
What they're listening for
- Умеет локализовать проблему вокруг Axum (Rust)
- Двигается от симптома к гипотезам и проверкам
- Отличает баг инструмента от ошибки использования или окружения