Ratenbegrenzung, Circuit Breaker und Wiederholungsstürme: Aufbau robuster APIs
Ihre API verarbeitet problemlos 1.000 Anfragen pro Sekunde. Dann fällt ein nachgelagerter Dienst aus, die Clients starten erneute Versuche, und plötzlich werden Sie mit 50.000 Anfragen pro Sekunde überschwemmt. So verhindern Sie Kettenausfälle.
Die Flut von Wiederholungsversuchen, die die Produktion lahmlegte
Folgendes ist in einem realen System passiert: Eine Datenbankreplik fiel für zwei Minuten aus. Die API gab daraufhin 500-Fehler zurück. Jeder Client verfügte über eine Wiederholungslogik: drei Wiederholungsversuche ohne Backoff. 1.000 Clients × 3 Wiederholungsversuche = 3.000 Anfragen pro Sekunde, die auf einen ohnehin schon überlasteten Server trafen. Die Primärdatenbank konnte diesen Ansturm nicht bewältigen. Nun war alles ausgefallen.
Dies wird als „Retry Storm“ bezeichnet und ist die häufigste Ursache für Kettenausfälle in verteilten Systemen.
Lösung 1: Exponentieller Backoff mit Jitter
Versuchen Sie es niemals sofort erneut. Verwenden Sie einen exponentiellen Backoff (bei jedem erneuten Versuch länger warten) mit zufälligem Jitter (damit nicht alle Clients gleichzeitig einen erneuten Versuch unternehmen):
import random
import time
import httpx
def fetch_with_retry(url, max_retries=3):
for attempt in range(max_retries):
try:
response = httpx.get(url, timeout=5.0)
response.raise_for_status()
return response.json()
except (httpx.HTTPStatusError, httpx.TimeoutException) as e:
if attempt == max_retries - 1:
raise
# Exponential backoff: 1s, 2s, 4s
base_delay = 2 ** attempt
# Jitter: randomize to prevent thundering herd
jitter = random.uniform(0, base_delay)
delay = base_delay + jitter
print(f"Retry {attempt + 1}/{max_retries} in {delay:.1f}s")
time.sleep(delay)Lösung 2: Muster für den Leistungsschalter
Ein Circuit Breaker protokolliert Fehler. Nach Überschreiten eines Schwellenwerts (z. B. 5 Fehler innerhalb von 30 Sekunden) „öffnet“ er sich und gibt sofort Fehler zurück, ohne den nachgeschalteten Dienst aufzurufen. Nach einer Abkühlphase lässt er eine Anfrage durch, um zu prüfen, ob sich der Dienst wieder erholt hat.
import time
from enum import Enum
class CircuitState(Enum):
CLOSED = 'closed' # normal operation
OPEN = 'open' # failing, reject all requests
HALF_OPEN = 'half_open' # testing recovery
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_timeout=30):
self.state = CircuitState.CLOSED
self.failure_count = 0
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.last_failure_time = None
def call(self, func, *args, **kwargs):
if self.state == CircuitState.OPEN:
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
else:
raise CircuitBreakerOpen("Service unavailable")
try:
result = func(*args, **kwargs)
self._on_success()
return result
except Exception as e:
self._on_failure()
raise
def _on_success(self):
self.failure_count = 0
self.state = CircuitState.CLOSED
def _on_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
# Usage
payment_breaker = CircuitBreaker(failure_threshold=5)
try:
result = payment_breaker.call(stripe.PaymentIntent.create, amount=1000)
except CircuitBreakerOpen:
return Response(
{'error': 'Payment service temporarily unavailable'},
status=503
)Lösung 3: Serverseitige Ratenbegrenzung
Schützen Sie Ihre API sowohl vor legitimer Überbeanspruchung als auch vor böswilligem Missbrauch. Nutzen Sie einen auf Redis basierenden Rate-Limiter mit gleitendem Zeitfenster:
# Django middleware: Redis-backed sliding window rate limiter
import time
import redis
class RateLimitMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.redis = redis.Redis(host='redis', port=6379)
def __call__(self, request):
client_ip = request.META.get('HTTP_X_FORWARDED_FOR',
request.META['REMOTE_ADDR'])
key = f'rate_limit:{client_ip}'
# Sliding window: 100 requests per 60 seconds
now = time.time()
pipe = self.redis.pipeline()
pipe.zremrangebyscore(key, 0, now - 60) # remove old entries
pipe.zadd(key, {str(now): now}) # add current request
pipe.zcard(key) # count requests
pipe.expire(key, 60) # auto-cleanup
_, _, request_count, _ = pipe.execute()
if request_count > 100:
return JsonResponse(
{'error': 'Rate limit exceeded. Try again in 60s.'},
status=429,
headers={'Retry-After': '60'}
)
return self.get_response(request)Die Checkliste zur Resilienz
Client-seitig: Exponentieller Backoff + Jitter bei allen Wiederholungsversuchen. Zeitlimits für Anfragen festlegen (niemals unbegrenzt warten). Circuit Breaker für jede externe Abhängigkeit implementieren.
Serverseitig: Ratenbegrenzung pro IP-Adresse und pro API-Schlüssel. Graceful Degradation (Rückgabe von zwischengespeicherten Daten bei langsamer Datenbank). Endpunkte für Zustandsprüfungen für Lastenausgleichsserver. Bulkhead-Muster (Trennung kritischer Dienste von nicht kritischen Diensten).
Infrastruktur: Automatische Skalierung basierend auf der Länge der Anforderungswarteschlange, nicht nur auf der CPU-Auslastung. Separate Lesereplikate für Endpunkte mit hohem Leseaufkommen. CDN für statische Inhalte und zwischenspeicherbare API-Antworten.
Alles geht ständig kaputt. Die Frage ist nicht, ob deine Abhängigkeiten ausfallen, sondern ob dein System damit problemlos umgeht, wenn es doch passiert.
— Werner Vogels, Technischer Leiter bei Amazon
