Circuit Breaker — предохранитель

Circuit Breaker быстро отклоняет обращения к нестабильной зависимости, чтобы не тратить threads, connections и latency budget на заведомо неуспешные попытки. Он не исправляет dependency и не заменяет timeout.

Состояния

stateDiagram-v2
    [*] --> Closed
    Closed --> Open: порог ошибок/медленных вызовов
    Open --> HalfOpen: истёк интервал ожидания
    HalfOpen --> Closed: пробные вызовы успешны
    HalfOpen --> Open: пробный вызов неуспешен
  • Closed — запросы проходят, breaker собирает статистику.
  • Open — запрос сразу получает контролируемую ошибку или fallback.
  • Half-open — пропускается ограниченное число probes.

Порог считают по окну, например последним N calls или временному интервалу. До минимального числа вызовов процент ошибок неинформативен.

Порядок защит

caller deadline
  → bulkhead/concurrency limit
  → circuit breaker
  → ограниченный retry
  → per-attempt timeout
  → downstream

Реальный порядок зависит от библиотеки, но нужно понимать, что именно breaker считает одним call: каждую попытку или весь retry. Retry внутри breaker может задержать открытие; retry снаружи может многократно получить open error.

Что считать ошибкой

Обычно учитываю:

  • connect/read timeout;
  • network error;
  • 5xx и 429, если контракт считает их временными;
  • slow call выше порога.

Ожидаемые 4xx клиента обычно не говорят о здоровье dependency. Если считать их failures, breaker откроется из-за неверных запросов.

Fallback

Fallback обязан сохранять смысл:

  • отдать last known value с признаком устаревания;
  • вернуть частичный результат, если контракт это допускает;
  • поставить команду в очередь;
  • явно сообщить temporary unavailable.

Нельзя молча вернуть пустой список: клиент примет недоступность за отсутствие данных.

Пример конфигурации

resilience:
  downstream:
    timeout: 800ms
    minimum-calls: 20
    failure-rate-threshold: 50
    open-state: 10s
    half-open-probes: 3

Числа — не рецепт. Их вывожу из SLO, нормальной latency и допустимого traffic. Метрики: состояние breaker, число rejected calls, error/slow-call rate, результат probes и latency dependency.

Связь с идемпотентностью

Breaker не решает неопределённый результат timeout. Если remote side effect успел выполниться, повтор создаст дубль. Для command нужны idempotency key или проверяемый operation status. Для чтения retry обычно безопаснее, но всё равно усиливает нагрузку.

Типичные сбои

  • Нет timeout: зависший call не возвращается, breaker медленно получает статистику.
  • Один breaker используется для независимых endpoints с разными профилями отказа.
  • Слишком короткий open interval создаёт постоянные probes.
  • Каждый instance одновременно переходит в half-open и устраивает всплеск.
  • Fallback скрывает повреждение данных.
  • После открытия breaker очередь запросов продолжает неограниченно расти выше.

Вопросы для собеседования

Чем circuit breaker отличается от retry? Retry повторяет попытку, breaker временно прекращает обращения к нестабильной зависимости.

Зачем half-open? Ограниченно проверить восстановление, не возвращая сразу весь traffic.

Почему timeout нужен отдельно? Breaker реагирует на результаты calls; timeout ограничивает длительность одной попытки.

Следует ли считать 404 failure breaker? Обычно нет: это валидный прикладной ответ, а не недоступность dependency.

Что опасного в fallback? Он может скрыть отказ и вернуть семантически ложные данные.

Как breaker защищает систему? Ограничивает каскадное потребление ресурсов и даёт dependency время восстановиться.

См. Микросервисная архитектура, REST, Отличия POST и PUT и PATCH методов HTTP.