Circuit Breaker Anti-Patterns: Keď Ochrana Spôsobuje Výpadky
Raz sme si vypadok zhorsili len tym, ze sme dali circuit breaker na zle miesto. Circuit breaker má byť poistka. Zle nastavený circuit breaker je skôr generátor výpadkov — a to je ten nepríjemný paradox.
Videl som incident v platbách, kde drobný problém na jednom endpointe (getBalance) otvoril zdieľaný breaker… a zrazu prestali fungovať aj úplne zdravé operácie ako charge() a refund(). Breaker spravil presne to, čo sme mu povedali — len nie to, čo sme chceli.
V tomto článku prejdem 5 anti-patternov pri circuit breakeroch, ktoré sa mi opakovane objavujú v produkcii, a ukážem, ako ich nastaviť tak, aby zvyšovali odolnosť (nie chaos): zdieľané breakery, príliš citlivé thresholdy, agresívny half-open, chýbajúce fallbacky a slabé testovanie.
Testované s: Resilience4j 2.1, Hystrix (legacy), Go kit circuit breaker
Anti-Pattern 1: Zdieľaný Circuit Breaker
Problém
// ZLE: Jeden breaker pre všetky endpointy
@Bean
public CircuitBreaker paymentCircuitBreaker() {
return CircuitBreaker.of("payment-service", config);
}
// Používaný pre všetky payment operácie
paymentClient.charge(amount); // Používa ten istý breaker
paymentClient.refund(txId); // Používa ten istý breaker
paymentClient.getBalance(userId); // Používa ten istý breaker
// Čo sa stane:
// 1. getBalance endpoint má bug, zlyháva 100%
// 2. Zdieľaný breaker sa otvorí
// 3. charge() a refund() tiež prestanú fungovať!
// 4. Všetky payment operácie zlyhajú kvôli zdravým endpointom
Riešenie
// SPRÁVNE: Oddelené breakery per operácia/endpoint
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
return CircuitBreakerRegistry.of(defaultConfig);
}
public void charge(BigDecimal amount) {
CircuitBreaker breaker = registry.circuitBreaker("payment-charge");
breaker.executeSupplier(() -> paymentClient.charge(amount));
}
public void refund(String txId) {
CircuitBreaker breaker = registry.circuitBreaker("payment-refund");
breaker.executeSupplier(() -> paymentClient.refund(txId));
}
// Teraz: zlyhania getBalance neovplyvňujú charge/refund
Anti-Pattern 2: Príliš Nízky Threshold
Problém
// ZLE: Otvára sa pri akomkoľvek zlyhaní
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(1) // 1% failure otvára breaker
.slidingWindowSize(10) // Len 10 requestov v okne
.build();
// Realita:
// 1 zlyhanie v 10 requestoch = 10% failure rate
// 10 requestov pri 1000 RPS = 10ms dát
// Jediný network blip → breaker sa otvorí → 30s výpadok
Riešenie
// SPRÁVNE: Rozumné thresholdy
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 50% zlyhaní na otvorenie
.slowCallRateThreshold(80) // 80% pomalých volaní
.slowCallDurationThreshold(Duration.ofSeconds(2))
.slidingWindowType(SlidingWindowType.TIME_BASED)
.slidingWindowSize(30) // 30 sekundové okno
.minimumNumberOfCalls(20) // Potreba 20+ volaní
.waitDurationInOpenState(Duration.ofSeconds(10))
.permittedNumberOfCallsInHalfOpenState(5)
.build();
Anti-Pattern 3: Žiadna Fallback Stratégia
Problém
// ZLE: Breaker sa otvorí, request zlyhá
public Order getOrder(String orderId) {
return circuitBreaker.executeSupplier(
() -> orderService.getOrder(orderId)
);
// Keď je breaker otvorený: CallNotPermittedException
// User vidí: 500 Internal Server Error
}
Riešenie
// SPRÁVNE: Zmysluplné fallbacky
public Order getOrder(String orderId) {
return circuitBreaker.executeSupplier(
() -> orderService.getOrder(orderId),
throwable -> getFallbackOrder(orderId, throwable)
);
}
private Order getFallbackOrder(String orderId, Throwable t) {
if (t instanceof CallNotPermittedException) {
// Breaker je otvorený - vráť cachované dáta
return orderCache.get(orderId);
}
if (t instanceof TimeoutException) {
// Timeout - vráť čiastočné dáta
return Order.builder()
.id(orderId)
.status(OrderStatus.UNKNOWN)
.message("Detaily objednávky dočasne nedostupné")
.build();
}
throw new ServiceUnavailableException("Order service down", t);
}
Anti-Pattern 4: Príliš Agresívne Testovanie v Half-Open
Problém
// ZLE: Half-open okamžite posiela traffic
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.waitDurationInOpenState(Duration.ofSeconds(60))
.permittedNumberOfCallsInHalfOpenState(100) // 100 requestov!
.build();
// Čo sa stane:
// 1. Breaker sa otvorí kvôli zlyhaniam
// 2. Po 60s, pošle 100 requestov na test
// 3. Ak služba stále obnovuje, 100 userov zasiahnutých
// 4. Breaker sa okamžite znova otvorí
// 5. Opakuje sa - služba sa nikdy nezotaví kvôli záťaži
Riešenie
// SPRÁVNE: Postupné zotavenie
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.waitDurationInOpenState(Duration.ofSeconds(30)) // Kratšie čakanie
.permittedNumberOfCallsInHalfOpenState(3) // Len 3 sondy
.build();
Anti-Pattern 5: Ignorovanie Timeout Konfigurácie
Problém
// ZLE: Default timeouty ktoré nesedia s SLA
@Bean
public WebClient paymentClient() {
return WebClient.builder()
.baseUrl("http://payment-service")
// Žiadny timeout - defaultuje na nekonečno!
.build();
}
// Čo sa stane:
// 1. Payment service visí 30 sekúnd
// 2. Request nie je počítaný ako "pomalý" lebo nedokončil
// 3. Thread pool sa vyčerpá pred reakciou breakera
// 4. Connection pool sa vyčerpá
// 5. Kaskádové zlyhanie cez všetky služby
Riešenie
// SPRÁVNE: Vrstvená timeout stratégia
@Bean
public WebClient paymentClient() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000) // 2s connect
.responseTimeout(Duration.ofSeconds(5)); // 5s response
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
// Circuit breaker so zodpovedajúcou konfiguráciou
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.slowCallDurationThreshold(Duration.ofSeconds(3)) // Nižšie ako HTTP timeout
.slowCallRateThreshold(50)
.build();
Monitoring
# Prometheus alerty pre circuit breaker problémy
- alert: CircuitBreakerOpen
expr: |
resilience4j_circuitbreaker_state{state="open"} == 1
for: 1m
labels:
severity: warning
annotations:
summary: "Circuit breaker {{ $labels.name }} je otvorený"
- alert: CircuitBreakerHighFailureRate
expr: |
resilience4j_circuitbreaker_failure_rate > 25
for: 5m
labels:
severity: warning
annotations:
summary: "Circuit breaker {{ $labels.name }} má >25% failure rate"
Checklist
## Circuit Breaker Konfigurácia
### Dizajn
- [ ] Oddelené breakery per operácia/endpoint
- [ ] Definovaný fallback pre každý breaker
- [ ] Zdokumentované očakávané failure modes
### Thresholdy
- [ ] Nastav failure rate threshold 25-50%
- [ ] Konfiguruj slow call threshold podľa SLO
- [ ] Použi minimum call count pred vyhodnotením
- [ ] Time-based sliding window pre variabilnú záťaž
### Zotavenie
- [ ] Wait duration zodpovedá recovery času služby
- [ ] Half-open permits max 3-5 testovacích volaní
- [ ] Zváž exponential backoff pre nestabilné služby
Záver
Circuit breakery môžu zhoršiť situáciu:
- Použi oddelené breakery per endpoint
- Nastav realistické thresholdy - nie príliš citlivé
- Vždy maj fallbacky - cache, defaults, graceful degradation
- Testuj half-open opatrne - postupné zotavenie
- Vrstvuj timeouty správne - HTTP < TimeLimiter < SlowCall
Testuj svoje circuit breakery s reálnymi zlyhaniami pred produkciou.
Súvisiace články
- gRPC Deadline Propagácia - Spracovanie timeoutov
- Database Connection Pool Vyčerpanie - Vyčerpanie zdrojov
Súvisiace články
gRPC Deadline Propagácia: Prevencia Kaskádových Zlyhaní
Frontend sa vzdá po 5s ale backend pracuje ďalších 30s. Bez deadline propagácie mrháte resources na odsúdené requesty. Ukážem ako to implementovať v Go.
Circuit Breaker vs Rate Limiter vs Bulkhead: Kedy Ktorý Pattern Použiť
Tri resilience patterns, ktoré sa často zamieňajú. Ukážem presne kedy ktorý bráni cascade failures a kedy to zhoršuje so skutočnými metrikami.
Elasticsearch Hot Shard Problém: Keď Jeden Node Robí Všetku Prácu
5 data nodov ale jeden je na 100% CPU. Nerovnomerné routing kľúče vytvárajú hot shardy. Ukážem ako detekovať skew a opraviť ho pomocou routing stratégií.
gRPC v Kubernetes: Prečo Service round-robin klame
Prečo má jeden pod 90% trafficu pri gRPC. Reprodukovateľný lab, riešenia od client-side LB po service mesh, a production checklist.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.