WAF++ WAF++
Back to WAF++ Homepage

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