中文翻译暂不可用,显示俄语原文。

Реализовать retry storm mitigation (exponential backoff + jitter)

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать retry storm mitigation (exponential backoff + jitter)

1. Цель задачи

Научиться предотвращать лавинные повторные запросы (retry storm) в многоагентной системе, когда сбой одного агента вызывает одновременные ретраи от множества других агентов. Реализовать механизм повторных вызовов с экспоненциальной задержкой (exponential backoff) и джиттером (jitter) для равномерного распределения нагрузки после частичного отказа.

Ключевой результат Два агента (сервиса), взаимодействующих по HTTP, где при временной недоступности второго агента первый повторяет запросы с управляемой задержкой, не вызывая лавинного роста трафика.

2. Исходные данные

Перед началом необходимо иметь:

Что нужноОткуда взять
Базовый код двух микросервисов (агентов) на PythonНаписать с нуля или использовать шаблон из репозитория курса (если есть)
Среда для запуска (Python, Docker)Локальная машина или облачная VM
Инструмент нагрузки (например, locust или ab)Установить через pip: locust, requests, aiohttp
Логи сервисовСтандартный stdout / stderr, при необходимости – файл
Если нет реальной многоагентной системы — симулируем:Создать два простых FastAPI приложения: agent_a (вызывающий) и agent_b (вызываемый). agent_b имеет endpoint, который случайно отвечает 503 (например, 30% ошибок). agent_a делает серию запросов к agent_b и реализует retry-логику.

3. Технологический стек

КомпонентИнструментыНазначение
Язык программированияPython 3.10+Реализация логики агентов
Web-фреймворкFastAPI + uvicornСоздание HTTP-сервисов
Асинхронный HTTP-клиентaiohttpВызовы между агентами
Retry-библиотекаtenacity (Python)Exponential backoff + jitter (запасной вариант: написать вручную)
Нагрузочное тестированиеlocust или простой скрипт с asyncioИмитация нагрузки и проверка storm
Логированиеlogging (Python)Фиксация retry-попыток и времени
Контейнеризация (опционально)Docker + docker-composeИзоляция и масштабирование агентов
Анализmatplotlib + pandasПостроение графика зависимости времени между retry-попытками

4. Этапы выполнения

Этап 1: Создание базового стенда (1 час)

Действия

  1. Создать структуру проекта

    retry-storm-mitigation/
    ├── agent_a/
    │   ├── main.py
    │   └── Dockerfile
    ├── agent_b/
    │   ├── main.py
    │   └── Dockerfile
    ├── docker-compose.yml
    └── scripts/
        └── load_test.py
    
  2. Реализовать agent_b/main.py — простой сервер с одним endpoint /call, который:

    • В 30% случаев возвращает 503 Service Unavailable
    • В остальных — 200 OK с телом {"result": "success"}
    • Логирует каждый запрос с меткой времени
  3. Реализовать agent_a/main.py — сервер с endpoint /trigger, который:

    • Принимает параметр count (число параллельных запросов к agent_b)
    • Запускает count асинхронных задач, каждая из которых делает один запрос к agent_b:/call (без retry)
    • Логирует статусы ответов
  4. Запустить оба агента через docker-compose up и проверить, что agent_a может достучаться до agent_b (одиночный запрос).

Ожидаемый результат этапа Работающие два сервиса, при ручном тесте curl localhost:8000/trigger?count=5 видны сообщения об ошибках 503.

Этап 2: Демонстрация retry storm (30 минут)

Действия

  1. Написать скрипт load_test.py (например, на asyncio), который делает 50 запросов к agent_a:/trigger?count=10 с минимальной паузой (0.1 сек между вызовами). Суммарно ≈ 500 запросов к agent_b.

  2. Запустить скрипт без retry-логики Собрать логи обоих агентов.

  3. Проанализировать паттерн в момент, когда agent_b начинает отвечать 503, agent_a продолжает отправлять запросы с той же интенсивностью. Если agent_b упадёт полностью — все потоки agent_a будут пытаться, усугубляя ситуацию (лавина).

  4. Построить график количества вызовов agent_b в секунду (по логам). Убедиться, что нет спада при ошибках.

Ожидаемый результат этапа Явная иллюстрация retry storm: при появлении ошибок количество запросов не снижается или даже растёт (если retry мгновенные).

Этап 3: Реализация exponential backoff (1,5 часа)

Действия

  1. Добавить в agent_a retry-логику с экспоненциальной задержкой.

    • Использовать библиотеку tenacity:
      from tenacity import retry, stop_after_attempt, wait_exponential
      
      @retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=1, max=10))
      async def call_agent_b():
          ...
      
    • Параметры: multiplier=1 (сек), min=1, max=10, max_attempts=5.
    • Логировать каждую попытку (номер, время).
  2. Запустить нагрузочный тест повторно Сравнить количество запросов к agent_b и время выполнения.

  3. Проверить эффект теперь при ошибках вызовы распределяются не мгновенно, а с задержками (1с, 2с, 4с, 8с, 10с). Однако если много клиентов одновременно получают ошибку и начинают retry с одинаковой задержкой (например, все ждут 1с), возникает «thundering herd» — первый ретрай всех придётся одновременно.

  4. Зафиксировать в логах, что в момент t+1с происходит всплеск запросов от всех задач.

Ожидаемый результат этапа Работающий exponential backoff, но возможен thundering herd при старте retry.

Этап 4: Добавление jitter (1,5 часа)

Действия

  1. Модифицировать retry, добавив jitter.

    • Использовать tenacity.wait_exponential с exp_base=2 и добавить tenacity.wait_random или tenacity.wait_chain? Лучше написать собственную wait-функцию для полного контроля.
    • Реализовать full jitter:
      import random
      import asyncio
      
      async def wait_full_jitter(retry_state):
          base_delay = 2 ** (retry_state.attempt_number - 1)  # 1, 2, 4, 8
          max_delay = 10
          actual_delay = min(base_delay, max_delay)
          jittered = random.uniform(0, actual_delay)
          await asyncio.sleep(jittered)
      
    • Или эквивалент через tenacity.wait_exponential + tenacity.wait_random:
      from tenacity import wait_combine, wait_exponential, wait_random
      wait = wait_combine(wait_exponential(multiplier=1, max=10), wait_random(0, 1))
      # но это не full jitter, а небольшой разброс
      
    • Рекомендуется реализовать full jitter для максимального разряда.
  2. Интегрировать в agent_a, переписав retry-декоратор или обертку.

    • Пример:
      from tenacity import retry, stop_after_attempt, wait_base
      
      class FullJitterWait(wait_base):
          def __call__(self, retry_state):
              base = 2 ** (retry_state.attempt_number - 1)
              base = min(base, 10)
              return random.uniform(0, base)
      
      @retry(stop=stop_after_attempt(5), wait=FullJitterWait())
      async def call_agent_b():
          ...
      
  3. Повторить нагрузочный тест Собрать логи и построить график времени между вызовами. Убедиться, что в любой момент времени количество одновременных ретраев не превышает (число клиентов) / (диапазон задержки). Пиковые нагрузки сглажены.

  4. Проверить крайний случай agent_b отвечает 100% ошибкой в течение 10 секунд. Retry должны распределиться равномерно, не создавая лавину.

Ожидаемый результат этапа Реализация full jitter с демонстрацией снижения пиковых нагрузок на логи и графики.

Этап 5: Тестирование и документирование (1 час)

Действия

  1. Написать unit-тесты для retry-логики

    • Проверить, что при успешном ответе retry не происходит.
    • Проверить, что при 5 ошибках подряд выбрасывается исключение.
    • Проверить, что задержка варьируется в ожидаемом диапазоне (jitter).
    • Использовать pytest и pytest-asyncio с mock-вызовами.
  2. Написать интеграционный тест

    • Поднять оба агента в тестовом контейнере (или ProcessPool).
    • Выполнить серию вызовов с известным процентом ошибок.
    • Убедиться, что total number of requests не превышает (количество оригиналов * max_attempts), и что пиковая нагрузка ≤ порога.
  3. Документировать в README.md

    • Описание механизма backoff + jitter.
    • Как запустить стенд.
    • Графики до/после (сохранить PNG).
    • Рекомендации по выбору параметров (multiplier, max_attempts, jitter type).

Ожидаемый результат этапа Набор тестов, README с визуализацией, чистый Git-репозиторий.

5. Критерии приемки (Definition of Done)

  • Создан проект с двумя FastAPI-агентами, запускаемый через docker-compose.
  • В agent_a реализована retry-логика с exponential backoff.
  • В retry-логику добавлен full jitter (random.uniform от 0 до base_delay).
  • Проведён нагрузочный тест без ошибок, и собран лог с временными метками.
  • Построены графики числа запросов/сек для сценариев: (а) без retry, (б) backoff без jitter, (в) backoff + jitter.
  • В тестах (минимум 3) проверено корректное поведение retry.
  • README содержит инструкцию по запуску и визуализацию результатов.
  • Код покрыт комментариями и оформлен по PEP8.

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

Основной артефакт Git-репозиторий со следующей структурой:

retry-storm-mitigation/
├── agent_a/
│   ├── main.py          # Реализация с retry + jitter
│   ├── retry_logic.py    # (опционально) вынесенная wait-функция
│   └── Dockerfile
├── agent_b/
│   ├── main.py          # Эмуляция сбоев
│   └── Dockerfile
├── tests/
│   ├── test_retry_unit.py
│   └── test_integration.py
├── scripts/
│   └── load_test.py      # Скрипт нагрузки
├── results/
│   ├── no_retry.png
│   ├── backoff_no_jitter.png
│   └── backoff_jitter.png
├── docker-compose.yml
├── README.md
└── requirements.txt

Содержание Полноценная демонстрация mitigation retry storm с помощью exponential backoff + jitter. Код может быть использован как reference-реализация для production-систем.

Опционально

  • Дополнительный сценарий с равномерным jitter (equal jitter) и сравнение.
  • Логирование в формате JSON для лучшего анализа.

7. Возможные сложности и их решение

СложностьРешение
Retry-библиотека tenacity не поддерживает асинхронный jitter напрямуюИспользовать wait_base, как показано в этапе 4, или написать собственную wait-функцию с asyncio.sleep
Неравномерное распределение нагрузки из-за разных времен ответа agent_bИспользовать stop_after_delay вместо stop_after_attempt для гарантии общего таймаута
Сложность визуализации логовФорматировать логи в CSV с помощью logging.Formatter и загружать в pandas для построения графиков
Docker-сеть не видит сервисыПроверить docker-compose network aliases и использовать порты agent_b:8001
Thundering herd не полностью устраняется full jitter при большом числе клиентовДобавить jitter в wait_exponential и уменьшить multiplier (например, 0.5)

8. Бюджет времени (оценка)

ЭтапВремя (часы)
1. Создание базового стенда1.0
2. Демонстрация retry storm0.5
3. Реализация exponential backoff1.5
4. Добавление jitter1.5
5. Тестирование и документирование1.0
Итого5.5

Примечание для первого раза При отсутствии опыта работы с tenacity и asyncio ожидайте дополнительно 1-1.5 часа на изучение документации и отладку.

9. Связанные вопросы из базы знаний

ВопросТема
#45Retry strategies: exponential backoff vs linear vs immediate
#47Jitter types: full, equal, decorrelated
#112Thundering herd problem and solutions
#134Circuit breaker pattern vs retry
#156Error propagation in multi-agent systems
#200Timeouts and deadlines in distributed systems
#245Load shedding under cascading failures
#311Graceful degradation in microservices
#401Chaos engineering: testing retry storms
#567Observability: logging and metrics for retries

10. Чек-лист самопроверки

  • Я проверил, что код retry-логики использует экспоненциальную задержку с базой 2 и максимальной задержкой 10 секунд.
  • Я убедился, что добавлен джиттер (например, random.uniform(0, base_delay)) для разброса retry-попыток.
  • Я запустил нагрузочный тест и сравнил графики: пик запросов/с уменьшился как минимум в 2 раза по сравнению с backoff без jitter.
  • Я написал хотя бы один unit-тест на функцию ожидания, проверяющий, что возвращаемое значение лежит в [0, max_delay].
  • Я убедился, что при полном отказе agent_b (100% 503) agent_a не делает более 5 попыток на каждый запрос, и общее количество запросов не превышает (orig * max_attempts).
  • Я закоммитил все изменения и README описывает, как воспроизвести результаты.