Circuit Breaker vs Rate Limiter vs Bulkhead: Kedy Ktorý Pattern Použiť
Pridali sme vsetky tri patterny a aj tak prisiel pager; problem bol v ich kombinacii. “Pridajme circuit breaker všade.” Takto vytvoríš systém, ktorý zlyháva kreatívnymi spôsobmi, ktoré nikto nepredvídal.
Circuit breaker, rate limiter a bulkhead riešia rôzne problémy. Použitie zlého z nich robí tvoj systém menej odolný, nie viac.
Testované na: Resilience4j 2.2, Spring Boot 3.2, Java 21, Prometheus + Grafana
Definícia Problémov
Circuit Breaker: Fail Fast pre Downstream Failures
Účel: Prestaň volať failing service
Chráni: TVOJ systém pred plytvaním zdrojmi na beznádejné volania
Trigger: Downstream failures (5xx, timeouty)
Bez circuit breaker:
Service A → Service B (down)
→ Threads blokované čakajúc na timeout
→ Thread pool vyčerpaný
→ Service A tiež zlyhá (kaskáda)
S circuit breaker:
Service A → Circuit OPEN → Okamžité zlyhanie (bez čakania)
→ Zdroje zachované
→ Service A pokračuje v práci (degraded)
Rate Limiter: Ochrana pred Preťažením
Účel: Limituj rýchlosť prichádzajúcich requestov
Chráni: DOWNSTREAM systém pred preťažením
Trigger: Príliš veľa requestov (bez ohľadu na úspech/zlyhanie)
Bez rate limiter:
100,000 RPS → Service → Database
→ Database preťažená
→ Všetko zlyhá
S rate limiter:
100,000 RPS → Rate Limiter (1000 RPS) → Service → Database
→ 99,000 zamietnutých okamžite
→ Database happy, 1000 requestov uspeje
Bulkhead: Izolácia Failure Domén
Účel: Izoluj zdroje per závislosť
Chráni: Systém pred jednou závislosťou, ktorá spotrebuje všetky zdroje
Trigger: Vyčerpanie zdrojov (threads, connections)
Bez bulkhead:
Service A → Service B (slow) → Používa všetkých 200 threads
→ Service C (fast) → Žiadne threads dostupné!
→ B aj C volania zlyhajú
S bulkhead:
Service A → Service B → Bulkhead (50 threads max)
→ Service C → Bulkhead (50 threads max)
→ Service B slow? Len 50 threads blokovaných
→ Service C stále funguje!
Rozhodovacia Matica
| Scenár | Circuit Breaker | Rate Limiter | Bulkhead |
|---|---|---|---|
| Downstream service zlyháva | ✅ | ❌ | ❌ |
| Príliš veľa prichádzajúcich requestov | ❌ | ✅ | ❌ |
| Jedna pomalá závislosť | ❌ | ❌ | ✅ |
| Prevencia kaskádového zlyhania | ✅ | ❌ | ✅ |
| Ochrana zdieľaného zdroja | ❌ | ✅ | ✅ |
| Client-side ochrana | ✅ | ❌ | ✅ |
| Server-side ochrana | ❌ | ✅ | ❌ |
Implementácia s Resilience4j
Circuit Breaker
// CircuitBreakerConfig.java
@Configuration
public class CircuitBreakerConfig {
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
// Otvor circuit po 50% zlyhaní
.failureRateThreshold(50)
// ... v posledných 10 volaniach
.slidingWindowType(SlidingWindowType.COUNT_BASED)
.slidingWindowSize(10)
// Zostaň open 30 sekúnd
.waitDurationInOpenState(Duration.ofSeconds(30))
// Povoľ 5 testovacích volaní v half-open stave
.permittedNumberOfCallsInHalfOpenState(5)
// Tieto zaznamenaj ako zlyhania
.recordExceptions(IOException.class, TimeoutException.class)
// Tieto ignoruj
.ignoreExceptions(BusinessException.class)
.build();
return CircuitBreakerRegistry.of(config);
}
}
// PaymentService.java
@Service
public class PaymentService {
private final CircuitBreaker circuitBreaker;
private final PaymentClient paymentClient;
public PaymentService(CircuitBreakerRegistry registry, PaymentClient client) {
this.circuitBreaker = registry.circuitBreaker("payment-service");
this.paymentClient = client;
}
public PaymentResult processPayment(PaymentRequest request) {
return circuitBreaker.executeSupplier(() ->
paymentClient.charge(request)
);
}
// S fallback
public PaymentResult processPaymentWithFallback(PaymentRequest request) {
return Try.ofSupplier(
CircuitBreaker.decorateSupplier(circuitBreaker,
() -> paymentClient.charge(request))
).recover(CallNotPermittedException.class,
ex -> PaymentResult.queued("Circuit open, queued for retry")
).recover(Exception.class,
ex -> PaymentResult.failed(ex.getMessage())
).get();
}
}
Rate Limiter
// RateLimiterConfig.java
@Configuration
public class RateLimiterConfig {
@Bean
public RateLimiterRegistry rateLimiterRegistry() {
RateLimiterConfig config = RateLimiterConfig.custom()
// 100 requestov za sekundu
.limitForPeriod(100)
.limitRefreshPeriod(Duration.ofSeconds(1))
// Čakaj max 500ms na permit
.timeoutDuration(Duration.ofMillis(500))
.build();
return RateLimiterRegistry.of(config);
}
}
// ApiController.java
@RestController
public class ApiController {
private final RateLimiter rateLimiter;
public ApiController(RateLimiterRegistry registry) {
this.rateLimiter = registry.rateLimiter("api");
}
@GetMapping("/data")
public ResponseEntity<Data> getData() {
return RateLimiter.decorateSupplier(rateLimiter, () ->
ResponseEntity.ok(dataService.fetch())
).get();
}
}
// Alebo s anotáciami (Spring Boot)
@Service
public class DataService {
@RateLimiter(name = "api", fallbackMethod = "fallback")
public Data fetch() {
return repository.findAll();
}
public Data fallback(RequestNotPermitted ex) {
throw new TooManyRequestsException("Rate limit exceeded");
}
}
Bulkhead
// BulkheadConfig.java
@Configuration
public class BulkheadConfig {
@Bean
public ThreadPoolBulkheadRegistry bulkheadRegistry() {
ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.custom()
// Max 10 concurrent volaní
.maxThreadPoolSize(10)
.coreThreadPoolSize(5)
// Queue 20 čakajúcich volaní
.queueCapacity(20)
// Drž idle threads 60s
.keepAliveDuration(Duration.ofSeconds(60))
.build();
return ThreadPoolBulkheadRegistry.of(config);
}
}
// ExternalServices.java
@Service
public class ExternalServices {
private final ThreadPoolBulkhead paymentBulkhead;
private final ThreadPoolBulkhead inventoryBulkhead;
private final ThreadPoolBulkhead notificationBulkhead;
public ExternalServices(ThreadPoolBulkheadRegistry registry) {
// Každý service má izolovaný thread pool
this.paymentBulkhead = registry.bulkhead("payment");
this.inventoryBulkhead = registry.bulkhead("inventory");
this.notificationBulkhead = registry.bulkhead("notification");
}
public CompletableFuture<PaymentResult> processPayment(Order order) {
return paymentBulkhead.executeSupplier(() ->
paymentClient.charge(order)
);
}
public CompletableFuture<InventoryResult> reserveInventory(Order order) {
return inventoryBulkhead.executeSupplier(() ->
inventoryClient.reserve(order)
);
}
}
Kombinovanie Patterns
Správne: Circuit Breaker + Bulkhead
// Správna kombinácia: CB vnútri Bulkhead
@Service
public class OrderService {
private final CircuitBreaker circuitBreaker;
private final ThreadPoolBulkhead bulkhead;
public CompletableFuture<OrderResult> createOrder(Order order) {
// 1. Bulkhead limituje concurrent volania (resource isolation)
// 2. Circuit breaker zlyháva rýchlo ak je service down
Supplier<OrderResult> decoratedSupplier =
CircuitBreaker.decorateSupplier(circuitBreaker, () ->
externalService.process(order)
);
return bulkhead.executeSupplier(decoratedSupplier);
}
}
Správne: Rate Limiter na Entry Point
// Rate limiter na API gateway / controller úrovni
@RestController
@RateLimiter(name = "api") // Prvá línia obrany
public class OrderController {
@PostMapping("/orders")
public Order createOrder(@RequestBody OrderRequest request) {
// CB + Bulkhead vnútri pre downstream volania
return orderService.create(request);
}
}
Zle: Rate Limiter pre Downstream Failures
// ZLE: Rate limiter tu nepomôže!
@Service
public class PaymentService {
@RateLimiter(name = "payment") // Nepomôže ak je payment service DOWN
public PaymentResult charge(Payment payment) {
return paymentClient.charge(payment); // Zlyhá bez ohľadu na rate
}
// SPRÁVNE: Použi circuit breaker
@CircuitBreaker(name = "payment")
public PaymentResult chargeCorrect(Payment payment) {
return paymentClient.charge(payment);
}
}
Benchmarky
Scenár: Payment Service Spadne
Setup:
- 100 RPS prichádzajúcich
- Payment service: 30s timeout → potom zlyhá
- Thread pool: 200 threads
BEZ akéhokoľvek pattern:
- Všetkých 200 threads blokovaných čakajúcich na timeout
- Po 2 sekundách: thread pool vyčerpaný
- Všetky requesty zlyhajú (vrátane non-payment)
S Circuit Breaker:
- Prvých 10 volaní: timeout (30s každé)
- Circuit sa otvorí po 50% zlyhaní
- Následné volania: okamžité zlyhanie (< 1ms)
- Thread pool usage: max 10 threads
- Non-payment endpointy: neovplyvnené
S Bulkhead:
- Payment bulkhead: 20 threads max
- Všetkých 20 threads blokovaných (30s timeout)
- Non-payment endpointy: fungujú (180 threads dostupných)
- ALE: payment requesty stále čakajú 30s pred zlyhaním
S Circuit Breaker + Bulkhead:
- Prvých 10 volaní: timeout v bulkhead (max 20 threads)
- Circuit sa otvorí
- Následné volania: okamžité zlyhanie
- Non-payment endpointy: neovplyvnené
- Thread usage: minimálne
Výsledky Load Testu
Target: 1000 RPS, Payment service down
| Konfigurácia | p99 Latency | Error Rate | Thread Usage |
|--------------|-------------|------------|--------------|
| Bez ochrany | 30,000ms | 100% | 200/200 |
| Len CB | 50ms | 100%* | 15/200 |
| Len Bulkhead | 30,000ms | 100% | 20/200 |
| CB + Bulkhead | 50ms | 100%* | 10/200 |
| Rate Limiter | 30,000ms | 100% | 200/200 |
* Errors sú rýchle zlyhania, nie timeouty
Monitoring
Prometheus Metriky
// Resilience4j auto-exponuje metriky
// application.yml
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true
instances:
payment:
baseConfig: default
management:
metrics:
tags:
application: order-service
Kľúčové Metriky
# Circuit Breaker stav
resilience4j_circuitbreaker_state{name="payment"}
# 0=CLOSED, 1=OPEN, 2=HALF_OPEN
# Failure rate
resilience4j_circuitbreaker_failure_rate{name="payment"}
# Bulkhead dostupné threads
resilience4j_bulkhead_available_concurrent_calls{name="payment"}
# Rate limiter dostupné permits
resilience4j_ratelimiter_available_permissions{name="api"}
Alerty
groups:
- name: resilience
rules:
- alert: CircuitBreakerOpen
expr: resilience4j_circuitbreaker_state == 1
for: 1m
labels:
severity: warning
annotations:
summary: "Circuit breaker {{ $labels.name }} je OPEN"
- alert: BulkheadSaturated
expr: resilience4j_bulkhead_available_concurrent_calls < 2
for: 5m
labels:
severity: warning
annotations:
summary: "Bulkhead {{ $labels.name }} blízko kapacity"
Checklist
## Výber Resilience Pattern
### Circuit Breaker - Použi keď:
- [ ] Downstream service môže zlyhať alebo byť pomalý
- [ ] Chceš rýchle zlyhanie namiesto timeout
- [ ] Kaskádové zlyhania sú riziko
### Rate Limiter - Použi keď:
- [ ] Ochrana pred traffic spikes
- [ ] API má usage kvóty
- [ ] Database/service má kapacitný limit
### Bulkhead - Použi keď:
- [ ] Viaceré závislosti zdieľajú thread pool
- [ ] Jedna pomalá závislosť nemá ovplyvniť ostatné
- [ ] Je potrebná resource izolácia
### Pravidlá Kombinácie:
- [ ] Circuit Breaker: per downstream service
- [ ] Rate Limiter: na API entry point
- [ ] Bulkhead: izoluj rôzne závislosti
- [ ] Nikdy: Rate Limiter pre downstream failures
Záver
Tri patterns, tri rôzne účely:
- Circuit Breaker: Downstream service zlyháva → fail fast
- Rate Limiter: Príliš veľa prichádzajúcej traffic → zamietni prebytok
- Bulkhead: Izoluj zlyhania → jedna závislosť nemôže spotrebovať všetky zdroje
Použitie zlého patternu je horšie ako žiadny pattern.
Súvisiace články
- Connection Pool Sizing s Little’s Law - Pool sizing
- K8s CPU Throttling Pitva - Resource limity
Súvisiace články
Java Virtual Threads vs Reactive: Kedy Zahodiť WebFlux za Project Loom
Virtual Threads v Java 21 sľubujú jednoduchší kód ako Reactive. Benchmarkujem oba pri 10k concurrent connections a ukážem kde ktorý vyhráva.
Circuit Breaker Anti-Patterns: Keď Ochrana Spôsobuje Výpadky
Circuit breakery bránia kaskádovým zlyhaniam ale zlá konfigurácia ich zhoršuje. Ukážem 5 anti-patternov: zdieľané breakery, zlé thresholdy, žiadny fallback.
Keď Prepared Statements Spravia PostgreSQL 10× Pomalším: Generic Plan Trap
Rovnaký query, rovnaké parametre, ale prod je pomalý a staging funguje. Ukážem ako reprodukovať generic plan problém s pgBouncer, Java/Go a ako ho fixnúť.
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.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.