English translation is not available yet. Showing Russian content.
Реализовать 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 час)
Действия
-
Создать структуру проекта
retry-storm-mitigation/ ├── agent_a/ │ ├── main.py │ └── Dockerfile ├── agent_b/ │ ├── main.py │ └── Dockerfile ├── docker-compose.yml └── scripts/ └── load_test.py -
Реализовать
agent_b/main.py— простой сервер с одним endpoint/call, который:- В 30% случаев возвращает 503 Service Unavailable
- В остальных —
200 OKс телом{"result": "success"} - Логирует каждый запрос с меткой времени
-
Реализовать
agent_a/main.py— сервер с endpoint/trigger, который:- Принимает параметр
count(число параллельных запросов к agent_b) - Запускает
countасинхронных задач, каждая из которых делает один запрос кagent_b:/call(без retry) - Логирует статусы ответов
- Принимает параметр
-
Запустить оба агента через docker-compose up и проверить, что
agent_aможет достучаться доagent_b(одиночный запрос).
Ожидаемый результат этапа Работающие два сервиса, при ручном тесте curl localhost:8000/trigger?count=5 видны сообщения об ошибках 503.
Этап 2: Демонстрация retry storm (30 минут)
Действия
-
Написать скрипт
load_test.py(например, на asyncio), который делает 50 запросов кagent_a:/trigger?count=10с минимальной паузой (0.1 сек между вызовами). Суммарно ≈ 500 запросов кagent_b. -
Запустить скрипт без retry-логики Собрать логи обоих агентов.
-
Проанализировать паттерн в момент, когда
agent_bначинает отвечать 503,agent_aпродолжает отправлять запросы с той же интенсивностью. Еслиagent_bупадёт полностью — все потокиagent_aбудут пытаться, усугубляя ситуацию (лавина). -
Построить график количества вызовов
agent_bв секунду (по логам). Убедиться, что нет спада при ошибках.
Ожидаемый результат этапа Явная иллюстрация retry storm: при появлении ошибок количество запросов не снижается или даже растёт (если retry мгновенные).
Этап 3: Реализация exponential backoff (1,5 часа)
Действия
-
Добавить в
agent_aretry-логику с экспоненциальной задержкой.- Использовать библиотеку 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.
- Логировать каждую попытку (номер, время).
- Использовать библиотеку tenacity:
-
Запустить нагрузочный тест повторно Сравнить количество запросов к
agent_bи время выполнения. -
Проверить эффект теперь при ошибках вызовы распределяются не мгновенно, а с задержками (1с, 2с, 4с, 8с, 10с). Однако если много клиентов одновременно получают ошибку и начинают retry с одинаковой задержкой (например, все ждут 1с), возникает «thundering herd» — первый ретрай всех придётся одновременно.
-
Зафиксировать в логах, что в момент t+1с происходит всплеск запросов от всех задач.
Ожидаемый результат этапа Работающий exponential backoff, но возможен thundering herd при старте retry.
Этап 4: Добавление jitter (1,5 часа)
Действия
-
Модифицировать 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 для максимального разряда.
- Использовать tenacity.wait_exponential с
-
Интегрировать в
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(): ...
- Пример:
-
Повторить нагрузочный тест Собрать логи и построить график времени между вызовами. Убедиться, что в любой момент времени количество одновременных ретраев не превышает
(число клиентов) / (диапазон задержки). Пиковые нагрузки сглажены. -
Проверить крайний случай
agent_bотвечает 100% ошибкой в течение 10 секунд. Retry должны распределиться равномерно, не создавая лавину.
Ожидаемый результат этапа Реализация full jitter с демонстрацией снижения пиковых нагрузок на логи и графики.
Этап 5: Тестирование и документирование (1 час)
Действия
-
Написать unit-тесты для retry-логики
- Проверить, что при успешном ответе retry не происходит.
- Проверить, что при 5 ошибках подряд выбрасывается исключение.
- Проверить, что задержка варьируется в ожидаемом диапазоне (jitter).
- Использовать pytest и pytest-asyncio с mock-вызовами.
-
Написать интеграционный тест
- Поднять оба агента в тестовом контейнере (или ProcessPool).
- Выполнить серию вызовов с известным процентом ошибок.
- Убедиться, что total number of requests не превышает (количество оригиналов * max_attempts), и что пиковая нагрузка ≤ порога.
-
Документировать в 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 storm | 0.5 |
| 3. Реализация exponential backoff | 1.5 |
| 4. Добавление jitter | 1.5 |
| 5. Тестирование и документирование | 1.0 |
| Итого | 5.5 |
Примечание для первого раза При отсутствии опыта работы с tenacity и asyncio ожидайте дополнительно 1-1.5 часа на изучение документации и отладку.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| #45 | Retry strategies: exponential backoff vs linear vs immediate |
| #47 | Jitter types: full, equal, decorrelated |
| #112 | Thundering herd problem and solutions |
| #134 | Circuit breaker pattern vs retry |
| #156 | Error propagation in multi-agent systems |
| #200 | Timeouts and deadlines in distributed systems |
| #245 | Load shedding under cascading failures |
| #311 | Graceful degradation in microservices |
| #401 | Chaos engineering: testing retry storms |
| #567 | Observability: 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 описывает, как воспроизвести результаты.