Best Practice: Circuit Breaker, Timeouts & Bulkheads
Kontext
Cascading Failures sind die häufigste Ursache großer Cloud-Outages. Eine langsame oder fehlerhafte Abhängigkeit ohne Circuit Breaker erschöpft schrittweise Thread Pools, Connection Pools und Request Queues des abhängigen Services – bis dieser ebenfalls ausfällt.
Häufige Probleme ohne strukturierte Resilience Patterns:
-
Eine langsame externe API lässt alle API-Handler-Threads auf Timeout warten
-
Database Connection Pool erschöpft sich und blockiert alle Services, die denselben Pool nutzen
-
Retry Storms: 1000 Clients versuchen gleichzeitig ihre fehlgeschlagenen Requests zu wiederholen
-
Optionaler Enrichment-Service fällt aus und reißt den gesamten Haupt-Service mit
Zugehörige Controls
-
WAF-REL-050 – Circuit Breaker & Timeout Configuration
-
WAF-REL-080 – Dependency & Upstream Resilience Management
Zielbild
-
Jeder ausgehende Call hat explizite Timeouts
-
Kritische Abhängigkeiten haben Circuit Breaker mit definierten Schwellenwerten
-
Retry-Logik verhindert Storms durch Exponential Backoff mit Jitter
-
Bulkheads isolieren verschiedene Abhängigkeitsklassen in separate Resource Pools
Technische Umsetzung
Python: Circuit Breaker mit pybreaker
import pybreaker
import httpx
import asyncio
import logging
from datetime import datetime
# Circuit Breaker konfigurieren
payment_gateway_cb = pybreaker.CircuitBreaker(
fail_max=5, # Nach 5 Fehlern: OPEN
reset_timeout=30, # Nach 30s: HALF-OPEN (ein Test-Request)
name="payment-gateway"
)
# Event-Listener für Logging
@payment_gateway_cb.on_state_change
def log_state_change(cb, old_state, new_state):
logging.warning(f"CircuitBreaker[{cb.name}]: {old_state} -> {new_state}")
async def charge_card(card_token: str, amount: float) -> dict:
"""Zahlung mit Circuit Breaker und Timeout."""
try:
# Circuit Breaker wrapping + Timeout
async with httpx.AsyncClient(timeout=httpx.Timeout(3.0)) as client:
response = await payment_gateway_cb.call_async(
client.post,
"https://payment-gateway.example.com/charge",
json={"card_token": card_token, "amount": amount}
)
return response.json()
except pybreaker.CircuitBreakerError:
# Circuit ist OPEN: Sofortige Ablehnung, keine Wartezeit
logging.warning("Payment gateway circuit open – fast failing")
raise ServiceUnavailableError("Payment gateway temporarily unavailable")
except httpx.TimeoutException:
# Timeout überschritten
raise ServiceTimeoutError("Payment gateway timeout after 3s")
# Retry mit Exponential Backoff + Jitter
async def charge_with_retry(card_token: str, amount: float) -> dict:
max_attempts = 3
for attempt in range(max_attempts):
try:
return await charge_card(card_token, amount)
except ServiceTimeoutError:
if attempt == max_attempts - 1:
raise
# Exponential Backoff + Jitter
wait = (2 ** attempt) + (asyncio.get_event_loop().time() % 1)
await asyncio.sleep(wait)
raise ServiceUnavailableError("Max retry attempts exceeded")
Java/Spring Boot: Resilience4j
// application.yml
resilience4j:
circuitbreaker:
instances:
payment-gateway:
registerHealthIndicator: true
slidingWindowSize: 10
minimumNumberOfCalls: 5
permittedNumberOfCallsInHalfOpenState: 2
automaticTransitionFromOpenToHalfOpenEnabled: true
waitDurationInOpenState: 30s
failureRateThreshold: 50 # 50% Fehlerrate → OPEN
slowCallDurationThreshold: 2s
slowCallRateThreshold: 80
retry:
instances:
payment-gateway:
maxAttempts: 3
waitDuration: 100ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
randomizedWaitFactor: 0.5 # Jitter ±50%
bulkhead:
instances:
payment-gateway:
maxConcurrentCalls: 10 # Max. gleichzeitige Calls
maxWaitDuration: 100ms
// PaymentService.java
@Service
public class PaymentService {
@CircuitBreaker(name = "payment-gateway", fallbackMethod = "paymentFallback")
@Retry(name = "payment-gateway")
@Bulkhead(name = "payment-gateway")
public ChargeResult chargeCard(String cardToken, BigDecimal amount) {
return paymentGatewayClient.charge(cardToken, amount);
}
public ChargeResult paymentFallback(String cardToken, BigDecimal amount,
CallNotPermittedException ex) {
// Circuit offen: Offline-Queue für spätere Verarbeitung
offlineQueue.enqueue(cardToken, amount);
return ChargeResult.queued("Payment queued for processing");
}
}
Istio: Service Mesh Circuit Breaking
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: payment-gateway
namespace: payment
spec:
host: payment-gateway.payment.svc.cluster.local
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
connectTimeout: 3s
http:
http1MaxPendingRequests: 50
http2MaxRequests: 100
maxRequestsPerConnection: 1000
maxRetries: 3
retryOn: "5xx,gateway-error,connect-failure,retriable-4xx"
retryRemoteStatuses: "500,502,503"
outlierDetection:
consecutive5xxErrors: 5 # 5 Fehler → Ausschluss
interval: 10s # Bewertungsfenster
baseEjectionTime: 30s # Minimum Ausschlusszeit
maxEjectionPercent: 50 # Max. 50% der Hosts ausschließen
splitExternalLocalOriginErrors: true
Terraform: ALB mit Timeout
resource "aws_lb" "api" {
name = "payment-api-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.public_subnet_ids
idle_timeout = 30 # Für REST APIs: 30s; Nicht Default 60s
tags = var.mandatory_tags
}
resource "aws_lb_target_group" "api" {
name = "payment-api-tg"
port = 8080
protocol = "HTTP"
vpc_id = var.vpc_id
deregistration_delay = 30 # Graceful shutdown Fenster
health_check {
enabled = true
path = "/health/ready"
interval = 15
timeout = 5
healthy_threshold = 2
unhealthy_threshold = 3
matcher = "200"
}
}
Typische Fehlmuster
-
Circuit Breaker schützt alle Calls inkl. lesender Operationen: Zu aggressiv – schreibende und lesende Calls separat konfigurieren
-
Retry ohne Jitter: Alle Clients retrien zur gleichen Zeit → Retry Storm
-
Zu kurzer Reset-Timeout: Circuit wechselt zu schnell zu HALF-OPEN → weitere Fehler
-
Connection Pool für alle DBs geteilt: Ein langsamer Query erschöpft Pool für alle anderen Services
Metriken
-
Circuit Breaker Open Rate: Anzahl Minuten pro Stunde im OPEN-State (Ziel: < 1 min/h)
-
Timeout Rate: % der Calls, die auf Timeout laufen (Ziel: < 0.1%)
-
Retry Rate: % der Calls, die mindestens einmal wiederholt wurden (Ziel: < 5%)
-
Bulkhead Rejection Rate: % der Calls, die durch Bulkhead abgelehnt wurden
Reifegrad
Level 1 – Keine Timeouts, keine Circuit Breaker
Level 2 – Basis-Timeouts konfiguriert
Level 3 – Circuit Breaker für alle kritischen Abhängigkeiten; Retry mit Backoff
Level 4 – Bulkheads per Abhängigkeitsklasse; Service Mesh verwaltet CB deklarativ
Level 5 – Adaptive Thresholds; Request Hedging für latenz-kritische Pfade