Каскадные падения при отсутствии timeouts и решение через hierarchical timeout/deadline budget propagation. Показано четыре сценария: cascade без timeouts, наивный per-hop timeout без иерархии, правильные hierarchical timeouts с убывающим бюджетом и deadline propagation через gRPC metadata с context cancellation.
Зависший request редко выглядит как громкая ошибка. Он просто держит thread, goroutine, memory, DB connection, HTTP connection pool slot или worker. Один медленный downstream сначала портит latency, потом забивает pools, затем upstream перестает принимать новые requests. Так рождается cascading failure: пользовательский симптом в API Gateway, корневая причина в slow query или зависшем dependency на несколько hops глубже.
Timeout ограничивает ожидание одного hop. Deadline ограничивает всю цепочку до абсолютного момента времени. Разница принципиальна. Если каждый hop ставит одинаковый 30s timeout, parent может уже сдаться, а child продолжит считать ответ, который никто не прочитает. Deadline propagation говорит downstream: работа бесполезна после T, отменяйся и освобождай ресурсы.
Понимать timeouts надо до production incident, потому что defaults часто опасны. HTTP clients, database drivers, SDK и queues в разных языках имеют разные semantics: connection timeout, read timeout, idle timeout, request timeout, statement timeout. Архитектор должен задавать budget сверху вниз и проверять, что каждый layer умеет cancellation.
::concept{slug="retry-backoff"} ::concept{slug="circuit-breaker"} ::concept{slug="bulkhead"}
Timeout это таймер на локальное ожидание: я жду этот вызов максимум N milliseconds. Deadline это контракт на всю операцию: ответ должен быть готов к timestamp T. Timeout относительный и может reset-иться на каждом hop. Deadline абсолютный и должен передаваться дальше.
Представь курьера с заказом, который должен быть доставлен за 30 минут. Если каждый участник цепочки говорит «я могу держать у себя до 30 минут», заказ легко будет ехать часами. Если на накладной написано «доставить до 14
», каждый участник видит остаток бюджета и понимает, когда продолжать бессмысленно.ADR-style framing: timeouts покупают containment, но могут создавать user-visible errors. Отсутствие timeouts покупает иллюзию терпеливости, но продает capacity всей системы. В production правильный выбор почти всегда explicit budgets плюс graceful degradation: fail fast, release resources, optionally return partial response or cached data.
Диаграмма показывает client, service-a как API gateway, service-b как orders service, service-c как pricing service и Postgres со slow query. Ребра отражают обычную call chain: client -> service-a -> service-b -> service-c/db. Визуально это service mesh, где один публичный request распадается на несколько downstream calls.
Главная деталь: проблема находится в DB, но impact поднимается вверх. Без timeout service-b ждет бесконечно, service-a ждет service-b, client ждет service-a. Когда таких requests много, pools исчерпываются и healthy paths тоже начинают падать.
В правильном сценарии budget уменьшается по мере движения вниз. Client дает 30s, service-a оставляет себе margin и вызывает service-b с 25s, service-b выделяет короткий timeout на DB, leaf fails fast. В deadline propagation сценарии absolute deadline travels в metadata, а context cancellation останавливает work.
no-timeout-cascade показывает DB hang без limits. Service-b thread stuck, затем его pool заполняется, затем service-a тоже исчерпывает threads и connection slots. Урок: slow downstream может положить upstream даже без hard failure. Infinite wait это не надежность, а denial of service против самого себя.
per-hop-timeout показывает частичное улучшение. Везде есть timeout 30s, поэтому вечного hang нет. Но budget не иерархический: service-a может уже отдать 504 клиенту, пока service-b и DB еще тратят ресурсы. Урок: одинаковый timeout на каждом hop ограничивает максимум, но не устраняет wasted work.
hierarchical-timeouts показывает decreasing budgets. Leaf DB получает короткий statement_timeout, падает за 5s, service-b освобождает DB connection, service-a еще имеет время на fallback или degraded response. Урок: failure должен случаться глубоко и быстро, а не поздно и наверху.
deadline-propagation-cancellation показывает gold standard. Client задает deadline, service-a и service-b передают absolute timestamp, DB driver получает cancellation, Postgres query отменяется. Урок: deadline без cancellation неполон. Надо не только перестать ждать, но и прекратить downstream work.
ADR-001: короткий timeout против false failures. Короткий timeout защищает ресурсы и latency tail, но может отрезать legit slow operations при traffic spikes, cold cache или GC pause. Длинный timeout уменьшает false errors, но держит resources и ухудшает p99. Решение: задавать timeout от measured latency distribution, обычно выше p99 нормального режима, но ниже user-visible SLO; разделять interactive и batch endpoints.
ADR-002: per-hop timeout против end-to-end deadline. Per-hop timeout проще внедрить: каждый client ставит свой limit. Но он не знает общий budget. Deadline требует propagation через gRPC metadata, HTTP header или context, зато сохраняет целостный user SLO. В микросервисной цепочке default должен быть deadline propagation, а per-hop timeout остается local guardrail.
ADR-003: timeout против retry. Timeout без retry может вернуть ошибку на transient blip. Retry без deadline может умножить нагрузку и создать retry storm. Правильная связка: retry только idempotent operations, exponential backoff + jitter, respect remaining deadline, stop before parent budget expires. Если осталось 200 ms, новый 1s retry бессмысленен.
ADR-004: cancel downstream против let it finish. Иногда продолжить работу полезно: background warming, async report, idempotent cache fill. Но для request-bound work после deadline ответ уже никому не нужен. Default: cancel context and release scarce resources. Исключения должны быть явными и вынесены в queue/job с отдельным lifecycle.
ADR-005: slow call как failure в circuit breaker. Если breaker считает только hard errors, деградация latency не откроет circuit, хотя user experience уже сломан. Считать slow calls failures быстрее защищает систему, но может агрессивно cut off dependency. Нужны thresholds от baseline и отдельные метрики для slow-call rate.
gRPC имеет deadlines как first-class concept: client задает deadline, server получает context и может передавать его дальше. Это хороший образец propagation model.
Go context.WithDeadline и context.WithTimeout позволяют связать goroutines, HTTP clients, DB calls и cancellation. Но это работает только если код реально передает context вниз.
Envoy и Istio имеют route timeouts, per-try timeouts и retry budgets. Они помогают централизовать policy, но proxy retries должны уважать application deadline.
Postgres statement_timeout отменяет query на стороне server. Это важнее, чем просто закрыть client socket: DB перестает тратить CPU/locks на работу, результат которой уже не нужен.
AWS SDK, OkHttp, Netty, Node fetch/undici, Python requests и database drivers имеют разные defaults. Production-grade service должен явно задавать все критичные timeout knobs, а не полагаться на version-specific behavior.
HTTP client без timeout. Один зависший downstream удерживает thread or promise forever и постепенно съедает pool.
DB query без statement_timeout. Application timeout сработал, но database продолжает query и держит locks.
Child timeout больше parent timeout. Parent уже returned error, child еще работает, retry приходит сверху, нагрузка удваивается.
Timeout reset на каждом hop. End-to-end user SLO теряется, цепочка может идти дольше допустимого.
Retries игнорируют remaining budget. Три retries по 10s внутри 5s deadline только увеличивают outage.
Cancellation не проброшена. В Go забыли ctx, в Java future не отменили, в Node AbortSignal не передали, и work leaked.
Один timeout для всех operations. Read profile, write profile, payment, search, export и admin job требуют разных budgets.
Не используй жесткие короткие timeouts для offline batch, где user не ждет response и важнее завершить работу. Там нужны job deadlines, leases и checkpointing, а не interactive request budgets.
Не применяй один global timeout ко всем downstream calls. Быстрый cache lookup и cross-region payment authorization не должны иметь одинаковый budget.
Не используй timeout как замену backpressure. Если dependency перегружен, нужны bulkheads, queues, rate limits и load shedding, иначе timeout просто превращает slow failure в fast retry storm.
Не ставь deadline так коротко, что система в нормальном p95 постоянно self-aborts. Сначала измерь baseline, потом задавай SLO и budget.
Не отменяй irreversible side effects без idempotency model. Если downstream мог уже провести payment или отправить email, timeout должен вести к reconciliation, а не к слепому retry.
Следующий материал: retry-backoff. Timeout почти всегда живет рядом с retry, и неправильная комбинация опаснее, чем отсутствие retry. Затем читай circuit-breaker и bulkhead: они защищают систему, когда timeout errors становятся массовыми. Для networking полезен tcp-handshake, чтобы понимать connection/read/idle timeout distinctions.
Связанные CloudArch материалы: ::concept{slug="retry-backoff"} ::concept{slug="circuit-breaker"} ::concept{slug="bulkhead"} ::concept{slug="tcp-handshake"}
Внешние источники: Google SRE Book по handling overload и timeouts, gRPC deadline docs, Envoy timeout/retry documentation, Postgres statement_timeout, AWS Builders Library про timeouts, retries and backoff with jitter.