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говорит нам, когда вернуться.
- HTTP 429 (Too Many Requests) — если заголовок
Ошибки инфраструктуры. Например, кратковременный сбой в DNS или перезагрузка одного из подов в Kubernetes.
Когда ретраи запрещены:
- Клиентские ошибки (4xx). Если вы получили 400 Bad Request или 401 Unauthorized, повтор запроса с теми же данными не изменит результат. Вы просто тратите ресурсы.
- Неидемпотентные операции. Если вы отправляете запрос на списание денег с карты или создание заказа (
POST /orders), повтор при таймауте может привести к тому, что клиент заплатит дважды. - Критические ошибки конфигурации. 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. Ограничение попыток и бюджеты ретраев
Бесконечные попытки — это путь к утечке памяти и зависанию потоков. Необходимо четко определить лимиты.
- Max Retries. Обычно устанавливается значение от 3 до 5. Если за 5 попыток с экспоненциальной задержкой сервис не ответил, значит, проблема серьезная, и нужно возвращать ошибку пользователю.
- Общий таймаут (Deadline/Budget). Вместо того чтобы считать количество попыток, лучше задать общее время на операцию. Например: «я готов пытаться делать этот запрос в течение 5 секунд, сколько бы раз я ни повторял».
- Retry Budget. Продвинутый метод, при котором сервис разрешает повторы только для определенного процента общего трафика (например, не более 10% всех запросов могут быть ретраями). Если лимит исчерпан, все последующие ошибки возвращаются клиенту без попыток. Это защищает систему от саморазрушения при массовых сбоях.
4. Паттерн Circuit Breaker: когда пора сдаться
Ретраи работают, когда сбой кратковременен. Но если сервис «лёг» окончательно, ретраи становятся вредными. Здесь на помощь приходит Circuit Breaker (Предохранитель).
Этот паттерн работает как электрический рубильник и имеет три состояния:
- Closed (Закрыт). Запросы проходят в обычном режиме. Мы считаем количество ошибок.
- Open (Открыт). Если процент ошибок превысил порог (например, 50% за последнюю минуту), предохранитель «размыкается». Все запросы обрываются мгновенно с ошибкой, даже не пытаясь достучаться до сервиса. Это дает упавшему сервису шанс восстановиться без внешнего давления.
- Half-Open (Полуоткрыт). Через некоторое время предохранитель пропускает один-два «пробных» запроса. Если они успешны — возвращаемся в состояние Closed. Если нет — снова в Open.
5. Практические рекомендации по реализации
Чтобы механизм повторов не стал источником проблем, следуйте этим правилам:
- Логируйте попытки. Вы должны видеть в мониторинге не только общую ошибку, но и то, что запрос был успешно выполнен с 3-й попытки. Это первый сигнал о том, что система начала деградировать.
- Используйте заголовки. Если вы используете API, которые поддерживают
Retry-After, всегда отдавайте приоритет этому значению перед своими внутренними алгоритмами. - Идемпотентность через ключи. Чтобы безопасно повторять POST-запросы, внедрите
Idempotency-Key(уникальный UUID запроса). Сервер должен сохранять результат обработки этого ключа, чтобы при повторном запросе просто вернуть старый ответ, а не выполнять операцию снова. - Разделяйте таймауты. Таймаут на один запрос (Connect Timeout / Read Timeout) и общий таймаут на всю цепочку повторов — это разные вещи. Не путайте их.
Заключение
Retries — это инструмент управления неопределенностью. Правильно настроенный бэк-офф с джиттером и Circuit Breaker делают систему «эластичной». Однако помните: ретраи не лечат причину сбоя, они лишь маскируют его, давая время на исправление. Если ваши логи забиты успешными повторами, значит, ваша архитектура нестабильна, и вместо настройки интервалов ожидания стоит задуматься об оптимизации производительности или масштабировании ресурсов.

