awesome-everything EN
↑ Обратно к восхождению

Архитектура бэкенда

Стратегии retry: backoff, jitter и thundering herd

Суть Exponential backoff без jitter создаёт синхронизированные retry storm. Full jitter размазывает retry по времени. Retry-After и retry budget не дают восстанавливающемуся сервису снова упасть.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Сервис возвращает 503. Тысяча клиентов планирует retry ровно через 2 секунды. В T+2с тысяча запросов бьёт восстанавливающийся сервер одновременно — тот самый спайк, который сломал его в первый раз. Механизм retry только что усилил outage.

Exponential backoff: базовая формула

Минимально жизнеспособный retry loop ждёт дольше перед каждой следующей попыткой:

delay = min(cap, base × 2^attempt)

base обычно 100–500 мс. cap обычно 30–60 с. Попытки ограничены 3–6 в зависимости от критичности операции.

С base = 200мс и cap = 30с:

  • Попытка 1 → ждём 200 мс
  • Попытка 2 → ждём 400 мс
  • Попытка 3 → ждём 800 мс
  • Попытка 4 → ждём 1 600 мс
  • Попытка 8 → ждём 30 с (cap)

Проблема: если 1000 клиентов упали одновременно, все выберут одну задержку и ретраят в один и тот же момент — синхронизированный спайк и есть thundering herd.

Jitter: размазываем retry по времени

Три варианта jitter (названы в AWS Architecture Blog):

Full jitter (рекомендован по умолчанию):

delay = random(0, min(cap, base × 2^attempt))

Выбирает случайное значение от 0 до экспоненциального cap. Самое плавное распределение. Проще всего реализовать.

Equal jitter:

delay = (base × 2^attempt)/2 + random(0, (base × 2^attempt)/2)

Половина детерминирована, половина случайна. Сложнее рассуждать.

Decorrelated jitter (используется в AWS SDK v3):

delay = min(cap, random(base, prev_delay × 3))

Окно каждого retry зависит от предыдущей задержки. Растёт органично. Эмпирически похож на full jitter по профилю нагрузки.

Избегай чистого exponential backoff без jitter — это учебниковая thundering-herd ловушка.

Тип jitterФормулаИспользуется в
Fullrandom(0, min(cap, base × 2^n))Большинство SDK
Equalhalf_fixed + random(0, half_fixed)Некоторые Java-библиотеки
Decorrelatedrandom(base, prev × 3) cappedAWS SDK v3
Нетmin(cap, base × 2^n)Thundering herd

Что ретраить — и что нет

Условия retry важны не меньше, чем тайминг:

Ретрай: 5xx-ошибки, network timeout, connection refused, 429 Too Many Requests (с Retry-After).

Не ретрай: 4xx-ошибки (кроме 408 и 429) — запрос клиента некорректен, retry даст тот же 4xx. 410 Gone — ресурс окончательно отсутствует.

Многие библиотеки наивно ретраят всё не-2xx, порождая retry storm на клиентские баги. Явно перечисляй условие retry в конфигурации.

Retry-After: подсказка сервера по backoff

Когда сервер возвращает 429 или 503, он может включить Retry-After: 30 (секунды) или абсолютную дату. Хорошо настроенный клиент уважает это перед применением своего backoff. gRPC использует трейлер grpc-retry-pushback-ms для того же.

Клиент, игнорирующий Retry-After и применяющий короткий собственный backoff — это thundering-herd в действии. Сервер попросил передышку, клиент отказал.

Retry budget: ограничиваем суммарный retry по call site

Индивидуальных ограничений попыток (3–6) недостаточно на масштабе. Retry budget — token-bucket допустимых retry в секунду по всему call site. Когда bucket опустел, клиент открывает circuit и возвращает детерминированную ошибку — перестаёт бить downstream.

Без retry budget outage сервиса заставляет каждого клиента сжечь свои 6 попыток одновременно, умножая трафик в 6 раз и превращая 30-секундный блип в минутный outage.

Почему это работает

Netflix и Google опубликовали tail-latency статьи, показывающие каскад от uncapped retry: медленный downstream вызывает retry, retry увеличивает нагрузку на downstream, повышенная нагрузка создаёт ещё больше медленных ответов, что создаёт ещё больше retry. Retry budget ломает этот цикл, делая rate retry жёстким ограничением, а не per-request политикой.

Викторина

Зачем добавлять jitter поверх exponential backoff?

Викторина

Сервер возвращает 503 с Retry-After: 30. Три клиента: A уважает Retry-After, B игнорирует и использует 1с backoff, C не ретраит вообще. Какой клиент ведёт себя корректно?

Расставь шаги по порядку

Поставь шаги thundering herd сценария в правильном порядке (без jitter, 1000 клиентов):

  1. 1 Все 1000 клиентов POST-ят на сервер; downstream перегружен и возвращает 503
  2. 2 Все 1000 клиентов планируют retry ровно на T + 1с (без jitter)
  3. 3 В T + 1с все 1000 ретраят одновременно — тот же спайк, что сломал сервер
  4. 4 Сервер снова возвращает 503; все 1000 планируют T + 2с
  5. 5 Паттерн повторяется бесконечно, не давая сервису восстановиться
Вспомните перед уходом
  1. 01
    Объясни, почему retry loop без jitter опаснее исходного сбоя, который он пытается восстановить.
  2. 02
    Что такое full jitter формула и что означает каждая переменная?
  3. 03
    Что такое retry budget и почему индивидуального ограничения в 6 попыток недостаточно?
Итог

Exponential backoff — min(cap, base × 2^n) — увеличивает ожидание между попытками, но без jitter синхронизирует всех клиентов на один момент и вызывает thundering herd. Full jitter добавляет random(0, ...) для равномерного размазывания. AWS SDK v3 использует decorrelated jitter с аналогичным профилем нагрузки. Ретраить только 5xx и timeout, не 4xx. Уважать заголовок Retry-After из ответов 429/503. Ограничить попытки 3–6 на запрос и применять retry budget (token-bucket) по call site, чтобы не дать failing downstream умереть от каждого retry-цикла.

Связанные уроки
встречается в185
Продолжить восхождение ↑Outbox и inbox: effectively-once через dual-write границу
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.