Ви дивитеся на дашборд. Зростає затримка. Декілька контейнерів перезапускаються. Потім у каналі інцидентів з’являються ті самі два слова:
«request timeout».
Хтось пропонує «просто збільшити тайм-аути», хтось інший пропонує «додати повтори». Третій—як завжди—пропонує «нескінченні повтори».
Останній варіант перетворює невелику помилку на тривалу проблему.
Що таке тайм-аути насправді (і чому це не баги)
Тайм-аут — це рішення. Це момент, коли ваша система каже: «Я більше не чекаю.» Це рішення може бути в клієнтській бібліотеці, зворотному проксі,
сервіс-меші, TCP-стеку ядра, DNS-резолвері або контрольній площині. Іноді в усіх одразу.
Помилка — вважати «тайм-аут» одним перемикачем. Такого немає. «Тайм-аут» — це сімейство дедлайнів, що взаємодіють: тайм-аут підключення, тайм-аут читання,
тайм-аут запису, тайм-аут простою, keepalive-тайм-аут, тайм-аут healthcheck-а, тайм-аут плавного завершення тощо. Кожен існує, щоб обмежити використання ресурсів
й стримувати відмову.
Якщо додати нескінченні повтори, ви знімаєте обмеження. Помилка перестає бути видимою, але навантаження не зникає — воно переміщується у черги,
пул з’єднань, стек потоків і вниз за ланцюгом сервісів. Ви також втрачаєте найважливіший сигнал під час інциденту: частоту відмов.
Ваша мета — не «без тайм-аутів». Ваша мета — «тайм-аути, що швидко відсікають безнадійні запити, повторюються тільки коли безпечно і зупиняються до того,
як система зруйнується».
Короткий жарт #1: Нескінченні повтори — як кричати «МИ ВЖЕ ДОЇХАЛИ?» у далекій поїздці. Ви не приїдете швидше; ви просто зробите всім некомфортно.
На що слід оптимізувати
- Обмежене чекання: у кожного запиту є дедлайн. Після нього відкидайте його й рухайтеся далі.
- Обмежені повтори: політика повторів — це бюджет, а не надія.
- Явний backoff і jitter: щоб не створювати синхронізовані штормові хвилі повторів.
- Усвідомлення ідемпотентності: не все можна безпечно повторювати.
- Видимість відмов: помилки мають проявлятися досить швидко, щоб спрацювала мітігація.
Цитата, яка триматиме вас у тюрі правди
Вернер Фогельс (парафраз): «Усе ламається; проєктуйте так, щоб стримувати і відновлювати відмови, а не прикидатися, що їх не буде.»
Факти та історія: як з’явилося так багато тайм-аутів
Тайм-аути в контейнеризованих системах не з’явилися тому, що інженери стали гіршими. Вони помножилися, бо системи стали більш розподіленими, багатошаровими
і залежними від мереж, які іноді поводяться як мережі.
- Поводження TCP при встановленні з’єднання завжди передбачає очікування: повторні посилки SYN і експоненційний backoff можуть перетворити «хост недоступний» на десятки секунд без тайм-ауту на рівні додатку.
- DNS-тайм-аути старші за контейнери: повтори резолвера по різних nameserver-ах можуть перевищувати терпіння вашого додатка, особливо при некоректних search-доменах.
- Рання мережа Docker швидко еволюціонувала: перехід від legacy links до user-defined bridges поліпшив DNS/дискавері, але додав нові шари, де може ховатися затримка.
- Мікросервіси примножили межі тайм-аутів: один користувацький запит може пройти 5–30 стрибків, кожен зі своїм дедлайном за замовчуванням.
- Бібліотеки повторів стали популярними після великих відмов: вони зменшили вплив транзієнтів, але також дали змогу створювати «штормові повтори» при неправильному використанні.
- Сервіс-меші нормалізували повтори і тайм-аути: меші на базі Envoy зробили політики конфігурованими, але також ускладнили питання «хто саме дав тайм-аут?».
- HTTP/2 змінив економіку з’єднань: менше з’єднань, більше мультиплексування; одне перевантажене з’єднання може посилити затримку, якщо контроль потоку невірно налаштований.
- Хмарні балансувальники стандартизували idle-тайм-аути: у багатьох середовищах за замовчуванням близько хвилини, що заважає довгим опитуванням і стрімінгу.
- Перезапуски контейнерів стали автоматичним «виправленням»: оркестратори перезапускають нездорові речі; без налаштування тайм-аутів ви отримуєте нестабільність замість відновлення.
Тема: сучасні стекі додали більше місць, де «очікування» — це політика. Ваше завдання — зробити ці політики послідовними, обмеженими і відповідними очікуванням користувачів.
Сплануйте тайм-аут: де він трапляється в системах Docker
Коли хтось каже «контейнер тайм-аутнув», запитайте: який контейнер, в якому напрямку, який протокол і який шар?
Тайм-аути групуються в кілька реальних категорій.
1) Завантаження образів і доступ до реєстру
Симптоми: деплої зависають, вузли не можуть запустити робочі навантаження, CI-запуски зависають на docker pull.
Причини: досяжність реєстру, DNS, зависання TLS-рукопотискання, проксі або втрата пакетів по шляху.
2) East-west виклики між сервісами (контейнер → контейнер)
Симптоми: періодичні 504, «context deadline exceeded» або тайм-аути на клієнті.
Причини: перевантажений апстрім, тиск conntrack, флуктуації DNS, проблеми оверлейної мережі, невідповідний MTU або шумні сусіди.
3) North-south (зовнішній вхід → сервіс)
Симптоми: балансувальник 504/499, тайм-аути проксі, завислі завантаження, падіння long-poll.
Причини: невідповідні idle-тайм-аути, буферизація проксі, повільні бекенди або великі відповіді через обмежені лінки.
4) Healthcheck-и, проби і рішення оркестратора
Симптоми: контейнери перезапускаються «рандомно», але логи показують, що сервіс був у порядку.
Причини: надто короткий тайм-аут healthcheck-а, не врахований час запуску, повільні залежності, CPU-throttling, затримки DNS.
5) Тайм-аути завершення
Симптоми: процеси «убиті», пошкоджений стан, частково записані файли, затримане дренування.
Причини: занадто короткий stop timeout, відсутність обробки SIGTERM, довгі паузи GC, блокуючий I/O, завислий NFS або повільне скидання на диск.
Швидкий план діагностики (перевірте перше/друге/третє)
Коли ви на чергуванні, не маєте часу милуватися складністю. Потрібна послідовність, яка швидко знайде вузьке місце і звузить зону ураження.
Перше: визначте межу тайм-ауту і хто чекає
- Це на стороні клієнта (логи додатка), на стороні проксі (логи ingress) або на рівні ядра (повторні SYN, DNS)?
- Це тайм-аут підключення чи читання? Вони вказують на різні відмови.
- Це один апстрім чи багато? Один вказує на локалізоване перевантаження; багато — на спільну інфраструктуру (DNS, мережа, вузол).
Друге: підтвердіть, чи це питання ємності, затримки чи відмови залежності
- Ємність: CPU-throttling, вичерпані потоки, насичення пулу з’єднань.
- Затримка: очікування дискового I/O, повторні відправки в мережі, повільний DNS.
- Відмова залежності: помилки апстріму, замасковані як тайм-аути через повтори або погані логи.
Третє: перевірте підсилення повторів
- 1% помилок апстріму створює 10x трафік через повтори?
- Чи кілька шарів роблять повтори (клієнт + sidecar + gateway)?
- Чи повтори синхронізовані і вирівняні (без jitter), створюючи хвилі трафіку?
Четверте: зупиніть кровотечу безпечно
- Зменшіть конкуренцію (rate limit, shed load).
- Вимкніть небезпечні повтори (недіпотентні операції).
- Збільшіть тайм-аути тільки коли доведете, що робота завершиться і черга не вибухне.
Практичні завдання: команди, виводи та рішення (12+)
Це «потрібні зараз відповіді» завдання. Кожне містить реалістичну команду, приклад виводу, що це означає, і рішення, яке ви приймаєте.
Виконуйте їх на хості Docker, якщо не вказано інше.
Завдання 1: Перевірте стан Docker-движка і помилки рантайму
cr0x@server:~$ sudo systemctl status docker --no-pager
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled)
Active: active (running) since Fri 2026-01-02 09:11:07 UTC; 3h 22min ago
Docs: https://docs.docker.com
Main PID: 1532 (dockerd)
Tasks: 37
Memory: 412.5M
CPU: 19min 12.320s
CGroup: /system.slice/docker.service
└─1532 /usr/bin/dockerd -H fd://
Jan 03 11:58:21 server dockerd[1532]: time="2026-01-03T11:58:21Z" level=warning msg="... i/o timeout"
Значення: Dockerd працює, але попередження на кшталт «i/o timeout» натякають на проблеми зі сховищем або мережею, що впливають на pulls/logging.
Рішення: Якщо ви бачите повторювані тайм-аути тут, розглядайте це як проблему хоста, а не баг додатка. Продовжуйте перевірки мережі/сховища.
Завдання 2: Визначте, які контейнери перезапускаються і чому
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.RestartCount}}'
NAMES STATUS RESTARTCOUNT
api-7f3c Up 2 minutes (healthy) 4
worker-2a91 Restarting (1) 10 seconds ago 19
nginx-edge Up 3 hours 0
Значення: RestartCount показує, чи тайм-аути викликають турбулентність. «Restarting» вказує на цикл падіння або невдалий healthcheck.
Рішення: Фокусуйтеся спочатку на контейнерах з найбільшим числом перезапусків; турбулентність може підсилювати тайм-аути, видаляючи потужності.
Завдання 3: Перевірте політику перезапуску та stop timeout (бюджет плавного завершення)
cr0x@server:~$ docker inspect -f '{{.Name}} Restart={{.HostConfig.RestartPolicy.Name}} StopTimeout={{.HostConfig.StopTimeout}}' worker-2a91
/worker-2a91 Restart=always StopTimeout=10
Значення: StopTimeout=10 означає, що Docker надсилає SIGTERM, а потім SIGKILL через 10 секунд.
Рішення: Якщо додатку потрібно більше часу на дренаж або скидання, збільшіть StopTimeout і виправте обробку SIGTERM; не «наполегливо повторюйте».
Завдання 4: Перевірте таймінги healthcheck-а і збої
cr0x@server:~$ docker inspect -f 'HC={{json .Config.Healthcheck}}' api-7f3c
HC={"Test":["CMD-SHELL","curl -fsS http://127.0.0.1:8080/health || exit 1"],"Interval":30000000000,"Timeout":2000000000,"StartPeriod":0,"Retries":3}
Значення: Тайм-аут healthcheck-а 2s, інтервал 30s, retries 3. StartPeriod=0 означає відсутність поблажки на запуск.
Рішення: Якщо запуск може займати 20–60s, додайте StartPeriod або збільште тайм-аут; інакше ви навчаєте оркестратор панікувати.
Завдання 5: Підтвердіть, чи це тайм-аут підключення чи читання
cr0x@server:~$ docker logs --tail 30 api-7f3c
2026-01-03T11:57:12Z ERROR upstream_call failed err="dial tcp 10.42.3.18:9090: i/o timeout"
2026-01-03T11:57:44Z ERROR upstream_call failed err="context deadline exceeded (Client.Timeout exceeded while awaiting headers)"
Значення: «dial tcp … i/o timeout» зазвичай стосується шляху підключення (досяжність мережі, SYN/ACK, фаєрвол, MTU, conntrack).
«awaiting headers» означає, що з’єднання встановлено, але сервер не відповів достатньо швидко (перевантаження, блокування, повільний I/O).
Рішення: Розділіть розслідування: тайм-аути підключення → мережа; тайм-аути заголовків/читання → апстрімна продуктивність або затримки.
Завдання 6: Виміряйте затримку DNS і збої всередині контейнера
cr0x@server:~$ docker exec -it api-7f3c sh -lc 'time getent hosts redis.default.svc'
10.42.2.9 redis.default.svc
real 0m0.412s
user 0m0.000s
sys 0m0.003s
Значення: 412ms для простого lookup — підозріло в швидкій LAN. Якщо інколи стрибає до секунд, DNS — головний підозрюваний.
Рішення: Якщо DNS повільний, не підвищуйте тайм-аути додатка перш за все; виправте шлях резолвера, кешування, search-домени або завантаження DNS-серверів.
Завдання 7: Перевірте налаштування DNS контейнера (search-домени можуть бути прихованим податком)
cr0x@server:~$ docker exec -it api-7f3c cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0
Значення: Взаємодіє вбудований DNS Docker (127.0.0.11). Опції важливі; поведінка ndots/search може викликати кілька запитів на ім’я.
Рішення: Якщо бачите довгі search-листи або високий ndots, ущільніть їх. Менше марних запитів = менше тайм-аутів.
Завдання 8: Перевірте базову досяжність мережі з контейнера до апстріму
cr0x@server:~$ docker exec -it api-7f3c sh -lc 'nc -vz -w 2 10.42.3.18 9090'
10.42.3.18 (10.42.3.18:9090) open
Значення: Порт доступний за 2 секунди прямо зараз. Якщо це час від часу не вдається, дивіться флап маршрути, перевантаження або викидання conntrack.
Рішення: Якщо стабільно вдається, але додаток тайм-аутить очікуючи заголовків, фокусуйтеся на продуктивності апстріму, а не на мережевих ACL.
Завдання 9: Перевірте втрати пакетів/повторні відправки на хості (тайм-аути люблять втрати)
cr0x@server:~$ sudo netstat -s | egrep -i 'retransmit|timeout|listen'
18342 segments retransmitted
27 TCP timeouts in loss recovery
Значення: Повторні відправки і тайм-аути в відновленні вказують на проблеми якості мережі або затори.
Рішення: Якщо retransmits зростають під час інцидентів, не «виправляйте» підвищенням тайм-аутів. Виправляйте втрати: MTU mismatch, погана NIC, перевантажений шлях, шумний сусід або перевантажений хост.
Завдання 10: Перевірте використання conntrack (класична причина дивних тайм-аутів підключення)
cr0x@server:~$ sudo sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_count = 262041
net.netfilter.nf_conntrack_max = 262144
Значення: Ви фактично на межі ліміту conntrack. Коли він повний, нові з’єднання відкидаються або поводяться неправильно, часто як тайм-аути.
Рішення: Зменшіть черговість підключень (keepalive, pooling), обережно підвищте conntrack max і припиніть повтори, що створюють шторм з’єднань.
Завдання 11: Шукайте CPU-throttling (тайм-аути, які не «повільні», а «не заплановані»)
cr0x@server:~$ docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}'
NAME CPU % MEM USAGE / LIMIT NET I/O
api-7f3c 285.12% 612MiB / 2GiB 1.2GB / 1.1GB
worker-2a91 98.44% 1.7GiB / 2GiB 88MB / 91MB
Значення: Високе завантаження CPU може проявлятися як тайм-аути запитів, бо робота ставиться в чергу за CPU.
Рішення: Якщо CPU завантажено, повтори зроблять ситуацію гіршою. Знижуйте навантаження, обмежуйте конкуренцію і масштабуйтесь або оптимізуйте.
Завдання 12: Визначте зупинки дискового I/O на хості (мовчазний генератор тайм-аутів)
cr0x@server:~$ iostat -x 1 3
Linux 6.5.0 (server) 01/03/2026 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.20 0.00 4.10 18.40 0.00 65.30
Device r/s w/s rkB/s wkB/s await svctm %util
nvme0n1 120.0 980.0 5200.0 64200.0 48.2 0.9 92.0
Значення: Високий iowait і високий await (48ms) свідчать про насичення диска. Контейнери, що чекають на диск, можуть виглядати як мережеві тайм-аути для клієнтів.
Рішення: Якщо піки await корелюють з тайм-аутами, виправте I/O: перенесіть гарячі шляхи на швидше сховище, зменшіть синхронні записи, налаштуйте логування або ізолюйте шумних сусідів.
Завдання 13: Перевірте драйвер логування контейнера і тиск логів
cr0x@server:~$ docker info --format 'LoggingDriver={{.LoggingDriver}}'
LoggingDriver=json-file
Значення: Логування json-file може стати проблемою I/O, якщо логи великі і ротація налаштована неправильно.
Рішення: Якщо диск гарячий і логи галасливі, обмежте розмір логів і налаштуйте ротацію; не заклеюйте проблему довшими тайм-аутами.
Завдання 14: Перегляньте події демону Docker у вікні інциденту
cr0x@server:~$ docker events --since 30m --until 0m
2026-01-03T11:41:02.112345678Z container die api-7f3c (exitCode=137)
2026-01-03T11:41:02.223456789Z container start api-7f3c (image=myrepo/api:latest)
2026-01-03T11:41:33.334567890Z container health_status: unhealthy api-7f3c
2026-01-03T11:42:03.445678901Z container health_status: healthy api-7f3c
Значення: exitCode=137 натякає на SIGKILL (часто через перевищення stop timeout або OOM killer). Флапи health показують межові тайм-аутні пороги.
Рішення: Якщо бачите SIGKILL — виправляйте завершення. Якщо бачите флапи здоров’я — налаштуйте таймінги healthcheck-а і дослідіть тиск ресурсів.
Завдання 15: Підтвердіть поведінку при зупинці (чи додаток обробляє SIGTERM?)
cr0x@server:~$ docker stop -t 10 api-7f3c
api-7f3c
Значення: Це запит на 10s граціозного зупинення. Якщо зазвичай потрібно довше або процес убивається, додаток не дренує швидко.
Рішення: Реалізуйте обробку SIGTERM: припиніть прийом нових запитів, дренуйте з’єднання, потім виходьте. Збільшення stop timeout — лише після того, як ви забезпечили коректний дренаж.
Завдання 16: Протестуйте затримку pull-а з реєстру явно (відокремлюйте pull-timeout від рантайм-тайм-аутів)
cr0x@server:~$ time docker pull alpine:3.19
3.19: Pulling from library/alpine
Digest: sha256:4b1d...
Status: Image is up to date for alpine:3.19
real 0m1.208s
user 0m0.074s
sys 0m0.062s
Значення: Pull зараз швидкий. Якщо деплої тайм-аутять тільки у пікові моменти, реєстр або проксі тротять, або ваш NAT перевантажений.
Рішення: Якщо pulls повільні — додайте кешування (registry mirror) або виправте egress; не «повторюйте вічно» під час деплою.
Налаштуйте повтори правильно: бюджети, backoff і jitter
Повтори не безкоштовні. Вони споживають потужності, підвищують хвостову латентність і перетворюють невеликі відсотки помилок на великі сплески трафіку.
Водночас вони — один з найкращих інструментів, коли їх обмежено і застосовано вибірково.
Мислення у термінах бюджету повторів
Почніть із простої правила: повтори повинні вміщуватися в дедлайн користувача. Якщо у запиту бюджет SLO 2s, не можна
робити три спроби по 2s. Це не стійкість; це омана з математикою.
«Бюджет» має враховувати:
- Час підключення
- Час обробки на сервері
- Час чергування на клієнті (пул потоків, асинхронні виконувачі)
- Затримку backoff між спробами
- Найгіршу мережеву варіативність
Які помилки варто повторювати?
Повторюйте тільки коли відмова ймовірно транзієнтна і операція безпечна.
- Хороші кандидати: скидання з’єднання, тимчасовий апстрім 503, деякі випадки 429 (якщо поважаєте Retry-After), DNS SERVFAIL (можливо), ідемпотентні GET-и.
- Погані кандидати: тайм-аути з невідомим станом сервера на недіємпотентних запитах (POST, що могло виконатися), детерміновані 4xx, помилки автентифікації, «payload too large».
- Складні випадки: тайм-аути читання — іноді апстрім повільний, іноді він мертвий. Повтор може подвоїти навантаження на сервіс у стресі.
Backoff і jitter: уникайте синхронізованих штормів повторів
Якщо кожен клієнт робить повтори точно через 100ms, виникне thundering herd: періодичні сплески трафіку, що триматимуть систему на межі відмови.
Використовуйте експоненційний backoff з jitter. Так, jitter здається забобоном. Це не так; це прикладена ймовірність.
Розумна політика за замовчуванням для багатьох внутрішніх RPC:
- Максимум спроб: 2–3 (включно з першою спробою)
- Backoff: експоненційний, починаючи з 50–100ms
- Jitter: full jitter або equal jitter
- Тайм-аут на спробу: менший за загальний дедлайн (наприклад, 300ms на спробу в межах 1s бюджету)
Не складайте повтори в різних шарах
Якщо ваш додаток робить повтори, ваш sidecar робить повтори, і ваш gateway теж — ви створили слот-машину. Вона в основному програє, але дуже впевнена.
Визначте один шар, який відповідає за повтори для даного шляху. Інші шари лише спостерігають і застосовують дедлайни, а не підсилюють трафік.
Чому «просто збільшити тайм-аут» часто не допомагає
Збільшення тайм-аутів може допомогти, коли у вас рідкісні, обмежені повільні операції, які завершаться, якщо трохи почекати.
Це не допомагає коли:
- Запити ставляться в чергу через перевантаження: довші тайм-аути лише дозволяють чергам розростатися.
- Залежності недоступні: ви просто довше витрачаєте потоки і сокети марно.
- Ви маскуєте мережеву «чорну діру» (MTU, фаєрвол): очікування не змінює фізику.
Короткий жарт #2: Тайм-аут — це дедлайн, а не стиль життя.
Конкретний шаблон налаштування, який працює
Для HTTP-клієнта, що викликає апстрім в межах того самого кластера/VPC:
- Загальний тайм-аут запиту: 800ms–2s (залежно від SLO)
- Тайм-аут підключення: 50ms–200ms (швидке відсікання недосяжних)
- Тайм-аут на спробу: 300ms–800ms
- Повтори: 1 повтор для ідемпотентних запитів (тому 2 спроби загалом)
- Backoff: 50ms → 150ms з jitter
- Жорсткий ліміт на одночасні in-flight запити (bulkhead)
Bulkhead — не опціонально. Повтори без обмежень конкуренції — це як самому собі влаштувати DDoS по апстріму.
Запуск, healthcheck-и та завершення: тайм-аути під вашим контролем
Більшість «тайм-аутів контейнерів», які не дають спокою вночі, самі спричинені помилками конфігурації життєвого циклу: додаток потребує часу, платформа очікує миттєво,
і всі сперечаються з графіком.
Запуск: давайте період поблажки, а не довгий повідець
Якщо сервіс завантажує моделі, прогріває кеші, виконує міграції або чекає залежності, він іноді буде повільним.
Ваше завдання — відокремити «запуск» від «нездоровий».
- Використовуйте період поблажки для запуску (Docker healthcheck StartPeriod або startup probe оркестратора).
- Робіть health-ендпоїнти дешевими і чутливими до залежностей: «я живий?» відрізняється від «я можу обслуговувати трафік?»
- Не запускайте міграції схем на кожному репліці при старті. Це не «автоматизація», а «синхронізований біль».
Healthcheck-и: маленькі тайм-аути, але не ірреальні
Тайм-аут 1–2 секунди для healthcheck-а підходить для більшості локальних ендпоїнтів — якщо контейнер має CPU і не заблокований I/O.
Але якщо ви ставите 200ms, бо «швидко — добре», ви не підвищуєте надійність. Ви підвищуєте ймовірність перезапусків при нормальних флуктуаціях.
Завершення: ставте до нього пріоритет
Docker надсилає SIGTERM, чекає, потім надсилає SIGKILL. Якщо ваш додаток ігнорує SIGTERM або блокується під час скидання логів на повільний диск, його вб’ють.
Убиті процеси рвуть запити, псують стан і спричиняють повтори від клієнтів — що виглядає як тайм-аути.
Практичні поради:
- Обробляйте SIGTERM: припиніть приймати нову роботу, дренуйте, потім виходьте.
- Встановлюйте StopTimeout, що покриває гірший випадок дренажу, але тримайте його обмеженим.
- Надавайте перевагу коротшим keepalive і тайм-аутам запитів, щоб дренаж завершувався швидше.
- Реєструйте латентність завершення: логуйте початок/кінець термінації і кількість in-flight запитів.
Проксі, балансувальники та ланцюги тайм-аутів
Запит користувача часто проходить кілька тайм-аутів:
браузер → CDN → балансувальник → ingress proxy → сервіс-меш → додаток → база даних.
Якщо ці дедлайни не вирівняні, коротший перемагає — і це може бути не той, якого ви очікували.
Як невідповідні тайм-аути створюють фантомні відмови
Приклад патерну:
- Тайм-аут клієнта: 10s
- Тайм-аут ingress proxy: 5s
- Тайм-аут додатка до БД: 8s
На 5-й секунді ingress відмовляється і закриває з’єднання з клієнтом. Додаток продовжує працювати до 8s, потім скасовує виклик до БД.
Між тим БД може все ще обробляти запит. Ви перетворили один повільний запит у марну роботу в трьох шарах.
Як виглядає правильне вирівнювання
- Зовнішні шари мають трохи більші тайм-аути, ніж внутрішні, але не радикально більше.
- Кожен хоп застосовує дедлайн і передає його вниз (заголовки, propagation контексту).
- Повтори відбуваються в одному шарі, з урахуванням ідемпотентності й бюджету.
Будьте обережні з idle-тайм-аутами
Idle-тайм-аути вбивають «тихі» з’єднання. Це важливо для:
- Server-sent events
- WebSockets
- Long-poll
- Великих завантажень, коли клієнт ставить паузу
Якщо у вас є стрімінг, явно налаштуйте idle-тайм-аути і додайте heartbeat на рівні додатка, щоб «idle» не сприймався як «мертвий».
Сховище та I/O-затримки: причина тайм-аутів, яку ніхто не хоче
Інженери люблять дебагати мережі, бо інструменти виглядають круто і графіки чіткі. Латентність сховища менш гламурна: вона проявляється як «await»
і робить усіх нещасними.
Контейнери тайм-аутять тому, що вони чекають на I/O частіше, ніж команди зізнаються. Типові винуватці:
- Лог-томи пишуть надто багато на повільні диски
- Накладення файлової системи overlay при великій кількості дрібних записів
- Збої мережевого сховища (NFS-стали, iSCSI-затор)
- Синхронно-активні бази на насичених вузлах
- Дискове обмеження на рівні вузла у віртуалізованих середовищах
Як затримка сховища стає «мережевим тайм-аутом»
Ваш обробник API записує лог, скидуючи буфер або пише в локальний кеш.
Цей запис блокується на 200ms–2s через зайнятий диск. Потік обробника не може відповісти.
Клієнт бачить «awaiting headers» і називає це мережевим тайм-аутом.
Ви виправляєте це шляхом:
- Зменшення синхронних записів у шляхах обробки запитів
- Використання буферизованого/асинхронного логування
- Розміщення stateful-навантажень на правильному класі сховища
- Відокремлення «шумних» навантажень від latency-чутливих
Три короткі історії з корпоративного життя
Коротка історія 1: Інцидент через неправильне припущення
Середня компанія запускала внутрішні сервіси на Docker-хостах з простим зворотним проксі спереду. Проксі мав upstream-тайм-аут 5 секунд.
Команди додатків припускали, що в них «10 секунд», бо їх HTTP-клієнти були налаштовані на 10 секунд. Ніхто не записав налаштувань проксі, бо «це просто інфраструктура».
Під час планового обслуговування бази даних латентність запитів зросла з <100ms до багатьох секунд. Додаток почав повертати відповіді десь у 6–8 секунд.
Клієнти терпляче чекали. Проксі — ні. Він почав розривати з’єднання на 5-й секунді, повертаючи 504.
Логи додатка були оманливі: запити «здавалися» завершеними, але клієнт уже відмовився. Деякі клієнти автоматично повторювали. Тепер той самий дорогий запит
виконувався двічі, іноді тричі. Навантаження на базу зросло, латентність зросла й ще більше запитів перетнули межу 5 секунд. Просте технічне обслуговування перетворилося на справжній інцидент.
Виправити було соромно просто: вирівняти дедлайни. Тайм-аут проксі став трохи більшим за тайм-аут DB в аплікації, а тайм-аут клієнта — трохи більшим за проксі.
Також вимкнули повтори для недіємпотентних ендпоїнтів і додали явні ідемпотентні ключі там, де потрібно. Найкраще: наступне обслуговування пройшло нудно — і це правильно.
Коротка історія 2: «Оптимізація», що відкотилася
Інша організація хотіла швидшого failover. Хтось зменшив тайм-аути викликів сервісу з 2s до 200ms і підвищив повтори з 1 до 5 «щоб компенсувати».
Здавалося розумно: швидке виявлення, кілька спроб, менше видимих помилок для користувача. У staging це навіть працювало — staging рідко має черги або реальні затори.
У production апстрім мав періодичні спади 300–600ms під час GC і оновлень кешу. Нові налаштування зробили нормальну варіативність помилкою.
Клієнти перевищували 200ms, тайм-аут, повторювалися, знову тайм-аут. Вони не падали швидше; вони падали голосніше.
Апстрім бачив не просто більше трафіку; він бачив синхронізовані сплески. П’ять повторів без jitter означали хвилі навантаження кожні кількасот мілісекунд.
CPU піднявся. Хвостова латентність погіршилася. Апстрім відстав і почав справді тайм-аутити. Тепер і апстріми, і апвістріми палили CPU на повтори і насичували пули з’єднань.
Відкат «оптимізації» стабілізував систему за кілька хвилин. Постійне виправлення — одноповтор з експоненційним backoff і jitter,
тайм-аут на спробу підганяли під спостережуваний p95, і ввели глобальний ліміт конкуренції. Також додали дашборди, що показували rate повторів і ефективне підсилення QPS.
Прихований урок: робота зі стійкістю, яка ігнорує хвости розподілу, — це оптимізм в YAML.
Коротка історія 3: Нудна, але правильна практика, що врятувала день
Фінансова команда запускала Docker-батч воркерів, що викликали зовнішнє API. API іноді тротливало з 429.
Команда мала нудну політику: поважати Retry-After, обмежувати повтори до 2 і застосовувати жорсткий дедлайн на джобу. Без героїки, без нескінченних циклів.
Одного дня зовнішній провайдер частково впав. Багато клієнтів побачили каскадні відмови, бо їх клієнти агресивно повторювали і добивали вже страждаюче API.
Ця команда воркерів сповільнилася замість того, щоб пришвидшуватися. Джоби тривали довше, але система залишалася стійкою.
Дашборди показували підвищення 429 і збільшення тривалості джобів, але черга не вибухнула. Чому? Бюджет повторів плюс backpressure.
Також був circuit breaker: після порогу невдач воркери припиняли виклики на короткий час охолодження.
Коли провайдер відновився, backlog команди очистився передбачувано. Ніякого екстреного масштабування, ніяких таємничих тайм-аутів, ніяких дебатів «треба додати ще повторів».
Нудні практики не отримують доповідей на конференціях. Вони тримають вас поза інцидент-бридами — і це краще.
Типові помилки: симптом → корінь → виправлення
1) Симптом: тайм-аути зростають саме коли апстрім сповільнюється
Корінь: повтори підсилюють навантаження; кілька шарів роблять повтори; відсутній backoff/jitter.
Виправлення: зменште кількість спроб (часто до 2 загалом), додайте експоненційний backoff з jitter, застосуйте обмеження конкуренції і забезпечте, щоб лише один шар робив повтори.
2) Симптом: “dial tcp … i/o timeout” по багатьох сервісах
Корінь: таблиця conntrack повна, втрата пакетів, MTU mismatch або фаєрвол блокує SYN/ACK.
Виправлення: перевірте завантаження conntrack, зменшіть кількість підключень через keepalive/pooling, обережно підвищте conntrack max, і перевірте MTU end-to-end.
3) Симптом: контейнери перезапускаються, а логи показують, що все «було в порядку»
Корінь: надто агресивний тайм-аут healthcheck-а; відсутній стартовий період; перевірки залежностей в liveness.
Виправлення: відокремте логіку liveness і readiness, додайте StartPeriod (або startup probe), налаштуйте тайм-аут під реалістичний p95 health-ендпоїнта під навантаженням.
4) Симптом: запити тайм-аутять під час деплоїв, а не в steady state
Корінь: незграбне завершення; stop timeout занадто короткий; дренаж не реалізовано; балансувальник і далі шле трафік термінованим задачам.
Виправлення: реалізуйте SIGTERM-drain, збільшіть StopTimeout належним чином, налаштуйте LB deregistration/drain delay і зменшіть keepalive/тайм-аути запитів, щоб виходити швидше.
5) Симптом: тайм-аути awaiting headers стають гіршими при деталізованому логуванні
Корінь: диск I/O насичений через великий обсяг логів або драйвер json-file; накладні витрати overlay filesystem.
Виправлення: обмежте/ротуйте логи, відправляйте логи асинхронно, перемістіть «гарячі» диски на SSD/NVMe, ізолюйте stateful-навантаження і вимірюйте iostat await під час інцидентів.
6) Симптом: DNS-запити інколи займають секунди
Корінь: повтори резолвера, перевантажений DNS, погана конфігурація search-доменів, вузьке місце в вбудованому DNS або випадкова втрата пакетів.
Виправлення: виміряйте latency lookup-ів всередині контейнерів, спростіть search-домени/опції, додайте кешування і забезпечте достатню пропускну здатність DNS-серверів з низькою втратою.
7) Симптом: довготривалі з’єднання розриваються через однакові інтервали
Корінь: idle-тайм-аути на балансувальнику/проксі; відсутність keepalive або heartbeat-ів.
Виправлення: вирівняйте idle-тайм-аути між шарами і додайте прикладні heartbeats для стрімінгу/WebSockets.
8) Симптом: підвищення тайм-аутів робить усе «гірше, але повільніше»
Корінь: ви перевантажені; довші тайм-аути заглиблюють черги і збільшують час утримання ресурсів.
Виправлення: скидати навантаження, зменшити конкуренцію, масштабувати потужності і вкоротити внутрішні тайм-аути, щоб відмови швидше звільняли ресурси.
Контрольні списки / покроковий план
Покроково: виправте тайм-аути контейнерів без нескінченних повторів
- Класифікуйте тайм-аут: підключення vs читання vs idle vs завершення. Використовуйте логи і метрики проксі, щоб ідентифікувати, де спрацьовує.
- Знайдіть найкоротший дедлайн у ланцюгу: CDN/LB/ingress/mesh/app/db. Найменший тайм-аут керує досвідом користувача.
- Виміряйте p50/p95/p99 латентність для шляху виклику. Налаштовуйте під хвости, а не під медіану.
- Встановіть загальний дедлайн на запит на основі SLO і UX (що користувачі витримають).
- Розділіть тайм-аути: короткий тайм-аут підключення, обмежений тайм-аут читання і загальний дедлайн.
- Виберіть одного власника повторів (клієнтська бібліотека або mesh, але не обидва). Вимкніть повтори в інших шарах.
- Обмежте повтори: зазвичай 1 повтор для ідемпотентних викликів. Більше спроб вимагають вагомих підстав і більшого бюджету.
- Додайте експоненційний backoff з jitter, завжди. Ніяких винятків для «внутрішнього» трафіку.
- Додайте bulkheads: обмежте конкуренцію і довжину черги; краще швидко відмовити, ніж дозволяти чергам рости.
- Зробіть небезпечні операції безпечними: ідемпотентні ключі для POST/PUT, де це можливо.
- Виправте таймінги життєвого циклу: StartPeriod healthcheck-а, реалістичний health timeout, і StopTimeout узгоджений з поведінкою дренажу.
- Перевірте обмеження хоста: запас conntrack, втрата пакетів, CPU-throttling, disk await.
- Доведіть, що зміна спрацювала: слідкуйте за rate повторів, підсиленням QPS апстріму, p99 латентністю і спалюванням error budget.
Швидкий чекліст: що змінити насамперед (найвищий ROI)
- Приберіть нескінченні повтори. Замініть на max attempts і дедлайн.
- Додайте jitter до будь-якого циклу backoff/повторів.
- Переконайтеся, що лише один шар робить повтори.
- Виправте StartPeriod healthcheck-а і надто короткі тайм-аути.
- Перевірте conntrack і disk await під час інцидентів.
Питання та відповіді
1) Чи варто коли-небудь використовувати нескінченні повтори?
Майже ніколи. Нескінченні повтори доречні лише в жорстко контрольованих бекґраундних системах з явним backpressure, довговічними чергами і операторською видимістю мертвих листів.
Для запитів, орієнтованих на користувача, нескінченні повтори перетворюють відмови на повільні катастрофи.
2) Скільки повторів «безпечне» для внутрішніх викликів сервісів?
Зазвичай: один повтор для ідемпотентних операцій, з backoff і jitter, і лише якщо у вас є вільна потужність. Якщо апстрім перевантажений, повтори — це не «безпечно», а бензин на вогні.
3) У чому різниця між тайм-аутом підключення і тайм-аутом читання?
Тайм-аут підключення охоплює встановлення з’єднання (маршрутизація, SYN/ACK, TLS-рукопотискання). Тайм-аут читання — очікування відповіді після встановлення з’єднання.
Тайм-аути підключення вказують на мережу/conntrack/фаєрвол. Тайм-аути читання вказують на латентність апстріму, черги або I/O-стали.
4) Чому тайм-аути зростають під час деплойтів?
Тому що неправильно обробляються shutdown і readiness. Терміновані контейнери продовжують отримувати трафік, або вони приймають запити не будучи готовими, або їх вбивають посеред обробки.
Виправляйте дренаж, stop timeout і час deregistration у балансувальника.
5) Як зрозуміти, що повтори викликають retry storm?
Шукайте стрибок апстрімного QPS, що не відповідає користувацькому трафіку, плюс підвищення error rate і p99 латентності. Також перевіряйте, чи помилки згруповані в періодичні хвилі (відсутність jitter).
Відстежуйте «спроб на запит», якщо є така метрика.
6) Чи однакові тайм-аути healthcheck-ів і тайм-аути запитів?
Ні. Healthchecks — це рішення платформи про те, вбити або маршрутизувати контейнер. Тайм-аути запитів — це рішення клієнтів про те, чи чекати.
Поганий healthcheck може виглядати як «рандомні тайм-аути», бо він видаляє потужність або перезапускає сервіс під час навантаження.
7) Чому DNS так важливий всередині контейнерів?
Бо кожен виклик сервісу починається з резолву, якщо ви не прив’язуєте IP (не робіть цього). Якщо DNS повільний, кожен запит платить цей податок, і повтори множать його.
Контейнерний DNS додає ще один шар (вбудований DNS або node-local caching), який може стати вузьким місцем.
8) Коли підвищення тайм-ауту — правильне рішення?
Коли ви довели, що робота завершується успішно з трохи більшим часом і у вас є ресурси утримувати роботу довше.
Приклад: відомий повільний, але обмежений endpoint для генерації звітів з низькою конкуренцією і коректним чергуванням.
9) Як уникнути дублювання ефектів при повторі POST-запитів?
Використовуйте ідемпотентні ключі (ідентифікатори запитів, згенеровані клієнтом і збережені на сервері), або проектуйте операцію як ідемпотентну.
Якщо не можете — не повторюйте автоматично; повідомляйте про помилку і нехай людина або джоб-система вирішують конфлікт.
10) Як запобігти каскаду тайм-аутів між сервісами?
Використовуйте дедлайни, що передаються end-to-end, bulkheads (обмеження конкуренції), circuit breakers і обмежені повтори з jitter.
Також тримайте внутрішні тайм-аути коротшими за зовнішні, щоб відмова зупинялася раніше всередині.
Висновок: практичні наступні кроки
Тайм-аути не ворог. Невизначене очікування — от ворог. Якщо хочете менше інцидентів, припиніть вважати повтори чарівним рішенням і почніть вважати їх контрольованими витратами.
- Виберіть дедлайн запиту, що відповідає реальності (SLO і терпимість користувача).
- Розділіть тайм-аути підключення/читання і логомінгуйте їх окремо.
- Обмежте повтори (звичайно один) і додайте експоненційний backoff з jitter.
- Переконайтеся, що лише один шар робить повтори; всі інші мають застосовувати дедлайни.
- Виправте налаштування життєвого циклу: StartPeriod healthcheck-а, реалістичний health timeout, плавне завершення з достатнім StopTimeout.
- Під час наступного інциденту з тайм-аутами виконайте швидкий план діагностики і практичні завдання вище — особливо conntrack, latency DNS і disk await.
Якщо ви зробите лише дві речі цього тижня: приберіть нескінченні повтори і вирівняйте тайм-аути між проксі і сервісами. Ви відчуєте різницю наступного разу, коли затримки підуть шкереберть.