Ретраи. Как эффективно реализовать переповторы
Проблема: В распределенных системах временные сбои (сеть, БД, внешние сервисы) — норма. Простой вызов "повторить при ошибке" часто усугубляет проблему.
Для начала рассмотрим какие бывают ошибки
Виды ошибок
Не все ошибки равны: ретраить можно только временные сбои. Постоянные ошибки повторять бесполезно и вредно для ретраев
Повторяемые ошибки
Временные сбои, которые могут исчезнуть при повторной попытке. Типичные кейсы повторяемых ошибок:
Сетевые сбои
- Таймауты TCP, ConnectionReset
- Проблемы балансировщика, кратковременная недоступность сети
Ограничения ресурсов
- HTTP 429 (Too Many Requests)
- Превышение лимитов API (Rate Limiting)
- Пример: Пользователь массово экспортирует данные → API временно ограничивает запросы
- Превышение лимитов API (Rate Limiting)
Ошибки серверов
- HTTP 5xx (503, 504)
- Перегрузка сервера, деградация БД
- Пример: (HTTP 503) Сервис геокодирования перегружен в час пик
- Перегрузка сервера, деградация БД
Конфликты данных
- Деадлоки БД, оптимистичные блокировки
- Конкурентные транзакции
- Пример: деадлоки БД - два клиента одновременно редактируют один заказ
- Конкурентные транзакции
Постоянные ошибки
- HTTP 400 (Bad Request)
Клиент отправил невалидные данные (например, буквы в поле "Цена"). Повторы бесполезны. - HTTP 404 (Not Found)
Ресурс удален (например, несуществующий ID товара). Повторы создают нагрузку. - HTTP 403 (Forbidden)
Постоянное отсутствие прав (например, просмотр чужих заказов).
Стратегии повторов
Экспоненциальная задержка (Exponential Backoff)
Растущая задержка: 1с → 2с → 4с → 8с
Для снижение нагрузки на сбойный ресурс, дает ему время на восстановление.
Пример:
Пользователь оплачивает заказ → Платежный шлюз временно недос тупен → Система повторяет через 0.5с, 1с, 2с → 95% платежей проходят со 2-3 попытки.
Джиттер (Jitter)
Когда к задержке добавляется случайное значение, чтобы 1000 запросов не повторились одновременно.
Формула: Фактическая задержка = Базовая задержка + random(0, 30% от задержки)
Для предотвращение синхронизации запросов
Пример:
Черная пятница: 10 000 корзин ожидают оплаты → Без джиттера: повтор всех 10к запросов одновременно → Коллапс платежной системы. С джиттером: запросы распределяются равномерно.
Комбинированные подходы
a) Retry-After + Backoff
Использование заголовка HTTP Retry-After для точного определения задержки.
HTTP/1.1 429 Too Many Requests Retry-After: 15 ← Ждать ровно 15 секунд
Когда использовать: При интеграции с внешними API (AWS, Azure, банки).
b) Адаптивные ретраи
Динамический расчет задержки на основе:
- Истории ответов сервиса
- Текущей нагрузки
- SLA системы
Пример:
Система логирования увеличивает задержку с 1с до 10с при 1000 ошибок/мин.
Ограничение попыток Максимальное число ретраев (напр., 3-5). Это предотвращает бесконечные цик лы.
После исчерпания попыток— фиксируем ошибку. Например:
- Асинхронная обработка (отправка в очередь/Dead Letter Queue)
- Уведомление мониторинга
Circuit Breaker ("Предохранитель" системы)
Это "автомат", который временно блокирует вызовы сбойного сервиса. Принцип: После N ошибок за период T, все последующие вызовы мгновенно завершаются ошибкой без реального вызова ресурса. Периодически проверяет "полуоткрытое" состояние.
Следует определить пороги срабатывания (N, T), время восстановления. Связать с SLA и поведением системы при отказе.
Состояния:
- Closed: вызовы проходят
Система работает нормально - Open: вызовы блокируются (ошибка без реального запроса)
После 5 ошибок за 1 мин - Half-Open: Пропускает часть трафика для проверки восстановления
Кейс: Сервис отправки SMS падает → Circuit Breaker блокирует вызовы на 2 мин → Предотвращает:
- Потерю денег за SMS
- Перегрузку очереди сообщений
- Каскадные сбои
Идемпотентность
Идемпотентность: операция должна безопасно переносить многократное выполнение (отдавать одинаковый результат). Следует требовать идемпотентности от операций, которые будут ретраиться (POST/PUT запросы, сообщения).
Долгие ретраи могут испортить UX. Для пользовательских запросов можно использовать короткие лимиты попыток или асинхронные паттерны.
Без идемпотентности выходят ошибки. Например:
- Двойное списание денег при повторе платежа
- 2 одинаковых заказа при сбое сети
Пример как реализовать идемпотентность:
- Уникальные ID запросов { "payment_id": "123e4567-e89b-12d3", "amount": 100 }
- Проверка статуса перед повторным выполнением
- Версионирование данных в БД
- Таймауты: общие таймауты на операцию (включая ретраи) должны быть значительно меньше таймаутов вызывающей стороны.
Кейс: Повтор платежа → Система проверяет payment_id → Если уже выполнен → возвращает текущий статус.
Логирование и Мониторинг
Детальное Логирование: фиксировать факт ретрая, номер попытки, причину ошибки, задержку. Отделять логи ретраев от исходных ошибок.
Примеры обязательных метрик:
- Количество ретраев (total, по сервисам/ошибкам).
- Количество успешных ретраев (восстановленных ошибок).
- Количество срабатываний Circuit Breaker.
- Процент запросов, завершившихся ошибкой после всех ретраев. Эти метрики показывают стабильность системы, эффективность стратегии и узкие места.
Антипаттерны: Чего избегать
a) Ретраи без ограничений
Чем опасно: Полный отказ системы при перманентной ошибке.
b) Ретраи для неидемпотентных операций
Пример: POST /cart/checkout → Повтор → 2 одинаковых заказа.
c) Каскадные ретраи
Сервис А вызывает Б → Б ретраит вызов к В → А ретраит вызов к Б.
Итог: Экспоненциальный рост нагрузки (10x).
Интеграция с Observability
a) Трассировка
Добавлять в логи:
retry_attempt: 2/5retry_delay: 2.3sroot_error: DB_DEADLOCK
b) Дашборды
Обязательные графики:
- Успешные ретраи vs Финальные ошибки
- Топ сервисов по срабатыванию Circuit Breaker
- Среднее время восстановления после сбоев
c) Алёрты
Circuit Breaker в состоянии OPEN > 5 минЭффективность ретраев < 70%