🏗️ Розділ 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 мають ігноруватися).

Пов’язані теми: