Как работает Circuit Breaker и какие у него состояния
Предохранитель размыкается при одновременном выполнении двух условий:
🟢 Junior Level
Circuit Breaker (Предохранитель) работает как конечный автомат (State Machine), отслеживающий результаты всех вызовов к удалённому ресурсу. Его главная задача — вовремя разорвать связь с нестабильным сервисом и не допустить истощения системных ресурсов.
┌────────────────────────┐
│ CLOSED │◄──────────────┐
│ (Обычная работа, │ │
│ вызовы разрешены) │ │
└───────────┬────────────┘ │
failureRate > threshold │ │
OR slowCalls > threshold │ │ Все N пробных вызовов
▼ │ успешны
┌────────────────────────┐ │
│ OPEN │ │
│ (Цепь разомкнута, │ │
│ мгновенный Fail-Fast)│ │
└───────────┬────────────┘ │
│ waitDuration истек │
▼ │
┌────────────────────────┐ │
│ HALF-OPEN │ │
│ (Проверка: N вызовов) ├───────────────┘
└───────────┬────────────┘
│ Хотя бы 1 ошибка / превышен порог
└──► (Возврат в OPEN)
Три основных состояния:
- CLOSED (Замкнут): Нормальное состояние. Все вызовы беспрепятственно проходят к целевому микросервису. Внутри ведётся учёт статистики (успехи, ошибки, время ответа).
- OPEN (Разомкнут): Предохранитель «сработал». Вызовы к внешнему сервису запрещены. Клиент мгновенно получает исключение (
CallNotPermittedException) или управление передаётся в запасной метод (Fallback). - HALF-OPEN (Полуразомкнут): Пробное состояние. Предохранитель пропускает строго фиксированное число тестовых запросов, чтобы проверить, оправился ли сервис от сбоя.
🟡 Middle Level
1. Точные правила переходов состояний
А. Переход CLOSED $\to$ OPEN
Предохранитель размыкается при одновременном выполнении двух условий:
- Количество вызовов в скользящем окне $\ge$
minimumNumberOfCalls. - Процент ошибок $\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 параллельных запросов:
- Ровно 10 первых потоков резервируют слоты и отправляются в реальный сервис.
- Оставшиеся 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/slidingWindowSizeminimumNumberOfCallsfailureRateThreshold(обычно 50%)slowCallDurationThreshold&slowCallRateThresholdwaitDurationInOpenStatepermittedNumberOfCallsInHalfOpenState
Красные флаги (Чего говорить НЕЛЬЗЯ):
- ❌ «В состоянии OPEN запросы ждут, пока сервис поднимется» (Нет, запросы мгновенно отклоняются без ожидания).
- ❌ «В состоянии HALF-OPEN трафик делится 50 на 50» (Нет, пропускается строго фиксированное небольшое количество пробных запросов, например 5 или 10, остальные реджектятся).
- ❌ «Любое исключение Java размыкает цепь» (Бизнес-ошибки вроде 400 или 404 должны игнорироваться).