Späť na blog

Circuit Breaker Anti-Patterns: Keď Ochrana Spôsobuje Výpadky

|
| resilience, microservices, circuit-breaker, fault-tolerance, distributed-systems

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:

  1. Použi oddelené breakery per endpoint
  2. Nastav realistické thresholdy - nie príliš citlivé
  3. Vždy maj fallbacky - cache, defaults, graceful degradation
  4. Testuj half-open opatrne - postupné zotavenie
  5. Vrstvuj timeouty správne - HTTP < TimeLimiter < SlowCall

Testuj svoje circuit breakery s reálnymi zlyhaniami pred produkciou.


Súvisiace články

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "Circuit Breaker Anti-Patterns: Keď Ochrana Spôsobuje Výpadky". https://www.michal-drozd.com/sk/blog/circuit-breaker-anti-patterns/ (Publikované 29. decembra 2025).