🏗️ Раздел 17 · Вопрос #6

Как работает Circuit Breaker и какие у него состояния

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


🟢 Junior Level

Circuit Breaker (Предохранитель) работает как конечный автомат (State Machine), отслеживающий результаты всех вызовов к удалённому ресурсу. Его главная задача — вовремя разорвать связь с нестабильным сервисом и не допустить истощения системных ресурсов.

                    ┌────────────────────────┐
                    │         CLOSED         │◄──────────────┐
                    │  (Обычная работа,      │               │
                    │   вызовы разрешены)    │               │
                    └───────────┬────────────┘               │
       failureRate > threshold  │                            │
       OR slowCalls > threshold │                            │ Все N пробных вызовов
                                ▼                            │ успешны
                    ┌────────────────────────┐               │
                    │          OPEN          │               │
                    │  (Цепь разомкнута,     │               │
                    │   мгновенный Fail-Fast)│               │
                    └───────────┬────────────┘               │
                                │ waitDuration истек         │
                                ▼                            │
                    ┌────────────────────────┐               │
                    │       HALF-OPEN        │               │
                    │ (Проверка: N вызовов)  ├───────────────┘
                    └───────────┬────────────┘
                                │ Хотя бы 1 ошибка / превышен порог
                                └──► (Возврат в OPEN)

Три основных состояния:

  1. CLOSED (Замкнут): Нормальное состояние. Все вызовы беспрепятственно проходят к целевому микросервису. Внутри ведётся учёт статистики (успехи, ошибки, время ответа).
  2. OPEN (Разомкнут): Предохранитель «сработал». Вызовы к внешнему сервису запрещены. Клиент мгновенно получает исключение (CallNotPermittedException) или управление передаётся в запасной метод (Fallback).
  3. HALF-OPEN (Полуразомкнут): Пробное состояние. Предохранитель пропускает строго фиксированное число тестовых запросов, чтобы проверить, оправился ли сервис от сбоя.

🟡 Middle Level

1. Точные правила переходов состояний

А. Переход CLOSED $\to$ OPEN

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

  1. Количество вызовов в скользящем окне $\ge$ minimumNumberOfCalls.
  2. Процент ошибок $\ge$ failureRateThreshold (например, $\ge 50\%$) ИЛИ процент медленных запросов $\ge$ slowCallRateThreshold (например, $\ge 70\%$).

Б. Переход OPEN $\to$ HALF-OPEN

По истечении интервала ожидания waitDurationInOpenState (например, 10–30 секунд) предохранитель автоматически (или по первому пришедшему запросу) переходит в состояние HALF-OPEN.

В. Переход HALF-OPEN $\to$ CLOSED (Восстановление)

В состоянии HALF-OPEN разрешается выполнить строго permittedNumberOfCallsInHalfOpenState (например, 5 или 10) запросов:

  • Если процент ошибок и медленных ответов среди этих пробных вызовов ниже пороговых значений — цепь замыкается (CLOSED), сервис признан здоровым, кольцевой буфер метрик сбрасывается.

Г. Переход HALF-OPEN $\to$ OPEN (Повторный срыв)

Если среди пробных запросов процент ошибок превысил порог — предохранитель немедленно возвращается в OPEN, и таймер ожидания запускается заново.

2. Типы скользящих окон (Sliding Window)

Resilience4j поддерживает два типа скользящих окон для сбора статистики:

1. Count-Based (По количеству вызовов):
   [✓, ✓, ✗, ✓, ✗, ✗, ✓, ✓, ✓, ✗] -> Фиксированный буфер из 10 последних вызовов

2. Time-Based (По времени):
   |  1 сек  |  2 сек  |  3 сек  |  4 сек  | ... |  60 сек |
   | 10 reqs | 25 reqs | 0 reqs  | 40 reqs | ... | 15 reqs | -> Агрегация за N секунд
  • COUNT_BASED: Учитывает последние $N$ вызовов. Не зависит от времени. Если сервис делает 1 запрос в час, старая ошибка может висеть сутками.
  • TIME_BASED: Учитывает вызовы за последние $T$ секунд (разбитых на временные сегменты/бакеты). Ошибки автоматически «забываются», когда проходит временной интервал.

🔴 Senior Level

1. Дополнительные спец-состояния в Resilience4j

Помимо трёх классических, в продакшене используются специальные административные состояния:

  • DISABLED: Предохранитель полностью выключен. Запросы проходят всегда, метрики не собираются. Полезно при проведении нагрузочного тестирования.
  • METRICS_ONLY: Запросы никогда не блокируются, но метрики собираются и отправляются в Prometheus/Micrometer. Позволяет «пристрелять» конфигурацию порогов без риска сломать прод.
  • FORCED_OPEN: Принудительное размыкание вручную (через Spring Boot Actuator / JMX) инженером эксплуатации при обнаружении критической уязвимости или плановом обслуживании целевого сервиса.

2. Внутреннее устройство: Lock-Free кольцевой буфер (Ring Buffer)

Для обеспечения высокой производительности (миллионы вызовов в секунду) сбор метрик реализован без глобальных блокировок (synchronized):

  • Для COUNT_BASED используется битовый массив или кольцевой буфер фиксированного размера на базе AtomicIntegerArray / LongAdder.
  • Запись результата занимает $O(1)$ по времени и $O(1)$ по памяти.
  • Состояние Circuit Breaker хранится в AtomicReference<State>. Смена состояний происходит через атомарную операцию compareAndSet().

3. Конкурентность в состоянии HALF-OPEN

Критический нюанс высоконагруженных систем: Если в состоянии HALF-OPEN сконфигурировано permittedNumberOfCallsInHalfOpenState: 10, а в сервис одновременно прилетает 200 параллельных запросов:

  1. Ровно 10 первых потоков резервируют слоты и отправляются в реальный сервис.
  2. Оставшиеся 190 запросов НЕ ждут завершения пробных 10 вызовов. Они немедленно отклоняются с исключением CallNotPermittedException (или уходят в Fallback). Это предотвращает эффект лавины (Thundering Herd) на только что начавший оживать сервис.

4. Фильтрация исключений: Игнорируемые vs Записываемые ошибки

Не каждая ошибка должна приводить к размыканию цепи. Ошибки бизнес-логики (клиент прислал неверный пароль или несуществующий ID) — это нормальная работа системы, а не сбой сервиса:

resilience4j:
  circuitbreaker:
    instances:
      orderService:
        recordExceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException
          - org.springframework.web.client.HttpServerErrorException # 5xx ошибки
        ignoreExceptions:
          - com.example.exceptions.EntityNotFoundException         # 404 не ломает цепь
          - com.example.exceptions.InvalidInputException           # 400 не ломает цепь

4 Tricky Questions

1. Что произойдет, если в состоянии HALF_OPEN разрешено 5 запросов, 4 вернули успех, а 5-й завис по сетевому тайм-ауту на 60 секунд?

Ответ: Предохранитель не сможет принять решение о возврате в CLOSED до тех пор, пока все разрешенные вызовы (permittedNumberOfCallsInHalfOpenState) не будут завершены или прерваны. В течение этих 60 секунд все входящие клиентские запросы будут отсекаться с CallNotPermittedException. Защита: Обязательно настраивать связку с @TimeLimiter или HTTP-клиентским тайм-аутом (ReadTimeout), чтобы зависший запрос завершился ошибкой по жесткому тайм-ауту (например, 1–2 секунды), после чего Circuit Breaker вернется в OPEN.

2. Почему при низком трафике Circuit Breaker может вообще никогда не перейти в состояние OPEN, даже если все 100% запросов падают с ошибками?

Ответ: Это происходит из-за параметра minimumNumberOfCalls. Если minimumNumberOfCalls = 50, а за весь день сервис получил всего 49 запросов (пусть даже все 49 завершились с HTTP 500), процент сбоев не рассчитывается вовсе, и предохранитель останется в состоянии CLOSED. Вывод: На микросервисах с редким трафиком minimumNumberOfCalls необходимо снижать (до 5–10) либо использовать TIME_BASED окно.

3. Чем TIME_BASED скользящее окно принципиально отличается от COUNT_BASED при резком спаде трафика?

Ответ:

  • В COUNT_BASED окне (размером 100) зафиксированные 60 ошибок останутся в буфере навсегда, пока не придут новые вызовы. Если после инцидента трафик упал почти до нуля, сервис будет продолжать оставаться в OPEN или периодически срываться, вспоминая ошибки утренней аварии.
  • В TIME_BASED окне память ограничена временем (например, последние 60 секунд). По истечении минуты старые аварийные бакеты вымываются из памяти, позволяя сервису начать с чистого листа при восстановлении.

4. В чем разница между slowCallDurationThreshold в Circuit Breaker и сетевым SocketTimeout / ReadTimeout?

Ответ:

  • SocketTimeout / ReadTimeout принудительно обрывает соединение и выбрасывает SocketTimeoutException, когда истекает заданное время.
  • slowCallDurationThreshold в Circuit Breaker НЕ обрывает соединение. Запрос успешно доходит до конца, возвращает ответ клиенту, но предохранитель помечает этот вызов в своем скользящем окне как «медленный» (Slow Call). Если доля таких медленных вызовов превысит slowCallRateThreshold, цепь разомкнется.

🎯 Шпаргалка для интервью

Главное:

  • Circuit Breaker работает по логике конечного автомата (FSM).
  • CLOSED: Запросы идут. Если calls >= minCalls и процент ошибок/задержек выше порогов $\to$ переход в OPEN.
  • OPEN: Запросы сразу реджектятся (Fail-Fast). По истечении waitDurationInOpenState $\to$ переход в HALF-OPEN.
  • HALF-OPEN: Пропускает фиксированные $N$ пробных запросов (permittedCalls). Если всё ок $\to$ CLOSED; при ошибках $\to$ OPEN.
  • Два типа окон: COUNT_BASED (последние $N$ вызовов) и TIME_BASED (вызовы за последние $T$ секунд).
  • Ошибки клиента (HTTP 4xx) должны помещаться в ignoreExceptions, иначе баги фронтенда разорвут связь со здоровым бэкендом.

Ключевые параметры Resilience4j:

  • slidingWindowType / slidingWindowSize
  • minimumNumberOfCalls
  • failureRateThreshold (обычно 50%)
  • slowCallDurationThreshold & slowCallRateThreshold
  • waitDurationInOpenState
  • permittedNumberOfCallsInHalfOpenState

Красные флаги (Чего говорить НЕЛЬЗЯ):

  • ❌ «В состоянии OPEN запросы ждут, пока сервис поднимется» (Нет, запросы мгновенно отклоняются без ожидания).
  • ❌ «В состоянии HALF-OPEN трафик делится 50 на 50» (Нет, пропускается строго фиксированное небольшое количество пробных запросов, например 5 или 10, остальные реджектятся).
  • ❌ «Любое исключение Java размыкает цепь» (Бизнес-ошибки вроде 400 или 404 должны игнорироваться).

Связанные темы: