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', регулярно ганяйте хаос-тести.