Короткий ответ
Универсального набора значений тайм-аутов и retry-политики не существует: правильные значения зависят от SLA каждого звена цепочки вызовов, допустимой задержки для конечного пользователя и поведения downstream-сервиса под нагрузкой; решение требует обсуждения компромисса между быстрым восстановлением отдельного запроса (retry) и риском усугубить перегрузку уже деградировавшего сервиса волной повторных запросов.
Как это работает подробнее
Каскадные таймауты возникают, когда задержка на одном уровне стека вызовов накапливается и превышает тайм-аут вызывающей стороны, которая, не дождавшись ответа, либо сама падает по тайм-ауту, либо (что хуже) повторяет запрос; если у каждого уровня стека независимые retry без учёта общего бюджета времени и без ограничения параллелизма, суммарное число запросов к уже перегруженному сервису может экспоненциально вырасти (retry storm), окончательно его обрушивая, даже если изначальная деградация была незначительной.
Пример кода
Пример бюджета тайм-аутов в цепочке A -> B -> C:
- Клиент ожидает ответ A максимум 2000 мс
- A вызывает B с тайм-аутом 1200 мс, max 2 retry с backoff (100мс, 300мс) + jitter
- B вызывает C с тайм-аутом 600 мс, max 1 retry, circuit breaker открывается
при error rate > 50% за 10 секундЧто использовать на практике
- Практика. применяют комбинацию приёмов: согласованные по всей цепочке тайм-ауты с запасом на каждом уровне (а не одинаковые значения везде), ограниченное число повторов с exponential backoff и джиттером, circuit breaker, который временно прекращает попытки к явно деградировавшему сервису вместо того, чтобы продолжать его бомбардировать, бюджет повторов на уровне всего запроса (а не независимо на каждом хопе), а также изоляцию пулов соединений/потоков (bulkhead), чтобы медленный downstream не исчерпывал общий пул ресурсов, блокируя вызовы к другим, здоровым сервисам.
- Компромисс. При деградации C circuit breaker в B должен открыться и начать быстро отклонять вызовы без реальных попыток к C, что проверяется нагрузочным тестом с искусственно замедленным C и замером, не растёт ли при этом суммарное число запросов к C сверх ожидаемого бюджета.
Подводные камни
- Риск. настраивать retry на каждом уровне независимо без координации, из-за чего один исходный запрос превращается в десятки фактических попыток к перегруженному сервису; выставлять одинаковые тайм-ауты на все уровни цепочки, что не даёт вышестоящим вызовам запаса на собственную обработку ответа; не реализовывать circuit breaker, из-за чего система продолжает ретраить заведомо недоступный сервис вместо быстрого отказа (fail fast).