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 randomimport
timeimport
httpxdef 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
# Exponentieller Backoff: 1s, 2s, 4s
base_delay = 2 ** attempt
# Jitter: Zufallswert, um „Thundering Herd“ zu vermeiden jitter
= random.uniform(0, base_delay)
delay = base_delay + jitter
print(f"Wiederholung {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 timefrom
enum import
Enumclass CircuitState(Enum):
CLOSED = 'closed' # Normalbetrieb
OPEN = 'open' # Fehlerzustand, alle Anfragen ablehnen
HALF_OPEN = 'half_open' #
Wiederherstellungstestclass 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#
Verwendungpayment_breaker = CircuitBreaker(failure_threshold=5)
try:
result = payment_breaker.call(stripe.PaymentIntent.create, amount=1000)
except CircuitBreakerOpen:
return Response(
{'error': 'Zahlungsdienst vorübergehend nicht verfügbar'},
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-gestützter Rate-Limiter mit gleitendem
Fenster 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}'
# Gleitendes Fenster: 100 Anfragen pro 60 Sekunden
now = time.time()
pipe = self.redis.pipeline()
pipe.zremrangebyscore(key, 0, now - 60) # alte Einträge
entfernen pipe.zadd(key, {str(now): now}) # aktuelle Anfrage
hinzufügen pipe.zcard(key) # Anfragen zählen
pipe.expire(key, 60) # automatische Bereinigung
_, _, 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
