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

Распределённые системы

Усиление ретраев: как 3 повтора на слой превращаются в метастабильную аварию

Суть Наивный ретрай на каждом слое глубокой цепочки вызовов множит нагрузку геометрически — один запрос становится 3^глубина вызовами к бэкенду — и сам трафик ретраев держит систему лежащей долго после того, как триггер ушёл. Фиксы: бэкофф с jitter, retry budget и circuit breaker.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 17 min

Failover базы занимает 8 секунд. Мелочь — клиенты просто ретраят. Но ретраит каждый слой: API-шлюз повторяет вызов сервиса 3 раза, сервис повторяет слой данных 3 раза, слой данных повторяет пул соединений 3 раза. Failover завершается на 8-й секунде. Авария — нет. Бэкенд завален потоком ретраев, прилетевших разом, таймаутится и порождает ещё ретраи. Через сорок минут кто-то понимает: триггер ушёл полчаса назад — система лежит только из-за самих ретраев.

Множение геометрическое, а не линейное

Ретрай кажется бесплатным. Интуиция говорит «отправь ещё раз, один лишний вызов». Эта интуиция верна только для листа. В слоёной системе у каждого хопа своя политика ретраев, и политики композируются умножением. Если цепочка вызовов глубиной 4 сервиса и каждый слой повторяет 3 раза при ошибке, один пользовательский запрос, упавший на дне, может породить до 3 × 3 × 3 × 3 = 3⁴ = 81 вызова к самой глубокой зависимости. Книга Google SRE предупреждает ровно об этом эффекте накопления — вложенные ретраи умножаются, а не складываются, — и канонический пример: 3 слоя по 4 повтора, что превращает одно действие пользователя в 4³ = 64 попытки к базе.

Число геометрическое по глубине, поэтому оно подкрадывается незаметно. Два слоя по 3 ретрая — это 9, неприятно, но переживаемо. Четыре слоя — 81. Добавь пятый — 243. Коэффициент усиления — повторы^глубина, а глубина в современном микросервисном меше редко равна двум. Самое скверное: это множение включается во время инцидента — ровно тогда, когда у бэкенда меньше всего запаса, чтобы его поглотить.

Глубина цепочкиПовторов на слойВызовов к глубокой зависимостиЭффект
1 (только лист)33То, что предполагает интуиция
233² = 9В основном переживаемо
433⁴ = 811% ошибок → ~81% лишней нагрузки
533⁵ = 243Самонаведённый DDoS

Почему оно не останавливается: метастабильная авария

Глубже ловушка в том, что происходит после того, как исходная проблема залечилась. 8-секундный failover закончился. Ёмкость восстановлена. А система лежит. Это метастабильная авария — термин, который Bronson и соавторы ввели в статье HotOS 2021, чтобы объединить retry-штормы, congestion collapse и death spirals под одной рамкой. У системы два устойчивых состояния: здоровое и деградировавшее. Временный триггер (спайк, failover, короткий сбой зависимости) толкает её в деградировавшее состояние, а поддерживающая петля обратной связи держит её там, хотя триггер давно ушёл.

Ретраи — каноничная поддерживающая петля. Пройди цикл: бэкенд замедляется → запросы таймаутятся → таймауты порождают ретраи → ретраи добавляют нагрузку → бэкенд замедляется сильнее → больше таймаутов → больше ретраев. Теперь ретраи — доминирующий трафик. Работа, которую делает система, — почти целиком повторы запросов, чьи исходные дедлайны уже прошли: бесполезная работа, не дающая ничего, кроме новой нагрузки. Триггер уже неважен; петля сама себя кормит. Поэтому «подождём, восстановится» не работает, и операторам приходится сбрасывать нагрузку или перезапускать слои, чтобы разорвать цикл силой.

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

Метастабильная авария так дезориентирует в постмортеме потому, что корневая причина в таймлайне (failover) и корневая причина затянувшейся аварии (петля ретраев) — это разные вещи. Люди жгут инцидент в погоне за триггером, который уже ушёл. Честная строка постмортема: триггер запустил это, но усиление ретраев — то, что держало нас лежащими; и единственный сработавший фикс — снижение нагрузки, а не починка триггера.

Фикс 1: экспоненциальный бэкофф с jitter

Первая причина смертоносности ретраев — синхронизация. Когда зависимость моргает, все клиенты таймаутятся примерно в один момент и ретраят примерно в один момент — thundering herd, прилетающий спайком. Ретраи с фиксированным интервалом делают хуже: они ресинхронизируют стадо на каждом круге. Экспоненциальный бэкофф (жди base, потом 2×base, потом 4×base) разносит попытки во времени, но не декоррелирует их — клиенты, стартовавшие вместе, всё равно шагают в ногу.

Фикс — jitter: добавь случайность, чтобы клиенты размазались по окну. Разбор AWS в Architecture Blog — каноничная ссылка. Full jitter берёт sleep = random(0, base × 2^attempt) — максимальный разброс, меньше всего вызовов. Equal jitter держит гарантированный пол: sleep = base/2 + random(0, base/2), чтобы ни один клиент не спал меньше половины бэкоффа. AWS обнаружили, что full и equal jitter завершаются примерно за одинаковое число вызовов; full jitter делает чуть меньше работы, equal jitter избегает слишком коротких сонов. Главный итог: jitter-бэкофф резко снижает и общее число вызовов, и время восстановления против простого экспоненциального бэкоффа. Фиксированный бэкофф без jitter — то, что нельзя выкатывать никогда.

Фикс 2: retry budget и circuit breaker

Бэкофф сглаживает тайминг ретраев; он не ограничивает их объём. Это делает retry budget. Вместо счётчика повторов на запрос ты ограничиваешь ретраи как долю от всего трафика — Google SRE и Finagle оба берут ~10%: клиент может тратить ретраи лишь до 10% объёма своих запросов, а как только бюджет исчерпан — падает быстро вместо повтора. Это превращает безграничное усиление в жёсткий потолок: даже при полной аварии трафик ретраев добавит максимум 10% лишней нагрузки, а не 80×.

Circuit breaker атакует ту же проблему с другого конца. Он следит за частотой ошибок к зависимости; как только ошибки переходят порог, он открывается — каждый вызов падает мгновенно, не касаясь сети, — на время кулдауна. После кулдауна он переходит в half-open, пропуская один пробный вызов; успех закрывает его, ошибка снова открывает. Открытый breaker — то, что останавливает поддерживающую петлю: бэкенд получает окно нулевого трафика ретраев, восстанавливается, и проба плавно впускает трафик обратно. Сочетай оба с двумя нерушимыми правилами: ретрай только идемпотентных операций и retryable-ошибок (никогда не ретрай 400, никогда не ретрай вслепую неидемпотентный POST) и проброс дедлайна — передавай оставшийся бюджет времени вниз по цепочке, чтобы сервис не ретраил запрос, от которого вызывающий уже отказался. Ретрай мёртвого запроса — чистейшая форма бесполезной, усиливающей работы.

Выбери лучший вариант

Цепочка глубиной 4 сервиса схлопывается под усилением ретраев при сбоях зависимости. Выбери фикс с наибольшим рычагом.

Викторина

Запрос проходит через 4 слоя сервисов, и каждый слой повторяет 3 раза при ошибке. В худшем случае сколько вызовов достигнет самой глубокой зависимости на один упавший пользовательский запрос?

Викторина

Сбой зависимости, запустивший аварию, ушёл 30 минут назад, но система всё ещё лежит под retry-штормом. Что на самом деле держит её лежащей?

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

Расставь защиты, которые сеньор накладывает слоями, от самого широкого сокращения радиуса взрыва к самому тонкому правилу корректности:

  1. 1 Retry budget: ограничь ретраи ~10% объёма запросов, чтобы у усиления был жёсткий потолок
  2. 2 Circuit breaker: открой по порогу ошибок, чтобы дать бэкенду окно восстановления без ретраев
  3. 3 Экспоненциальный бэкофф с jitter: десинхронизируй thundering herd во времени
  4. 4 Ретрай только идемпотентных операций и retryable-ошибок — никогда 400, никогда слепой POST
  5. 5 Пробрасывай дедлайн, чтобы ни один слой не ретраил запрос, от которого вызывающий уже отказался
Вспомните перед уходом
  1. 01
    Объясни коллеге, почему система может лежать 30 минут после того, как запустивший аварию сбой уже ушёл.
  2. 02
    Почему экспоненциального бэкоффа с jitter в одиночку недостаточно, и что добавить, чтобы реально ограничить усиление?
Итог

Ретраи кажутся бесплатными, но композируются умножением, а не сложением: в цепочке глубиной 4, где каждый слой повторяет 3 раза, один упавший запрос может стать 3⁴ = 81 вызовом к самой глубокой зависимости, и усиление растёт геометрически с глубиной. Хуже того, трафик ретраев становится самоподдерживающимся — это метастабильная авария, где временный триггер толкает систему в деградировавшее устойчивое состояние, а петля обратной связи (медленно → таймаут → ретрай → больше нагрузки → ещё медленнее) держит её там долго после того, как триггер ушёл; поэтому системы лежат 30 минут после того, как вызвавшая их авария закончилась. Защиты накладываются слоями: экспоненциальный бэкофф с jitter десинхронизирует thundering herd и режет время восстановления (full или equal jitter, никогда фиксированный бэкофф); retry budget ограничивает ретраи примерно 10% объёма запросов, чтобы у усиления был жёсткий потолок; circuit breaker открывается по порогу ошибок, давая бэкенду окно восстановления без ретраев. Ограничь дальше, ретраяя только идемпотентные операции и retryable-ошибки и пробрасывая дедлайны, чтобы ни один слой не ретраил запрос, от которого вызывающий уже отказался. Цель — никогда не ноль ретраев — временные сбои реально успевают со второй попытки — а ретраи, которые не могут размножиться в шторм, кладущий тебя.

Продолжить восхождение ↑Retry amplification: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.