Что такое паттерн 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’ом как отдельная ошибка, что искусственно завысит процент сбоев в 3 раза и приведет к ложному размыканию цепи.
4 Tricky Questions
1. Почему медленные ответы (Slow Calls) для микросервисов опаснее, чем мгновенные ошибки 500?
Ответ: Мгновенная ошибка 500 быстро освобождает поток исполнения (Thread) и сетевое соединение. Медленный ответ (например, зависание на 10–30 секунд из-за deadlock в БД) держит поток заблокированным. В серверах с моделью 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 действием).