Як працює 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 мають ігноруватися).