中文翻译暂不可用,显示俄语原文。
Реализовать circuit breaker для LLM API
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать circuit breaker для LLM API
1. Цель задачи
Научиться проектировать и внедрять паттерн Circuit Breaker для защиты LLM API от каскадных отказов и перегрузок. Реализовать логику автоматического размыкания цепи при превышении порога ошибок (>50%) за 10-секундное окно, а также корректный fallback-механизм для поддержания функциональности потребителя API.
Ключевой результат Работающий circuit breaker с конфигурируемыми параметрами, который при превышении 50% ошибок за 10 секунд размыкает цепь и переключает вызовы на fallback-обработчик.
2. Исходные данные
Перед началом необходимо иметь:
| Что нужно | Откуда взять |
|---|---|
| LLM API (OpenAI / YandexGPT / локальная модель) | Реальный ключ API или локальный сервер (vLLM, Ollama) |
| Тестовый скрипт для вызова LLM | Написать самостоятельно на Python + httpx |
| Симулятор ошибок (500, timeout, 429) | Интегрировать в тестовый стенд |
| Fallback-функция | Реализовать как заглушку (например, возврат заранее заготовленного ответа) |
| Метрики (количество вызовов, ошибок, состояние breaker) | Prometheus / просто логирование в stdout |
Если нет реального LLM API — симулируем:
- Запускаем локальный HTTP-сервер на Flask/FastAPI, который эмулирует LLM эндпоинт.
- Сервер настраивается так, чтобы генерировать 50%+ ошибок (500, 503, таймауты) при каждом втором запросе или по расписанию.
- Пишем простой клиент, который делает запросы к этому серверу с использованием circuit breaker.
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Язык программирования | Python 3.11+ | Реализация logic breaker |
| HTTP-клиент | httpx (asyncio) | Асинхронные вызовы LLM API |
| LLM API (реальный/симуляция) | OpenAI API / FastAPI (stub) | Целевой сервис для тестирования |
| Библиотека circuit breaker | pybreaker (опционально) | Готовая имплементация или реализуем вручную |
| Мониторинг | Prometheus + client (prometheus_client) | Сбор метрик (вызовы, ошибки, состояния) |
| Логирование | structlog / logging | Отслеживание переключений |
| Тестирование | pytest + pytest-asyncio | Модульные и интеграционные тесты |
| Конфигурация | Pydantic Settings | Параметры breaker (порог, окно, таймауты) |
4. Этапы выполнения
Этап 1: Проектирование и подготовка окружения (30 минут)
Действия
-
Создать структуру проекта:
circuit_breaker/__init__.pycore.py— логика breakerfallback.py— fallback-обработчикиconfig.py— Pydantic-схема параметровmetrics.py— Prometheus метрикиstub_server.py— симулятор LLM API (если нет реального)tests/test_breaker.py
-
Определить параметры circuit breaker (через Pydantic):
class BreakerConfig(BaseSettings): failure_threshold: int = 5 # кол-во ошибок за окно recovery_timeout: int = 30 # сек до перехода в half-open window_size: int = 10 # окно в секундах failure_percentage: float = 50.0 # порог в % (будем вычислять) max_retries: int = 2 # попытки до открытия цепи -
Создать stub-сервер на FastAPI, который с вероятностью 60% возвращает 500:
# stub_server.py import random from fastapi import FastAPI, HTTPException import asyncio app = FastAPI() @app.get("/v1/chat/completions") async def chat(): await asyncio.sleep(0.1) if random.random() < 0.6: raise HTTPException(500, "Simulated server error") return {"choices": [{"message": {"content": "stub answer"}}]}
Ожидаемый результат этапа Проектная папка, конфигурация, работающий stub-сервер (если используем симуляцию).
Этап 2: Реализация логики circuit breaker (1.5–2 часа)
Действия
-
Реализовать класс
CircuitBreakerс тремя состояниями:CLOSED,OPEN,HALF_OPEN. -
Использовать скользящее окно для подсчёта доли ошибок:
- Хранить временные метки успешных и неуспешных вызовов за последние
window_sizeсекунд. - При каждом вызове вычислять:
error_rate = errors / total * 100. - Если
error_rate > failure_percentage→ перейти вOPEN.
- Хранить временные метки успешных и неуспешных вызовов за последние
-
Реализовать переходы:
CLOSED → OPEN: при превышении порога ошибок.OPEN → HALF_OPEN: послеrecovery_timeoutсекунд (один пробный запрос).HALF_OPEN → CLOSED: если пробный запрос успешен.HALF_OPEN → OPEN: если пробный запрос снова неудачен.
-
Добавить потокобезопасность (через
asyncio.Lock). -
Пример скелета:
class CircuitBreaker: STATES = ("CLOSED", "OPEN", "HALF_OPEN") def __init__(self, config: BreakerConfig): self.config = config self.state = "CLOSED" self.history: list[tuple[float, bool]] = [] # (timestamp, success?) self.last_open_time = 0.0 self.lock = asyncio.Lock() async def call(self, func, *args, **kwargs): # 1. Проверить состояние # 2. Если OPEN и таймаут истёк → HALF_OPEN # 3. Выполнить func (с таймаутом) # 4. Обновить историю # 5. Проверить процент ошибок # 6. Вернуть результат или выбросить CircuitBreakerError -
Реализовать декоратор/контекстный менеджер для удобного применения.
Ожидаемый результат этапа Готовый класс CircuitBreaker с корректной логикой состояний и скользящим окном.
Этап 3: Fallback-механизм (30 минут)
Действия
-
Создать функцию fallback:
async def fallback_response(original_request: dict) -> dict: # Возвращает заглушку или альтернативный ответ return {"choices": [{"message": {"content": "Service temporarily unavailable. Please try later."}}]} -
Интегрировать fallback в вызов: при
CircuitBreakerErrorили при открытом состоянии сразу вызывать fallback, а не бросать исключение пользователю. -
Опционально: реализовать приоритетный fallback (например, локальная модель → кэш → заглушка).
-
Пример использования:
breaker = CircuitBreaker(config) async def safe_llm_call(prompt: str): try: return await breaker.call(llm_api, prompt=prompt) except CircuitBreakerError: return await fallback_response(prompt)
Ожидаемый результат этапа Fallback-функция, вызываемая при размыкании цепи.
Этап 4: Мониторинг и метрики (30 минут)
Действия
-
Добавить Prometheus-метрики:
llm_requests_total{status="success|error|fallback"}llm_circuit_breaker_state{state="closed|open|half_open"}llm_request_duration_seconds(гистограмма)
-
Экспонировать метрики через
/metrics(использоватьprometheus_client+starlette). -
Настроить логирование переходов и событий (например,
logger.info("Circuit breaker opened")).
Ожидаемый результат этапа Метрики доступны по /metrics, состояния фиксируются в логах.
Этап 5: Тестирование и отладка (1 час)
Действия
-
Написать модульные тесты для
CircuitBreaker:- Проверить переход по порогу 50% ошибок за 10 с.
- Проверить восстановление после таймаута.
- Проверить, что fallback вызывается только при открытом состоянии.
-
Написать интеграционный тест со stub-сервером:
- Запустить stub-сервер с 60% ошибок.
- Выполнить 20 запросов через circuit breaker с порогом 50%.
- Убедиться, что после ~10 запросов цепь разомкнулась и начали приходить fallback-ответы.
-
Запустить ручное тестирование: увеличить порог до 80% -> цепь не должна размыкаться.
Ожидаемый результат этапа Все тесты проходят, circuit breaker работает согласно спецификации.
5. Критерии приемки (Definition of Done)
- Класс
CircuitBreakerреализован и поддерживает состояния CLOSED, OPEN, HALF_OPEN. - При превышении 50% ошибок за последние 10 секунд цепь переходит в OPEN.
- После перехода в OPEN все запросы немедленно перенаправляются на fallback без вызова реального API.
- Через заданный
recovery_timeout(по умолчанию 30 с) выполняется пробный запрос (HALF_OPEN). - Успешный пробный запрос переводит цепь обратно в CLOSED.
- Неуспешный пробный запрос возвращает цепь в OPEN.
- Fallback-функция возвращает корректный ответ (заглушку).
- Prometheus-метрики экспонируются: количество вызовов, ошибок, fallback, текущее состояние.
- Написаны минимум 3 модульных и 1 интеграционный тест (все проходят).
- Код покрыт обработкой исключений и таймаутов (не более 2 секунд на запрос).
6. Ожидаемый результат
- Файл/артефакт Python-пакет
circuit_breakerс реализацией, тестами, конфигурацией и примером использования. - Содержимое ключевого файла (
core.py): основной класс CircuitBreaker со всей логикой. - Дополнительные результаты
- README.md с инструкцией по запуску, конфигурации и тестированию.
- Скрипт
stress_test.py, который эмулирует нагрузку и демонстрирует работу breaker. - Лог файл с примерами переходов состояний.
7. Возможные сложности и их решение
| Сложность | Решение |
|---|---|
| Некорректный подсчёт ошибок в многопоточном окружении | Использовать asyncio.Lock при обновлении истории. Хранить историю в collections.deque с maxlen. |
| Полуоткрытое состояние позволяет одному запросу, но он может зависнуть (timeout) | Установить жёсткий таймаут на пробный запрос (например, 1 с). Если таймаут — считаем ошибкой. |
| Неоднозначность порога: 50% ошибок за 10 секунд — как считать, если за окно было мало запросов? | Ввести минимальное количество запросов за окно (например, 3). Если меньше — не размыкать. |
| Забыли сбросить историю после перехода в CLOSED | После успешного пробного запроса очищать историю, чтобы начать с чистого листа. |
| Fallback сам может выбросить исключение | Оборачивать fallback в try/except и логировать, возвращать супер-заглушку. |
8. Бюджет времени (оценка)
| Этап | Время |
|---|---|
| Этап 1: Проектирование и подготовка | 30 мин |
| Этап 2: Реализация логики circuit breaker | 1.5–2 ч |
| Этап 3: Fallback-механизм | 30 мин |
| Этап 4: Мониторинг и метрики | 30 мин |
| Этап 5: Тестирование и отладка | 1 ч |
| Итого | 4–4.5 ч |
Примечание для первого раза с учётом изучения библиотек и отладки рекомендуется выделить 6-7 часов.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 23 | Реализация timeout для HTTP-запросов |
| 45 | Экспоненциальный backoff при повторных попытках |
| 67 | Graceful degradation в микросервисах |
| 89 | Мониторинг уровня ошибок с Prometheus |
| 112 | Асинхронные вызовы API с asyncio |
| 156 | Rate limiting для внешних API |
| 203 | Обработка исключений в асинхронном коде |
| 250 | Паттерн Retry с джиттером |
| 301 | Конфигурация приложений через Pydantic |
| 405 | Тестирование async-функций с pytest |
10. Чек-лист самопроверки
- Я понимаю разницу между состояниями CLOSED, OPEN, HALF_OPEN и корректно их реализовал.
- Я проверил, что при 40% ошибок (ниже порога) цепь остаётся закрытой.
- Я убедился, что после размыкания все вызовы идут в fallback без доступа к реальному API.
- Я протестировал сценарий, когда пробный запрос в HALF_OPEN успешен — цепь закрывается.
- Я экспонировал метрики и проверил, что состояние breaker отображается в Prometheus (если настроен).