Backend

retries

retries

В распределенных системах сеть — это не надежная труба, а капризный посредник. Пакеты теряются, DNS-запросы зависают, а микросервисы перезагружаются. В таких условиях любой сетевой запрос может завершиться ошибкой. Самый интуитивный способ справиться с этим — просто отправить запрос еще раз.

Кажется, что механизм Retries (повторных попыток) — это «серебряная пуля» для обеспечения отказоустойчивости. Однако на практике неосторожное внедрение ретраев превращает кратковременный сбой в полноценный каскадный отказ всей инфраструктуры. В этой статье мы разберем, как правильно внедрять повторы, чтобы они помогали, а не добивали систему.

Почему «просто повторить» — это плохая идея?

Представьте ситуацию: ваш сервис БД начал тормозить из-за высокой нагрузки. Время отклика выросло с 100 мс до 2 секунд. Клиентские сервисы начинают получать таймауты. Если каждый из этих сервисов настроен на 3 автоматических повтора без задержки, нагрузка на БД мгновенно вырастает в 4 раза.

Это явление называется «Retry Storm» (шторм повторов). Вместо того чтобы дать базе данных время восстановиться, вы заваливаете её лавиной новых запросов. В итоге система, которая могла бы «выплыть» за 10 секунд, уходит в глубокий нокаут на час.

1. Классификация ошибок: что можно повторять, а что нельзя

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

Когда ретраи уместны:

Транзиентные (временные) ошибки. Это сбои, которые исчезнут сами по себе через миллисекунды или секунды.

    • Ошибки сети (Connection reset, Timeout).
    • HTTP 503 (Service Unavailable) — сервис временно перегружен.
    • HTTP 429 (Too Many Requests) — если заголовок Retry-After говорит нам, когда вернуться.

Ошибки инфраструктуры. Например, кратковременный сбой в DNS или перезагрузка одного из подов в Kubernetes.

Когда ретраи запрещены:

  1. Клиентские ошибки (4xx). Если вы получили 400 Bad Request или 401 Unauthorized, повтор запроса с теми же данными не изменит результат. Вы просто тратите ресурсы.
  2. Неидемпотентные операции. Если вы отправляете запрос на списание денег с карты или создание заказа (POST /orders), повтор при таймауте может привести к тому, что клиент заплатит дважды.
  3. Критические ошибки конфигурации. 500 Internal Server Error часто означает баг в коде. Повтор запроса не исправит баг, а только заполнит логи многократно одной и той же ошибкой.

2. Стратегии ожидания: от фиксированного интервала к экспоненте

Если мы решили, что запрос можно повторить, встает вопрос: когда именно это сделать?

Фиксированный интервал (Fixed Interval)

Самый простой подход: ждать ровно 1 секунду и пробовать снова. Это работает в простых скриптах, но в высоконагруженных системах это создает «пульсирующую» нагрузку. Все упавшие запросы возвращаются в систему одновременно, создавая новые пики нагрузки.

Экспоненциальный бэк-офф (Exponential Backoff)

Это золотой стандарт индустрии. Интервал ожидания увеличивается в геометрической прогрессии. Например: 100мс $\to$ 200мс $\to$ 400мс $\to$ 800мс. Это дает сервису-приемнику «дышать» и время на восстановление.

Добавление «джиттера» (Jitter) — секретный ингредиент

Даже экспоненциальный бэк-офф не спасает от синхронизации. Если 1000 клиентов упали одновременно, они все будут делать повторы в одни и те же моменты времени. Чтобы разбить этот синхронный поток, добавляют Jitter — случайное отклонение от графика.

Вместо строгого wait = 2^attempt, мы используем wait = (2^attempt) + random_offset. Это размазывает нагрузку по временной шкале, превращая «пики» в ровное плато.

3. Ограничение попыток и бюджеты ретраев

Бесконечные попытки — это путь к утечке памяти и зависанию потоков. Необходимо четко определить лимиты.

  1. Max Retries. Обычно устанавливается значение от 3 до 5. Если за 5 попыток с экспоненциальной задержкой сервис не ответил, значит, проблема серьезная, и нужно возвращать ошибку пользователю.
  2. Общий таймаут (Deadline/Budget). Вместо того чтобы считать количество попыток, лучше задать общее время на операцию. Например: «я готов пытаться делать этот запрос в течение 5 секунд, сколько бы раз я ни повторял».
  3. Retry Budget. Продвинутый метод, при котором сервис разрешает повторы только для определенного процента общего трафика (например, не более 10% всех запросов могут быть ретраями). Если лимит исчерпан, все последующие ошибки возвращаются клиенту без попыток. Это защищает систему от саморазрушения при массовых сбоях.

4. Паттерн Circuit Breaker: когда пора сдаться

Ретраи работают, когда сбой кратковременен. Но если сервис «лёг» окончательно, ретраи становятся вредными. Здесь на помощь приходит Circuit Breaker (Предохранитель).

Этот паттерн работает как электрический рубильник и имеет три состояния:

  1. Closed (Закрыт). Запросы проходят в обычном режиме. Мы считаем количество ошибок.
  2. Open (Открыт). Если процент ошибок превысил порог (например, 50% за последнюю минуту), предохранитель «размыкается». Все запросы обрываются мгновенно с ошибкой, даже не пытаясь достучаться до сервиса. Это дает упавшему сервису шанс восстановиться без внешнего давления.
  3. Half-Open (Полуоткрыт). Через некоторое время предохранитель пропускает один-два «пробных» запроса. Если они успешны — возвращаемся в состояние Closed. Если нет — снова в Open.

5. Практические рекомендации по реализации

Чтобы механизм повторов не стал источником проблем, следуйте этим правилам:

  1. Логируйте попытки. Вы должны видеть в мониторинге не только общую ошибку, но и то, что запрос был успешно выполнен с 3-й попытки. Это первый сигнал о том, что система начала деградировать.
  2. Используйте заголовки. Если вы используете API, которые поддерживают Retry-After, всегда отдавайте приоритет этому значению перед своими внутренними алгоритмами.
  3. Идемпотентность через ключи. Чтобы безопасно повторять POST-запросы, внедрите Idempotency-Key (уникальный UUID запроса). Сервер должен сохранять результат обработки этого ключа, чтобы при повторном запросе просто вернуть старый ответ, а не выполнять операцию снова.
  4. Разделяйте таймауты. Таймаут на один запрос (Connect Timeout / Read Timeout) и общий таймаут на всю цепочку повторов — это разные вещи. Не путайте их.

Заключение

Retries — это инструмент управления неопределенностью. Правильно настроенный бэк-офф с джиттером и Circuit Breaker делают систему «эластичной». Однако помните: ретраи не лечат причину сбоя, они лишь маскируют его, давая время на исправление. Если ваши логи забиты успешными повторами, значит, ваша архитектура нестабильна, и вместо настройки интервалов ожидания стоит задуматься об оптимизации производительности или масштабировании ресурсов.