Частичное замедление превратилось в полный отказ после ретраев клиентов — найдите причину и фикс
Сервис A вызывает сервис B. Одна из реплик БД у B потеряна, поэтому B замедлился и начал отдавать ошибки. Через минуты вся платформа лежала. Ниже — таймлайн инцидента с дашбордов золотых сигналов. Объясните, что превратило частичную деградацию в полный отказ, и дайте конкретный фикс, чтобы это не повторилось.
14:02 svc-B p99 latency 200ms→3s, error rate 2%→15% (потеряна одна реплика БД)
14:03 svc-A повторяет каждый упавший вызов сразу, до 3x — без backoff, без jitter
14:03 входящий QPS к svc-B: 8k → 41k (повторы наложились: A, gateway, мобилка)
14:04 пул потоков svc-B насыщен, p99 → timeout, доля ошибок → 100%
14:05 svc-C (делит пул потоков svc-A) тоже падает — каскад
14:07 реплика БД восстановилась, но трафик повторов держит svc-B на 100% ошибок
Определите причину и дайте фикс.
Это retry storm (шторм повторов). B потеряла реплику и замедлилась; A повторяла сразу без backoff и jitter, повторы наложились по слоям, и нагрузка на B подскочила в 5 раз (8k→41k QPS) — thundering herd, насытивший её и каскадом ударивший в общий пул потоков. Фикс: экспоненциальный backoff с jitter, retry budget, ограничивающий повторы, circuit breaker (открыть при ошибках, half-open для пробы), плюс timeout и bulkhead; повторять только идемпотентные вызовы.
- ✗Винить только потерю реплики, упуская усиление повторами
- ✗Чинить агрессивнее повторяя или убрав timeout, усугубляя шторм
- ✗Забыть jitter, retry budget или что повторам нужна идемпотентность
- →Почему jitter важен поверх экспоненциального backoff?
- →Чем retry budget отличается от лимита повторов на один запрос?
Разбор
Что произошло
Потеря одной реплики БД — лишь триггер, а не причина полного отказа. Причина — retry storm (шторм повторов): когда B замедлилась и начала отдавать ошибки, A повторяла каждый упавший вызов сразу, без backoff и без jitter, до 3 раз. Повторы наложились на всех слоях (мобильный клиент → gateway → A), и входящий трафик к B вырос почти впятеро — с 8k до 41k QPS. Это thundering herd: здоровый на тот момент сервис захлебнулся именно от повторов.
частичная деградация (15% ошибок) ──повторы×N──▶ 100% ошибок + каскад
Пул потоков B насытился, а так как svc-C делил пул потоков с A, отказ каскадно перекинулся на неё. Даже когда реплика вернулась в 14:07, лавина повторов удерживала B на 100% ошибок — система не могла выйти из ямы сама.
Фикс
рассинхронизировать клиентов, чтобы они не били залпом.
перестать повторять, когда доля превышена.
ошибку, периодически пробуя восстановление в состоянии half-open.
все потоки; изолировать пулы, чтобы отказ B не ронял svc-C.
- Экспоненциальный backoff + jitter — растянуть повторы во времени и
- Retry budget — ограничить повторы малой долей запросов (например < 10%) и
- Circuit breaker — при высокой доле ошибок разомкнуть цепь и быстро отдавать
- Timeout на каждый вызов + bulkhead — не давать медленной зависимости занимать
- Повторять только идемпотентные операции.