Timeout и circuit control
1) Зачем это нужно
Системы падают не от одного «фатального» сбоя, а от накопления задержек и «волновых» ретраев. Таймауты ограничивают время ожидания и высвобождают ресурсы, а circuit control (breaker + shedding + адаптивная конкуренция) не дает деградации распространиться по цепочке зависимостей. Цель — держать p95/p99 в целевых границах и сохранять доступность при частичных отказах.
2) Базовые определения
2.1 Виды таймаутов (L7/L4)
Connect timeout — установление TCP/TLS-соединения.
TLS/Handshake timeout — рукопожатия TLS/HTTP2 preface.
Write timeout — отправка запроса (включая тело).
Read timeout — ожидание первого байта ответа и/или всего тела.
Idle/Keep-Alive timeout — неактивное соединение.
Overall deadline — «жесткий» срок на весь запрос (end-to-end).
2.2 Бюджет таймаута (deadline budget)
Выделяем целевой `deadline_total` и делим по стадиям:- `ingress (gateway) + authZ + app + DB/cache + outbound PSP`.
- шлюз: 30 мс,
- приложение: 120 мс,
- БД: 120 мс,
- PSP: 100 мс,
- запас: 30 мс.
2.3 Propagation и отмена
`deadline`/`timeout` должен передаваться вниз по цепочке (контекст, заголовки, gRPC Deadline). При истечении — отмена фоновых операций (abort/ctx cancel), очистка блокировок/семафоров.
3) Стратегии установки таймаутов
1. Сверху-вниз: исходя из SLO и p95 — задать end-to-end deadline, затем разбить на под-таймауты.
2. Идентифицировать «дорогие» пути (загрузка файла, отчеты, внешние PSP) — отдельные более длинные, но лимитированные.
- idempotent (GET/повторы статуса) — короче, агрессивнее;
- write/денежные — чуть длиннее, но с однократным повтором и идемпотентностью.
4. Градуировать по планам/тенантам (enterprise может иметь длиннее timeout, но меньший параллелизм).
4) Circuit breaker: модели и параметры
4.1 Политики срабатывания
Failure-rate: доля ошибок ≥ X% на окне N запросов/времени.
Consecutive failures: M подряд неудач.
Slow-call rate: доля вызовов дольше порога T.
Error classes: таймауты/5xx/connection-reset → «фатальные», 4xx — не учитываем.
4.2 Состояния
Closed — пропускает все, накапливает статистику.
Open — мгновенный отказ (экономит ресурсы, не давит зависимость).
Half-open — малые «пробы» (N запросов) для «проверки воды».
4.3 Полезные дополнения
Bulkhead (шпангоуты): пул потоков/соединений на зависимость, чтобы одна не «высосала» все.
Adaptive concurrency: автоматическое ограничение параллелизма (AIMD/Vegas-подобные алгоритмы) по наблюдаемой латентности.
Load Shedding: ранний отказ/деградация при нехватке локального ресурса (очереди, CPU, GC-паузы).
5) Взаимодействие: таймауты, ретраи, лимиты
Сначала deadline, потом ретраи: каждый повтор должен умещаться в общий дедлайн.
Backoff + jitter для повторов; уважать `Retry-After` и retry-budget.
Rate limiting: при открытом breaker — снижайте лимиты, чтобы не усиливать шторм.
Idempotency: обязательна на write-операциях (во избежание дублей при «немых» таймаутах).
Где ретраить: предпочтительно на краю (клиент/шлюз), а не глубоко внутри.
6) Практические целевые значения (ориентиры)
Публичные read API: end-to-end `200–500 мс`, read timeout `100–300 мс`.
Критичные write (платежи): `300–800 мс` e2e; внешний PSP ≤ `250–400 мс`.
Connect/TLS: `50–150 мс` (больше — проблема сети/решолвинга).
Idle: `30–90 с` (мобильные клиенты — короче, чтобы экономить батарею).
Значения корректируйте по p95/p99 и регионам.
7) Конфиги и примеры
7.1 Envoy (cluster + route, псевдо)
yaml clusters:
- name: payments_psp connect_timeout: 100ms type: STRICT_DNS lb_policy: ROUND_ROBIN circuit_breakers:
thresholds:
- priority: DEFAULT max_connections: 2000 max_requests: 2000 max_retries: 50 outlier_detection:
consecutive_5xx: 5 interval: 5s base_ejection_time: 30s max_ejection_percent: 50
routes:
- match: { prefix: "/api/v1/payments" }
route:
cluster: payments_psp timeout: 350ms # per-request deadline idle_timeout: 30s retry_policy:
retry_on: "reset,connect-failure,refused-stream,5xx,gateways"
num_retries: 1 per_try_timeout: 200ms
7.2 NGINX (периметр)
nginx proxy_connect_timeout 100ms;
proxy_send_timeout 200ms; # write proxy_read_timeout 300ms; # read (первый байт/все тело)
keepalive_timeout 30s;
send_timeout 15s;
Быстрый отказ при перегрузке limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn addr 50;
7.3 gRPC (клиент, Go-псевдо)
go ctx, cancel:= context.WithTimeout(context.Background(), 350time.Millisecond)
defer cancel()
resp, err:= client.Pay(ctx, req) // Deadline передается вниз
7.4 HTTP-клиент (Go)
go client:= &http.Client{
Timeout: 350 time.Millisecond, // общий дедлайн на запрос
Transport: &http.Transport{
TLSHandshakeTimeout: 100 time.Millisecond,
ResponseHeaderTimeout: 250 time.Millisecond,
IdleConnTimeout: 30 time.Second,
MaxIdleConnsPerHost: 100,
},
}
7.5 Resilience4j (Java, псевдо)
yaml resilience4j.circuitbreaker.instances.psp:
slidingWindowType: TIME_BASED slidingWindowSize: 60 failureRateThreshold: 50 slowCallDurationThreshold: 200ms slowCallRateThreshold: 30 permittedNumberOfCallsInHalfOpenState: 5 waitDurationInOpenState: 30s
resilience4j.timelimiter.instances.psp:
timeoutDuration: 350ms
8) Наблюдаемость и алертинги
8.1 Метрики
`http_client_requests{endpoint, status}`, `client_latency_bucket`
8.2 Трейсы
Спаны: ingress → handler → DB/Redis → внешние.
Атрибуты: `timeout_ms_target`, `circuit_state`, `queue_time_ms`.
Экземплары: привязывать пики p99 к конкретным trace-id.
8.3 Алерты
`p99_latency{critical}` > цели X минут подряд.
`timeout_rate{dependency}` скачкообразно > Y%.
Частые переходы в `open`/«флаппинг» breaker.
Рост `shed_requests_total` при высоком CPU/GC.
9) Adaptive Concurrency & Load Shedding
9.1 Идея
Автоматика снижает параллелизм при росте хвостов латентности:- AIMD: увеличивать медленно, снижать резко.
- Vegas-подобный: держать целевую очередь (queue time).
- Token-based: каждый запрос «сжигает» токен; токены выдаются по измеренной скорости.
9.2 Реализация
Локальные семафоры per-route; цель — удерживать `queue_time` ниже порога.
Глобальный «предохранитель» (предельный RPS/конкурентность) на шлюзе.
При нехватке CPU/соединений — ранний отказ до выполнения логики (429/503 с `Retry-After`).
10) Тестирование и хаос-сценарии
Latency injection: искусственно добавлять 50–300 мс на зависимость.
Packet loss/dup/drop (tc/tbf, Toxiproxy).
Knob turning: уменьшать пулы соединений, повышать нагрузку до saturation.
Kill/Degrade одну зону/шард (частичная недоступность).
Проверки: не «проваливается» ли ретрай-шторм; breaker открывается предсказуемо; не растет ли очередь.
11) Антипаттерны
Один глобальный «read timeout» без детализации connect/TLS/пер-стадия.
Отсутствие общего дедлайна → ретраи выходят за рамки SLO.
Ретраи без джиттера и без retry-budget.
«Вечные» соединения без idle-таймаутов → утечки дескрипторов.
Breaker считает 4xx как фатальные ошибки.
Нет отмены/abort → фоновые работы продолжаются после таймаута клиента.
Слишком длинные таймауты для мобильных/нестабильных сетей.
12) Специфика iGaming/финансов
Критичные write (депозиты/выводы): один короткий повтор с Idempotency-Key, затем `202 Accepted` + polling вместо бесконечных ожиданий.
PSP/банкинг: раздельные политики по провайдеру/региону (некоторые медленнее).
Ответственные платежи и лимиты: при блокировках/ревью — быстрый `423/409`, не растягивать «висящие» операции.
Отчетность/агрегации — запускать асинхронно (batch + статус-ресурс).
13) Чек-лист prod-готовности
- Определен end-to-end deadline по критическим маршрутам (GET/POST).
- Разложен бюджет по стадиям; включена propagation дедлайна.
- Конфиги connect/TLS/read/write/idle таймаутов на шлюзе и клиентах.
- Circuit breaker с порогами failure-rate и slow-call; корректная half-open логика.
- Bulkheads на зависимости; лимиты параллелизма per-route.
- Load shedding до выполнения бизнес-логики при перегрузке.
- Интеграция с ретраями: backoff+джиттер, retry-budget, уважение `Retry-After`.
- Идемпотентность write, `Idempotency-Key` и outbox для событий.
- Метрики: timeout/slow-call/состояние breaker/queue time/конкурентность.
- Хаос-тесты: инъекция задержек/потерь/сбоев, деградация зон.
- Документация для клиентов: примеры таймингов, коды ответов, советы по повторам.
14) TL;DR
Дайте каждому запросу жесткий дедлайн, разложите его по стадиям и распространяйте вниз по цепочке. Управляйте отказами через circuit breaker + bulkheads + adaptive concurrency + load shedding. Повторы — только в рамках дедлайна, с джиттером и бюджетом; write — только идемпотентные. Мерьте timeout/slow-call, состояние breaker и `queue_time`, регулярно гоняйте хаос-тесты.