Как проектировать retry storm mitigation (защита от лавинных ретраев)?
Краткий тезис
Retry storm (лавина повторных запросов) — ситуация, когда из-за временного сбоя тысячи клиентов одновременно начинают повторять запросы, вызывая лавинообразный рост нагрузки на сервер. В Agentic RAG эта проблема особенно опасна, так как агенты выполняют множественные вызовы LLM и инструментов, и каждый сбой может породить цепные ретраи. Защита строится на комбинации четырёх методов: backoff|Exponential backoff (backoff|экспоненциальная задержка) с Jitter (дрожание), Circuit breaker (автоматический размыкатель цепи), Client-side rate limiting и Dead letter queue (очередь недоставленных сообщений).
1. Термин: Retry Storm в контексте Agentic RAG
Retry storm — это каскад повторных запросов, возникающий, когда временный сбой (например, перегрузка LLM-эндпоинта или недоступность векторной базы данных) заставляет множество клиентов одновременно повторять запросы. В RAG|Agentic RAG агенты могут совершать десятки вызовов за один пользовательский запрос (LLM, инструменты, поиск), поэтому один сбой может породить лавину ретраев от разных агентов.
Почему это критично
- Агенты часто используют фиксированные таймауты и ретраи по умолчанию — без продуманной стратегии они быстро перегружают backend.
- LLM-провайдеры (OpenAI, Anthropic) могут временно возвращать ошибки 429 Too Many Requests или 503 Service Unavailable.
- В RAG-системе векторная БД (Pinecone, Weaviate) также может быть узким местом.
Термины
- Exponential backoff — стратегия, при которой задержка между ретраями растёт экспоненциально (1 с, 2 с, 4 с, 8 с…).
- Jitter — случайное отклонение от расчётной задержки, чтобы разнести моменты повторов у разных клиентов.
- Circuit breaker — паттерн, который после определенного числа ошибок временно блокирует все запросы к сбойному сервису.
- Dead letter queue (DLQ) — очередь для сообщений/задач, которые не удалось обработать после максимального числа ретраев.
2. Exponential backoff — основа стратегии
Exponential backoff задаёт интервал повтора равным base_delay * (2^attempt) плюс случайный jitter. Базовая формула (без джиттера):
delay = base * (2 ** attempt)
Где attempt — номер попытки (начиная с 0).
Пример в Python (with jitter):
import random
import time
def exponential_backoff(base, attempt, max_delay=60):
delay = base * (2 ** attempt)
delay = min(delay, max_delay) # верхняя граница
jitter = random.uniform(0, 1) # случайная добавка до 1 с
return delay + jitter
# Использование:
for attempt in range(3):
try:
call_service()
break
except TemporaryError:
time.sleep(exponential_backoff(1, attempt))
Почему нужен jitter
Без джиттера все клиенты, начавшие ретрай в один момент, будут повторять запросы синхронно (например, ровно через 1 с, 2 с, 4 с). Добавление случайной составляющей размазывает пики нагрузки.
Рекомендации по параметрам
| Параметр | Значение | Комментарий |
|---|---|---|
| base_delay | 0.5–1 с | Минимальная задержка перед первым ретреем |
| max_delay | 30–60 с | Ограничение, чтобы не ждать слишком долго |
| max_attempts | 3–5 | Для LLM-вызовов обычно хватает 3 попыток |
| jitter | 0–100% от delay | Джиттер может быть как аддитивным, так и мультипликативным |
3. Circuit breaker — защита на уровне сервиса
Circuit breaker (автоматический выключатель) — паттерн, предотвращающий лавину запросов к нестабильному сервису. Состояния:
- Closed (замкнут) — запросы проходят, ошибки считаются.
- Open (разомкнут) — запросы немедленно отвергаются с ошибкой, не доходя до сервиса.
- Half-open (полуоткрыт) — пробный запрос разрешён; если успешен — переходим в Closed, иначе — снова Open.
Параметры circuit breaker
| Параметр | Рекомендация |
|---|---|
| threshold (число ошибок за окно) | 5–10 ошибок за 10 секунд |
| timeout (время в Open) | 30–60 с |
| success threshold (для Half-open) | 1–3 успешных пробных запроса |
Пример реализации на Python (упрощённо):
import time
class CircuitBreaker:
def __init__(self, threshold=5, timeout=30):
self.threshold = threshold
self.timeout = timeout
self.failures = 0
self.last_failure_time = 0
self.state = 'closed'
def call(self, func, *args, **kwargs):
if self.state == 'open':
if time.time() - self.last_failure_time > self.timeout:
self.state = 'half-open'
else:
raise Exception('Circuit breaker open')
try:
result = func(*args, **kwargs)
if self.state == 'half-open':
self.state = 'closed'
self.failures = 0
return result
except Exception:
self.failures += 1
self.last_failure_time = time.time()
if self.failures >= self.threshold:
self.state = 'open'
raise
В Agentic RAG circuit breaker полезно ставить перед каждым внешним сервисом: LLM-провайдером, векторной БД, инструментами (API погоды, поисковик).
4. Client-side rate limiting — ограничение повторов на клиенте
Client-side rate limiting — механизм, который не даёт одному клиенту отправлять слишком много ретраев в единицу времени. Это важно, потому что даже при правильном backoff много параллельных агентов могут создать нагрузку.
Подходы
- Token bucket — токены пополняются с заданной скоростью, каждый ретрай тратит токен.
- Sliding window — подсчёт количества ретраев за последние N секунд.
Пример token bucket
import time
class TokenBucket:
def __init__(self, rate, capacity):
self.rate = rate # токенов в секунду
self.capacity = capacity
self.tokens = capacity
self.last_refill = time.time()
def consume(self, tokens=1):
now = time.time()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.last_refill = now
if self.tokens >= tokens:
self.tokens -= tokens
return True
return False
# Использование перед ретреем:
rate_limiter = TokenBucket(rate=5, capacity=10)
if rate_limiter.consume():
# выполнить retry
pass
else:
# поставить в очередь или вернуть ошибку клиенту
В Agentic RAG rate limit можно настраивать отдельно для каждого агента или для всего процесса. Например, не более 10 ретраев на LLM в минуту от одного агента.
5. Dead letter queue (DLQ) — безопасное завершение неудачных попыток
DLQ — концепция из message-driven архитектур. Если после максимального числа ретраев (например, 3) запрос всё ещё не удался, он помещается в отдельную очередь для ручного анализа. В Agentic RAG DLQ может быть реализована как база данных или топик Kafka.
Сценарий
- Агент выполняет запрос к LLM.
- Возникает ошибка 503.
- Агент повторяет запрос с exponential backoff.
- После 3 попыток — ошибка сохраняется.
- Запрос (или его сериализованное представление) отправляется в DLQ.
- Агент может уведомить пользователя, что запрос не выполнен, или продолжить с облегчённым fallback.
Плюсы
- Не блокирует поток обработки.
- Позволяет анализировать повторяющиеся ошибки.
- Снижает нагрузку на backend (вместо бесконечных ретраев).
6. Композиция методов — пример архитектуры для Agentic RAG
Общая схема защиты от retry storm в агенте:
Пользователь → Agent → CircuitBreaker → TokenBucket → ExponentialBackoff + Jitter → LLM API
↓ (после 3 попыток)
Dead Letter Queue / Fallback
Псевдокод
def robust_llm_call(prompt, context):
cb = circuit_breaker_registry['llm']
rl = rate_limiter_registry['llm']
for attempt in range(max_retries):
if cb.state == 'open':
# немедленный отказ
raise CircuitBreakerOpen()
if not rl.consume():
# ждём, пока появится токен
time.sleep(0.5)
continue
try:
result = llm_api_call(prompt, context)
cb.report_success()
return result
except (TemporaryError, RateLimitError) as e:
cb.report_failure()
delay = exponential_backoff(base=1, attempt=attempt, jitter=True)
time.sleep(delay)
# после исчерпания попыток
dead_letter_queue.send({'prompt': prompt, 'context': context, 'error': str(e)})
return fallback_response() # например, "Извините, попробуйте позже"
7. Мониторинг и алертинг
Без мониторинга невозможно понять, что retry storm начинается. Ключевые метрики:
| Метрика | Источник |
|---|---|
| Количество ретраев на клиент | Логи клиента |
| Уровень ошибок сервиса (5xx) | Мониторинг эндпоинта |
| Latency при успешных запросах | APM (Datadog, Prometheus) |
| Состояние circuit breaker (open/closed) | Экспорт метрик |
| Число сообщений в DLQ | Kafka / очереди |
Рекомендация выставить алерты, если за 10 секунд количество ретраев превышает среднее значение на 3 сигмы. При срабатывании автоматически поднимать порог circuit breaker или временно отключать некритичных агентов.
Пет-проект для закрепления
Задача Создать симуляцию retry storm с двумя агентами, которые одновременно вызывают один LLM-сервер (заглушку) с периодическими ошибками. Реализовать защиту: exponential backoff + jitter, circuit breaker, token bucket и DLQ.
Инструменты Python с asyncio, aiohttp (для имитации HTTP-вызовов), Prometheus client (для сбора метрик), Redis (для хранения состояния circuit breaker и rate limiter).
Шаги:
- Напишите mock-сервер на aiohttp, который возвращает 200 в 70% случаев и 503 в 30%.
- Реализуйте клиент-агент с retry логикой без защиты — запустите 10 параллельных вызовов, замерьте нагрузку (количество запросов к серверу).
- Добавьте exponential backoff с jitter — замерьте пиковые нагрузки.
- Добавьте circuit breaker (на каждые 5 ошибок за 10 с) — посмотрите, как он снижает трафик.
- Добавьте token bucket (5 токенов/с) — убедитесь, что общее число ретраев ограничено.
- Реализуйте DLQ: если после 3 попыток ошибка не исчезла, запись в Redis-список.
- Соберите метрики (через prometheus_client — количество ретраев, ошибок, состояний) и выведите график нагрузки до и после защиты.
Ожидаемый результат
- График демонстрирует, что пиковая нагрузка снижена в 3–5 раз.
- Circuit breaker размыкается при устойчивом сбое, и количество ретраев падает до нуля.
- DLQ накапливает записи для анализа.
- Вы научитесь комбинировать четыре паттерна на практике.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 825 | Агентные loops и итеративные вызовы LLM |
| 828 | Circuit breaker для инструментов |
| 831 | Мониторинг и observability в Agentic RAG |
| 10 | Self-RAG и контроль качества |
| 20 | Rate limiting для LLM API |
Навигация
- Предыдущий: 829
- Следующий: 831
- Индекс: 00. Индекс разборов