Один retry не спасёт: как circuit breaker отключает проблемный LLM-провайдер до того, как он положит прод
Вчера под статьёй о таймаутах и ретраях мне справедливо возразили: если лимит провайдера просел надолго, одна дополнительная попытка просто переносит сбой на пару секунд позже.
Согласен. Retry помогает пережить короткий сетевой сбой. Но если провайдер деградировал на десять минут, продолжать стучаться в него каждым запросом — значит увеличивать задержки, расходы и очередь.
Для таких ситуаций нужен circuit breaker — автоматический выключатель, который временно исключает проблемного провайдера из маршрутизации.
Что именно делает circuit breaker
У него три состояния.
Closed — всё работает нормально. Запросы идут к основному провайдеру, а система собирает статистику ошибок.
Open — порог ошибок превышен. Новые запросы сразу отправляются в fallback, упрощённый сценарий или завершаются контролируемой ошибкой. Мы больше не тратим несколько секунд на заведомо проблемный маршрут.
Half-open — после паузы система пропускает несколько тестовых запросов. Если они успешны, провайдер возвращается в работу. Если ошибки продолжаются, выключатель снова открывается.
Главная польза здесь не в том, что запрос будет повторён. Наоборот: система на некоторое время перестаёт делать бессмысленные попытки.
Какие ошибки должны открывать circuit breaker
Считать все неуспешные ответы одинаково нельзя.
Я бы учитывал:
— повторяющиеся 429, особенно если провайдер возвращает большой Retry-After; — таймауты соединения и ожидания первого токена; — ответы 502, 503 и 504; — обрывы потока до получения полезного результата; — резкий рост latency выше допустимого порога.
Не стоит учитывать:
— неверный API-ключ; — ошибку в структуре запроса; — превышение контекстного окна; — отказ валидации на нашей стороне; — пользовательскую отмену запроса.
Такие ошибки требуют исправления данных или конфигурации. Отключение провайдера проблему не решит.
Пример настроек для небольшого проекта
Универсальных цифр нет, но начать можно с простой схемы:
— окно наблюдения — 30 секунд; — минимум 20 запросов для принятия решения; — открытие circuit breaker при 50% временных ошибок; — пауза перед проверкой — 60 секунд; — в состоянии half-open — 3 тестовых запроса; — возврат в closed только после успешных тестов.
Минимальное число запросов важно. Если открыть breaker после одной ошибки из двух, система начнёт переключаться между провайдерами от любого случайного сбоя.
Как это связано с общим бюджетом времени
Допустим, пользовательский запрос должен завершиться за 15 секунд.
Если circuit breaker закрыт:
1. Отдаём основному провайдеру до 8 секунд. 2. При единичной временной ошибке можем сделать ещё одну короткую попытку, если бюджет позволяет. 3. Если накопленный порог ошибок превышен, открываем breaker. 4. Оставшееся время отдаём проверенному fallback-провайдеру.
Если breaker уже открыт, первые 8 секунд не теряются вообще. Запрос сразу идёт по резервному маршруту.
Именно поэтому circuit breaker дополняет общий дедлайн, а не заменяет его.
Fallback тоже может упасть
Распространённая ошибка — направить весь трафик на резервную модель и случайно положить уже её.
Поэтому для каждого провайдера нужны отдельные:
— лимиты параллельных запросов; — очереди; — circuit breaker; — бюджеты токенов; — метрики latency и ошибок.
Это принцип bulkhead: проблема одного внешнего сервиса не должна забирать все ресурсы приложения.
Если резервная модель слабее или дороже, полезно заранее определить режим деградации. Например:
— сократить контекст; — отключить необязательные инструменты агента; — использовать более короткий системный промпт; — вернуть черновой ответ без дополнительной проверки; — временно перевести тяжёлую задачу в асинхронную очередь.
Очередь подходит не всегда
Для генерации отчёта или обработки документа запрос можно поставить в очередь и выполнить позже.
Но в интерактивном чате ожидание в несколько минут хуже честного сообщения об ошибке. Поэтому очередь должна быть частью продуктового сценария, а не универсальным способом спрятать недоступность провайдера.
Что сохранять в логах
Чтобы circuit breaker не превратился в чёрный ящик, я сохраняю:
— причину изменения состояния; — долю ошибок в текущем окне; — количество запросов и таймаутов; — значение Retry-After; — время открытия и закрытия; — результаты half-open запросов; — долю трафика, ушедшего на fallback; — изменение стоимости и latency после переключения.
Отдельный алерт нужен не только на ошибки, но и на слишком долго открытый breaker. Иначе резервный провайдер может незаметно стать основным на несколько дней.
Итоговая схема
Retry нужен для единичного кратковременного сбоя.
Circuit breaker нужен, чтобы перестать отправлять запросы в сервис, который системно деградировал.
Fallback нужен, чтобы сохранить функциональность.
Bulkhead и лимиты нужны, чтобы переключение не положило резервный маршрут.
А общий дедлайн не позволяет всей этой логике превратить один запрос пользователя в минуту ожидания.
Поэтому правильный вопрос — не «сколько ретраев делать», а «в какой момент перестать ретраить и исключить провайдера из маршрутизации».
Какой порог вы используете для открытия circuit breaker: процент ошибок, рост latency или сочетание нескольких сигналов?