Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Retry storm: лавина повторных запросов

Retry storm: как повторные попытки добивают упавший сервис

Retry storm: умножение повторов по слоям, экспоненциальный backoff с джиттером, бюджет повторов и правила, когда повторять нельзя.

Прямой ответ

Retry storm возникает, когда повторные попытки многократно усиливают нагрузку на сервис, который уже не справляется. Сервис замедляется, клиенты получают таймауты, повторяют запросы, нагрузка растёт, и восстановление становится невозможным.

Опасность в умножении по слоям:

клиент          3 попытки
  шлюз          3 попытки      → 9
    сервис A    3 попытки      → 27
      сервис B  3 попытки      → 81

Три уровня с тремя попытками дают восемьдесят один запрос на один исходный. Именно поэтому правило «повторять на каждой ошибке» на каждом слое приводит к катастрофе при первом же сбое.

Три обязательных элемента корректного повтора:

Элемент Зачем
Экспоненциальный backoff Увеличивает паузу между попытками, давая системе восстановиться
Джиттер Разносит повторные попытки клиентов, убирая синхронные волны
Бюджет повторов Ограничивает долю повторных запросов от общего числа

Реализация backoff с полным джиттером:

import random, asyncio

async def call_with_retry(fn, attempts=4, base=0.1, cap=5.0):
    for i in range(attempts):
        try:
            return await fn()
        except (TimeoutError, TransientError):
            if i == attempts - 1:
                raise
            sleep = random.uniform(0, min(cap, base * 2 ** i))   # full jitter
            await asyncio.sleep(sleep)

Полный джиттер — случайное значение от нуля до вычисленной задержки — на практике работает лучше фиксированной экспоненты, поскольку полностью убирает синхронность клиентов.

Бюджет повторов ограничивает долю повторов:

class RetryBudget:
    def __init__(self, ratio=0.1, window=100):
        self.ratio, self.window = ratio, window
        self.total = self.retries = 0

    def allow(self) -> bool:
        self.total += 1
        if self.retries < self.ratio * self.total:
            self.retries += 1
            return True
        return False

Если повторов уже 10 % от общего числа, следующие не выполняются. Это защищает систему в момент массового сбоя, когда повторы бесполезны.

Практика: правила корректного повтора

1. Повторяйте только на переходных ошибках. Таймаут, отказ соединения и коды 502, 503, 504 — переходные. Коды 400, 401, 404, 422 означают, что запрос неверен, и повтор не поможет.

2. Не повторяйте на 429 без учёта Retry-After. Ответ 429 прямо сообщает о перегрузке; немедленный повтор усиливает её. Заголовок Retry-After обязателен к учёту — о сбросе нагрузки в статье про load shedding.

3. Разрешайте повторы только на одном слое. Если повтор выполняет клиент, сервис не должен повторять. Выбор места — архитектурное решение, которое фиксируют явно.

4. Проверяйте идемпотентность. Повтор запроса, который уже выполнился, но не вернул ответ, приводит к двойному действию. Ключ идемпотентности решает проблему; о приёмах — в статье про идемпотентность.

5. Ограничивайте общее время. Суммарное время всех попыток должно укладываться в таймаут вызывающей стороны, иначе повтор бессмысленен: клиент уже получил ошибку.

6. Сочетайте с circuit breaker. При устойчивом отказе цепь размыкается, и попытки прекращаются вовсе; о состояниях автомата — в статье про circuit breaker.

7. Измеряйте долю повторов. Метрика показывает, когда система работает на пределе; рост доли повторов предшествует отказу.

Рекомендации:

  1. Задавайте максимальное число попыток, а не только задержку. Бесконечный повтор с backoff всё равно исчерпает ресурсы.
  2. Не повторяйте при разорванной цепи. Ответ автомата «цепь разомкнута» означает немедленный отказ без обращения к сервису.
  3. Тестируйте сценарий отказа. Нагрузочный тест с отключённой зависимостью показывает, умножается ли нагрузка; о методике — в статье про Locust.
  4. Согласуйте политику повторов в контракте. Клиент должен знать, повторяет ли сервер запрос, иначе политики наложатся.
  5. Не повторяйте запись без ключа идемпотентности. Риск двойного списания выше, чем выгода от повтора.

Практический сценарий: восстановление после сбоя заняло час вместо минуты

Контекст: сервис платежей перестал отвечать из-за проблемы с сетью. После устранения причины сервис не восстановился в течение часа.

  1. Диагностика. Метрики показали, что после восстановления сети нагрузка на сервис была в восемь раз выше обычной. Сервис не справлялся и снова уходил в таймауты.
  2. Причина. Повторы выполнялись на четырёх уровнях: мобильный клиент, шлюз, сервис заказов и библиотека HTTP-клиента. Каждый слой делал по три попытки без согласования.
  3. Усугубляющий фактор. Задержка между попытками была фиксированной — одна секунда на всех уровнях, поэтому волны повторов совпадали.
  4. Действия. (а) Повторы оставили только на уровне сервисов, отключив их в клиенте и шлюзе; (б) ввели экспоненциальный backoff с полным джиттером; (в) добавили бюджет повторов в 10 %; (г) для операции списания ввели ключ идемпотентности, поскольку повтор без него был небезопасен; (д) настроили circuit breaker с размыканием после пяти последовательных отказов; (е) добавили метрику доли повторов и алерт при превышении 15 %.
  5. Результат. При следующем сбое восстановление заняло около минуты: после устранения причины нагрузка вернулась к обычной, поскольку накопленные повторы были ограничены бюджетом.

Вывод: повторные попытки на каждом слое превращают кратковременный сбой в длительный. Повтор должен быть в одном месте, с джиттером и бюджетом, иначе система не может восстановиться даже после устранения причины.

FAQ

Сколько попыток считать нормой? Две-три на одном слое. Больше имеет смысл только для коротких операций с низкой стоимостью повтора.

Почему джиттер важнее самой экспоненты? Без джиттера все клиенты повторяют одновременно, создавая синхронные волны нагрузки. Джиттер распределяет повторы по времени, что снижает пик.

Нужно ли повторять запросы на чтение и на запись одинаково? Нет. Чтение обычно идемпотентно и повторяется свободно. Запись требует ключа идемпотентности, иначе повтор может выполнить действие дважды.

Что делать вместо повтора, если операция критична? Ставить задачу в очередь с отложенным выполнением. Очередь переживает недоступность потребителя и не создаёт немедленной нагрузки; о гарантиях доставки — в статье про надёжность очередей.

Источники: AWS Builders Library (timeouts, retries, backoff with jitter), Google SRE Book, gRPC retries — ссылки в шапке.