Как проектировать 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_delay0.5–1 сМинимальная задержка перед первым ретреем
max_delay30–60 сОграничение, чтобы не ждать слишком долго
max_attempts3–5Для LLM-вызовов обычно хватает 3 попыток
jitter0–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.

Сценарий

  1. Агент выполняет запрос к LLM.
  2. Возникает ошибка 503.
  3. Агент повторяет запрос с exponential backoff.
  4. После 3 попыток — ошибка сохраняется.
  5. Запрос (или его сериализованное представление) отправляется в DLQ.
  6. Агент может уведомить пользователя, что запрос не выполнен, или продолжить с облегчённым 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)Экспорт метрик
Число сообщений в DLQKafka / очереди

Рекомендация выставить алерты, если за 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).

Шаги:

  1. Напишите mock-сервер на aiohttp, который возвращает 200 в 70% случаев и 503 в 30%.
  2. Реализуйте клиент-агент с retry логикой без защиты — запустите 10 параллельных вызовов, замерьте нагрузку (количество запросов к серверу).
  3. Добавьте exponential backoff с jitter — замерьте пиковые нагрузки.
  4. Добавьте circuit breaker (на каждые 5 ошибок за 10 с) — посмотрите, как он снижает трафик.
  5. Добавьте token bucket (5 токенов/с) — убедитесь, что общее число ретраев ограничено.
  6. Реализуйте DLQ: если после 3 попыток ошибка не исчезла, запись в Redis-список.
  7. Соберите метрики (через prometheus_client — количество ретраев, ошибок, состояний) и выведите график нагрузки до и после защиты.

Ожидаемый результат

  • График демонстрирует, что пиковая нагрузка снижена в 3–5 раз.
  • Circuit breaker размыкается при устойчивом сбое, и количество ретраев падает до нуля.
  • DLQ накапливает записи для анализа.
  • Вы научитесь комбинировать четыре паттерна на практике.

Связь с другими вопросами

ВопросТема
825Агентные loops и итеративные вызовы LLM
828Circuit breaker для инструментов
831Мониторинг и observability в Agentic RAG
10Self-RAG и контроль качества
20Rate limiting для LLM API

Навигация