Що таке патерн Circuit Breaker
У житлових будинках електричний автомат у щитку розмикає коло при короткому замиканні або перевантаженні мережі, щоб проводка не зайнялася. У мікросервісах Circuit Breaker робит...
🟢 Junior Level
Circuit Breaker («Автоматичний вимикач» / «Запобіжник») — це архітектурний патерн стабільності розподілених систем, який запобігає каскадному падінню сервісів при збої або деградації однієї із залежностей.
Без Circuit Breaker (Каскадний збій):
[Клієнти] ──► [Order Service] ──(Таймаут 5 сек)──► [Payment Service (ЗАВИС)]
│ (Усі 200 потоків Tomcat зайняті очікуванням)
▼
[ПАДІННЯ ВСІЄЇ СИСТЕМИ 💥]
З Circuit Breaker (Fail-Fast):
[Клієнти] ──► [Order Service] ──► [Circuit Breaker: OPEN ⚡]
│ (Миттєве повернення помилки / Fallback за 1 мс)
▼
[Швидка відмова / Graceful Degradation ✅]
Основна аналогія:
У житлових будинках електричний автомат у щитку розмикає коло при короткому замиканні або перевантаженні мережі, щоб проводка не зайнялася.
У мікросервісах Circuit Breaker робить те саме: якщо залежний сервіс починає віддавати помилки або зависати, запобіжник розмикає коло (переходить у стан OPEN) і негайно відхиляє наступні запити (Fail-Fast), не витрачаючи ресурси на безглузде очікування тайм-аутів.
Три базові стани:
- CLOSED (Замкнений): Нормальний режим. Запити вільно відправляються залежному сервісу. Метрики збираються.
- OPEN (Розімкнений): Виявлено аварію. Запити до віддаленого сервісу не відправляються взагалі. Одразу викликається запасний сценарій (Fallback) або викидається виняток (
CallNotPermittedException). - HALF-OPEN (Напіврозімкнений): Час очікування минув. Запобіжник пропускає обмежену кількість пробних запитів. Якщо вони пройшли успішно — сервіс відновився (
CLOSED). Якщо впали — коло знову розмикається (OPEN).
🟡 Middle Level
1. Реалізація на Spring Boot та Resilience4j
Resilience4j — сучасний легковажний стандарт відмовостійкості для Java (який прийшов на заміну застарілому Netflix Hystrix).
@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentClient {
private final RestClient restClient;
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResponse processPayment(PaymentRequest request) {
return restClient.post()
.uri("https://payment-service/api/v1/charge")
.body(request)
.retrieve()
.body(PaymentResponse.class);
}
// Fallback-метод: ЗОБОВ'ЯЗАНИЙ мати ту саму сигнатуру параметрів + Throwable наприкінці
public PaymentResponse paymentFallback(PaymentRequest request, Throwable ex) {
log.warn("Сервіс платежів недоступний: {}. Застосовується деградація.", ex.getMessage());
return PaymentResponse.builder()
.status(PaymentStatus.DEFERRED_PENDING) // Переведення у відкладену обробку
.orderId(request.orderId())
.build();
}
}
2. Конфігурація в application.yml
resilience4j:
circuitbreaker:
instances:
paymentService:
slidingWindowType: COUNT_BASED # Облік за кількістю викликів
slidingWindowSize: 20 # Аналіз останніх 20 викликів
minimumNumberOfCalls: 10 # Мінімальна кількість викликів для включення розрахунків
failureRateThreshold: 50 # Відсоток помилок для розмикання (50%)
slowCallRateThreshold: 70 # Відсоток повільних відповідей (70%)
slowCallDurationThreshold: 2000ms # Виклик вважається повільним, якщо триває > 2 сек
waitDurationInOpenState: 15s # Час знаходження в стані OPEN
permittedNumberOfCallsInHalfOpenState: 5 # Пробних викликів у стані HALF-OPEN
automaticTransitionFromOpenToHalfOpenEnabled: true
3. Розмежування: Circuit Breaker vs Retry vs Rate Limiter vs Bulkhead
| Патерн | Основна мета | Що робить при збої |
|---|---|---|
| Circuit Breaker | Запобігання каскадним аваріям та захист завислої системи | Розмикає коло і моментально відхиляє нові виклики (Fail-Fast) |
| Retry | Подолання короткочасних мережевих флуктуацій | Повторює запит, що впав, через інтервал часу |
| Rate Limiter | Контроль пропускної здатності (RPS) та захист від перевантаження | Обмежує вхідну частоту запитів (Leaky Bucket / Token Bucket) |
| Bulkhead | Ізоляція ресурсів (пулів потоків, семафорів) | Обмежує кількість паралельних потоків, виділених на один сервіс |
Небезпека: Використання Retry БЕЗ Circuit Breaker під час деградації віддаленого сервісу призводить до Retry Storm (лавини повторів), яка остаточно «добиває» вмираючий сервіс мільйонами повторних запитів.
🔴 Senior Level
1. Повільні відповіді (Slow Calls) vs Явні помилки (Failures)
Головний убивця мікросервісів у продакшені — це НЕ швидкі помилки HTTP 500, а повільні відповіді (Slow Calls):
- Якщо сервіс $B$ моментально повертає
500 Internal Server Error, сервіс $A$ звільняє потік Tomcat за 5 мілісекунд. - Якщо сервіс $B$ зависає на 15 секунд через блокування в БД, кожен запит утримує потік ОС/Tomcat протягом 15 секунд.
- При пулі у 200 потоків (
server.tomcat.threads.max=200) і вхідному трафіку 50 req/sec, весь пул потоків сервісу A буде вичерпано рівно за 4 секунди. Сервіс $A$ перестає відповідати на Health Checks від Kubernetes (Liveness/Readiness probe) і йде в рестарт.
Тому сучасний Circuit Breaker зобов’язаний відстежувати не лише failureRateThreshold, але й slowCallRateThreshold.
2. Локальний (In-Memory) vs Розподілений Circuit Breaker
Чому Resilience4j зберігає метрики ковзного вікна локально в пам’яті кожного пода (JVM), а не в спільному розподіленому кеші (наприклад, Redis)?
In-Memory (Рекомендований підхід):
[Pod 1 (Local CB)] ──► [Ext Service]
[Pod 2 (Local CB)] ──► [Ext Service]
[Pod 3 (Local CB)] ──► [Ext Service]
(Немає залежності від Redis, нульові накладні витрати на мережу)
Розподілений через Redis (Антипатерн):
[Pod 1] ───(Кожен запит пише в Redis)───► [Redis Cluster]
(Якщо Redis загальмує — Circuit Breaker сам стане причиною аварії!)
- Накладні витрати: Перевірка стану запобіжника на кожен виклик через мережу в Redis додасть $1\text{–}2$ мс затримки на кожен міжсервісний запит.
- Single Point of Failure: Падіння загального Redis паралізує виконання всіх захисних механізмів системи.
- Особливість локального підходу: При горизонтальному автоскейлінгу (HPA в K8s) новий под стартує зі станом
CLOSED(холодний старт). Тому важливо грамотно виставлятиminimumNumberOfCalls, щоб запобігти передчасним хибним спрацьовуванням.
3. Порядок застосування аспектів (Aspect Order Pitfall)
У Spring Boot при одночасному використанні @CircuitBreaker, @Retry та @Bulkhead порядок застосування Spring AOP аспектів має вирішальне значення:
Правильний порядок (ззовні всередину):
[Вхідний виклик]
│
▼
1. @Bulkhead ──► Обмежує паралелізм (не дає зайняти всі потоки)
│
▼
2. @CircuitBreaker ──► Перевіряє стан кола (CLOSED/OPEN)
│
▼
3. @Retry ──► Виконує повтори ТІЛЬКИ якщо Circuit Breaker замкнений
│
▼
4. [Цільовий метод]
Якщо помилково помістити @CircuitBreaker всередині @Retry, то кожен із 3 повторів обліковуватиметься Circuit Breaker’ом як окрема помилка, що штучно завищить відсоток збоїв утричі та призведе до хибного розмикання кола.
4 Tricky Questions
1. Чому повільні відповіді (Slow Calls) для мікросервісів небезпечніші за миттєві помилки 500?
Відповідь: Миттєва помилка 500 швидко звільняє потік виконання (Thread) і мережеве з’єднання. Повільна відповідь (наприклад, зависання на 10–30 секунд через дедлок у БД) тримає потік заблокованим. У серверах із моделлю Thread-per-Request (Tomcat) пул потоків (за замовчуванням 200) вичерпується за лічені секунди при помірному трафіку. У результаті сервіс перестає приймати будь-які запити (включно з незв’язаними та Health Check’ами) і падає від starvation (голодування потоків).
2. Що станеться, якщо Fallback-метод сам почне виконувати мережевий запит і впаде?
Відповідь: Це грубе порушення патерну відмовостійкості. Fallback повинен мати гарантовану доступність:
- Якщо Fallback викликає сторонній сервіс без власного захисту, виникає вторинна каскадна аварія.
- Виняток, викинутий із Fallback-методу, спливає назовні до клієнта, що викликав метод, нівелюючи всю користь запобіжника. Правило: Fallback має бути виключно in-memory операцією (повернення дефолтного значення зі статичної константи, читання з локального кешу Caffeine, генерація заглушки або асинхронне скидання повідомлення в локальний буфер/диск).
3. Як правильно тестувати Circuit Breaker в інтеграційних тестах?
Відповідь: Для тестування переходів станів використовують:
- MockServer / WireMock: Імітація повернення HTTP 500 або штучної затримки (
withFixedDelay(3000)). - Пряме керування CircuitBreakerRegistry: У тестах можна програмно перемикати стани:
circuitBreakerRegistry.circuitBreaker("paymentService").transitionToOpenState(); - Перевірка того, що при відкритому стані метод цільового сервісу взагалі не викликається (WireMock верифікує
0 requests received), а керування миттєво передається у Fallback.
4. Чому Circuit Breaker не захищає від вичерпання пам’яті (OutOfMemoryError) при використанні неблокуючого Reactive-стеку (WebFlux)?
Відповідь:
У WebFlux потоки не блокуються в очікуванні I/O, тому вичерпання пулу потоків Event Loop (Netty) не відбувається. Проте, якщо залежний сервіс зависає, тисячі незавершених реактивних ланцюжків (Mono/Flux) із буферами даних накопичуються в Heap-пам’яті JVM. Якщо вхідний потік запитів високий, а тайм-аути великі, реактивний сервіс упаде з OutOfMemoryError.
Circuit Breaker у реактивному коді (transformDeferred(CircuitBreakerOperator.of(...))) захищає від накопичення очікуючих реактивних підписок, негайно розриваючи стрім за тайм-аутом оператора.
🎯 Шпаргалка для інтерв’ю
Головне:
- Circuit Breaker захищає викликаючий сервіс від каскадного падіння при зависанні або збоях залежностей (Fail-Fast).
- Три стани: CLOSED (норма), OPEN (коло розімкнене, запити не йдуть, викликається Fallback), HALF-OPEN (перевірка здоров’я сервісу тестовими викликами).
- Ключовий тригер: Не лише помилки (
failureRateThreshold), але й затримки відповідей (slowCallRateThreshold). - Стандарт де-факто в Java — Resilience4j (анотація
@CircuitBreaker). - Circuit Breaker працює локально в пам’яті JVM, а не через розподілений Redis.
Архітектурна зв’язка стабільності:
\(\text{Bulkhead (Ізоляція)} \to \text{Circuit Breaker (Fail-Fast)} \to \text{Retry (Повтори)} \to \text{Fallback (Деградація)}\)
Червоні прапорці (Чого говорити НЕ МОЖНА):
- ❌ «Circuit Breaker робить повторні спроби запиту» (Повтори робить Retry; Circuit Breaker, навпаки, забороняє запити).
- ❌ «Ми синхронізуємо стан Circuit Breaker між усіма подами через Redis» (Додає критичний мережевий hop та єдину точку відмови).
- ❌ «У Fallback-методі ми звертаємося до запасної бази даних через мережу» (Fallback зобов’язаний бути надійною та швидкою in-memory дією).