Read in English · Leer en español · Lire en français
Abschnitt 9.4 von 9.8 · Fortgeschritten · 32 Min. Lesezeit
Timeouts, Wiederholungen, Circuit Breaker, Bulkheads und Graceful Degradation
Videolektion
5 Min. · auf Englisch gesprochen, mit UntertitelnNur anhören (Audio)
Transkript lesen
Willkommen zu Abschnitt neun Punkt vier. In einem verteilten System ist immer irgendeine Abhängigkeit langsam oder fällt aus. Die Frage ist nicht, wie man Ausfälle vermeidet, sondern wie man verhindert, dass ein lokaler Ausfall zu einem globalen wird.
Stellen Sie sich vor, Sie rufen einen Lieferanten an, der nie abnimmt. Wenn Sie den ganzen Tag in der Leitung bleiben, erledigen Sie nichts anderes. Wenn alle Kollegen dasselbe tun, steht das ganze Büro still. Vernünftige Büros legen nach ein paar Klingeltönen auf. Das ist ein Timeout. Sie versuchen es etwas später erneut, aber nicht endlos. Das ist eine Wiederholung mit Pause. Sie rufen einen Lieferanten, der offensichtlich geschlossen hat, nicht mehr an und prüfen in einer Stunde erneut. Das ist ein Circuit Breaker. Sie sorgen dafür, dass ein Lieferant nicht alle Telefone belegen kann. Das ist ein Bulkhead. Und sie haben einen Plan B, etwa die Preisliste von gestern. Das ist ein Fallback. Softwareservices brauchen genau dieselben Gewohnheiten.
Darum ist das wichtig. Der Bestellservice der Essenslieferplattform hat zwölfhundert Threads auf sechs Instanzen. Zur Mittagszeit sind laut Littles Gesetz nur fünfzig belegt: fünfunddreißig für Checkouts, die den Zahlungsanbieter aufrufen, und fünfzehn für alles andere. Um zwölf Uhr einunddreißig antwortet der Zahlungsanbieter nicht mehr, hält die Verbindungen aber offen. Der Zahlungsclient hat keinen Timeout, also hält jeder Checkout seinen Thread unbegrenzt. Jede Sekunde hängen hundert weitere Threads fest. Innerhalb von etwa zwölf Sekunden wartet jeder Thread auf den Zahlungsanbieter. Jetzt scheitern auch Bestellhistorie, Warenkorb und Sendungsverfolgung, obwohl sie Zahlungen nie berühren. Ein Ausfall in fünfundzwanzig Prozent des Verkehrs hat hundert Prozent lahmgelegt. Mit einem Timeout von zwei Sekunden belegt der Checkout höchstens zweihundert Threads, sodass etwa neunhundertfünfundachtzig für alles andere bleiben. Nur der Checkout scheitert, und zwar eindeutig. Ein Bulkhead geht weiter. Die Begrenzung der Zahlungsaufrufe auf vierzig Threads pro Instanz garantiert mindestens neunhundertsechzig Threads für alles andere, unabhängig von den Timeouts.
Timeouts sollten entlang einer Aufrufkette kleiner werden. Eine Deadline macht das automatisch. Die mobile App setzt für die ganze Anfrage eine Deadline von tausend Millisekunden. Das Gateway verbraucht zwanzig Millisekunden und gibt die Restzeit weiter: neunhundertachtzig. Der Bestellservice verbraucht fünfzig, dann dreihundertzwanzig für andere Aufrufe, und gibt sechshundertzehn an die Preisgestaltung weiter. Die Preisgestaltung braucht etwa achthundert Millisekunden und schlägt daher sofort fehl, statt Arbeit zu leisten, auf die niemand wartet.
Wiederholungen verwandeln kurze Störungen in Erfolge. Aber Wiederholungen sind auch Last. Wiederholen Sie nur vorübergehende Fehler, etwa Timeouts oder ein fünf null drei, und nur idempotente Operationen, denn eine Anfrage mit Timeout kann erfolgreich gewesen sein. Wiederholen Sie zwei- oder dreimal und warten Sie jedes Mal länger: exponentielles Backoff, bis zu einer Obergrenze. Fügen Sie Jitter hinzu, eine zufällige Wartezeit. Sonst wiederholen tausend Clients, die gemeinsam gescheitert sind, auch gemeinsam, in Wellen, die die Abhängigkeit unten halten. Und Wiederholungen multiplizieren sich über Schichten. Drei Versuche auf drei Schichten ergeben siebenundzwanzig Aufrufe. Wiederholen Sie also nur auf einer Schicht und begrenzen Sie Wiederholungen auf etwa zehn Prozent der Anfragen.
Ein Circuit Breaker umhüllt Aufrufe an eine Abhängigkeit. Solange er geschlossen ist, gehen Aufrufe durch, und Fehler werden gezählt. Überschreiten die Fehler eine Schwelle, zum Beispiel die Hälfte der letzten zwanzig Aufrufe, öffnet der Breaker. Jetzt schlägt jeder Aufruf schnell fehl, ohne das Netzwerk zu berühren. Das schützt die Threads des Aufrufers und gibt der Abhängigkeit Raum zur Erholung. Nach einer Wartezeit, zum Beispiel dreißig Sekunden, wird er halboffen und lässt einige Testaufrufe durch. Gelingen sie, schließt er wieder. Scheitert ein Testaufruf, öffnet er sich für einen weiteren Zeitraum.
Ein Bulkhead ist nach den wasserdichten Abteilungen eines Schiffs benannt. Hier bekommt jede Abhängigkeit ihren eigenen begrenzten Thread-Pool. Der Zahlungsanbieter ist langsam, also füllt sich sein Pool, und weitere Zahlungsaufrufe werden sofort abgelehnt. Aber Restaurant- und Kurieraufrufe laufen normal weiter. Ein Timeout begrenzt, wie lange ein Aufruf Ressourcen halten kann. Ein Bulkhead begrenzt, wie viele gleichzeitig gehalten werden können.
Scheitert ein Aufruf, muss der Aufrufer trotzdem antworten. Ein Fallback kann die zuletzt bekannte Speisekarte sein, eine geschätzte Lieferzeit, beliebte Gerichte statt persönlicher Empfehlungen oder ein klarer, schneller Fehler. Das ist Graceful Degradation: Die Kernabläufe funktionieren weiter, während Extras zurücktreten. Ein Fallback muss einfach, unabhängig vom ausgefallenen Teil und regelmäßig getestet sein. Sicherheit ist die Ausnahme. Ist der Autorisierungsservice nicht erreichbar, ist der sichere Fallback, zu verweigern. Prüfen Sie schließlich, ob eine Operation idempotent ist, bevor Sie sie wiederholen. Eine Retry-Policy für einen Zahlungsaufruf ohne Idempotenzschlüssel ist kein Resilienzmuster. Sie ist ein Generator für Doppelabbuchungen.
Der vollständige Abschnitt ergänzt Hedged Requests und eine Checkliste für jeden Remote-Aufruf. Als Nächstes betrachten wir in Abschnitt neun Punkt fünf, wie gut gut genug ist. Bis gleich.
Was Sie wissen müssen
- Kaskadierende Ausfälle entstehen, wenn Aufrufer auf eine langsame Abhängigkeit warten, bis ihnen selbst Threads und Verbindungen ausgehen; jeder Integrationspunkt braucht gezielten Schutz (Nygard, Release It!).
- Setzen Sie explizite Connect- und Request-Timeouts anhand gemessener Latenzperzentile, die entlang der Aufrufkette kleiner werden, und geben Sie die verbleibende Deadline weiter, damit kein Service an einer aufgegebenen Anfrage arbeitet.
- Wiederholen Sie nur vorübergehende Fehler bei idempotenten Operationen, wenige Male, mit begrenztem exponentiellem Backoff und Full Jitter (Brooker, 2015).
- Wiederholungen multiplizieren sich über Schichten (drei Versuche auf drei Schichten ergeben 27 Aufrufe); wiederholen Sie nur auf einer Schicht und begrenzen Sie Wiederholungen mit einem Budget von etwa 10 % der Anfragen.
- Ein Circuit Breaker schlägt schnell fehl, solange eine Abhängigkeit ausgefallen ist: Er wechselt bei Fehlern von geschlossen zu offen und über halboffene Testaufrufe zurück zu geschlossen.
- Bulkheads geben jeder Abhängigkeit oder jedem Aufrufer einen eigenen begrenzten Pool, und Fallbacks halten die Kernabläufe am Laufen; Fallbacks müssen aber einfach, unabhängig vom ausgefallenen Teil und getestet sein und bei Sicherheitsentscheidungen geschlossen fehlschlagen (Fail-closed).
- Idempotenz macht Wiederholungen, Hedging und Replays sicher; Hedged Requests senken die Tail-Latenz für idempotente Lesezugriffe zu geringen, budgetierten Kosten an zusätzlicher Last.
Ausfälle sind normal; ihre Ausbreitung ist es nicht
In einem verteilten System ist immer irgendeine Abhängigkeit langsam oder fällt aus: Ein Zahlungsanbieter hat eine schlechte Minute, eine Datenbank macht ein Failover, eine Netzwerkverbindung verliert Pakete, ein Deployment geht schief. Abschnitt 6.1 hat gezeigt, warum sich solche Teilausfälle nicht verhindern lassen und warum ein Aufrufer oft nicht einmal erkennen kann, was passiert ist. Die architektonische Frage lautet daher nicht, wie man Ausfälle vermeidet, sondern wie man verhindert, dass ein lokaler Ausfall zu einem globalen wird. Ein kaskadierender Ausfall ist die typische Art, wie Systeme sterben: Eine langsame Abhängigkeit macht ihre Aufrufer langsam, deren Ressourcen füllen sich mit wartenden Anfragen, sie werden ihrerseits für ihre eigenen Aufrufer langsam, und binnen Sekunden hat ein Problem in einer unbedeutenden Komponente die ganze Plattform lahmgelegt.
Michael Nygard hat die Muster, die solche Ketten unterbrechen, in Release It! (2007; zweite Auflage 2018) katalogisiert und sich dabei auf Ausfälle gestützt, die er untersucht hatte. Seine zentrale Beobachtung ist, dass Integrationspunkte, also jede Stelle, an der ein System ein anderes aufruft, die Hauptquelle von Instabilität sind und dass jeder einzelne gezielten Schutz braucht. Die Muster in diesem Abschnitt (Timeouts, Deadlines, Wiederholungen mit Backoff, Circuit Breaker, Bulkheads, Fallbacks, Idempotenz und Hedging) sind der Standardwerkzeugkasten. Sie sind Taktiken im Sinne von Abschnitt 2.5: Jede ist eine einzelne Entscheidung, die auf Verfügbarkeit zielt, und jede hat ihren Preis.
Timeouts: das erste und wichtigste Muster
Ein Timeout begrenzt, wie lange ein Aufrufer wartet. Ohne Timeout hält ein Aufruf an eine Abhängigkeit, die nie antwortet, einen Thread, eine Verbindung und Speicher so lange, wie das Betriebssystem die Verbindung offen hält, und das können viele Minuten sein. Viele Client-Bibliotheken haben standardmäßig keinen Timeout (die weit verbreitete Python-Bibliothek requests wartet unbegrenzt, wenn man nichts anderes angibt); das Fehlen eines Timeouts ist also eine Entwurfsentscheidung durch Unterlassen. Das ausgearbeitete Beispiel unten zeigt die Folgen.
Meist kommt es auf zwei Timeouts an. Der Connect-Timeout begrenzt den Verbindungsaufbau und kann kurz sein, innerhalb eines Rechenzentrums wenige hundert Millisekunden, weil ein gesunder Server Verbindungen fast sofort annimmt. Der Request-Timeout begrenzt den gesamten Austausch und sollte aus Messungen abgeleitet werden: knapp über der hohen Perzentil-Latenz der Abhängigkeit (p99 oder p99.9) unter normaler Last, sodass er krankhafte Wartezeiten abschneidet, ohne gewöhnliche langsame Anfragen scheitern zu lassen. Er muss außerdem in das eigene Budget des Aufrufers passen. Gibt das Gateway nach 2 Sekunden auf, wartet der Bestellservice aber 10 Sekunden auf die Preisgestaltung, arbeitet der Bestellservice 8 Sekunden lang an einer Anfrage, auf die niemand wartet. Timeouts sollten daher entlang einer Aufrufkette kleiner werden, und das ist die Idee hinter Deadlines.
Deadlines und ihre Weitergabe
Eine Deadline ist ein absoluter Zeitpunkt, bis zu dem die gesamte Anfrage abgeschlossen sein muss. Deadline-Weitergabe gibt die verbleibende Zeit an jeden nachgelagerten Aufruf weiter, sodass jeder Service weiß, wie viel Budget noch übrig ist, statt einen eigenen festen Timeout anzuwenden. gRPC hat das eingebaut: Ein Client setzt eine Deadline, die verbleibende Zeit reist mit dem Aufruf mit, und ein Server kann prüfen, ob die Deadline überschritten ist, und die Arbeit einstellen. Über HTTP geben Teams einen Header mit den verbleibenden Millisekunden oder der absoluten Deadline weiter, idealerweise über dieselbe Middleware, die auch den Tracing-Kontext weitergibt (Abschnitt 9.6).
Die Weitergabe bringt zwei Vorteile. Ein Service kann schnell fehlschlagen, wenn die verbleibende Zeit offensichtlich zu kurz für die Arbeit ist, statt eine Datenbankabfrage zu starten, die nicht rechtzeitig fertig werden kann. Und wenn der Benutzer aufgibt, stoppt auch die Arbeit darunter, statt weiter Kapazität zu verbrauchen, und das genau während der Überlast, die die Verzögerung verursacht hat. Das ist Load Shedding toter Anfragen, einer der wirksamsten Schutzmechanismen, die ein System unter Stress haben kann.
Eine Deadline, die durch eine Aufrufkette weitergegeben wird
Ausgearbeitetes Beispiel
Ausgearbeitetes Beispiel: wie ein fehlender Timeout zum Totalausfall wird
Der Bestellservice der Essenslieferplattform läuft mit 6 Instanzen zu je 200 Worker-Threads: 1200 Threads. Zur Mittagszeit erhält er 400 Anfragen pro Sekunde: 100 Checkouts, die den Zahlungsanbieter aufrufen (normalerweise 300 ms, sodass ein Checkout seinen Thread insgesamt etwa 350 ms hält), und 300 andere Anfragen (Bestellhistorie, Warenkorb, Sendungsverfolgung), die 50 ms dauern. Für den Zahlungsclient ist kein Timeout konfiguriert. Um 12:31 antwortet der Zahlungsanbieter nicht mehr, hält die Verbindungen aber offen.
| Normal belegte Threads (Littles Gesetz) | Checkout 100 × 0,35 s = 35; übriger Verkehr 300 × 0,05 s = 15; insgesamt 50 von 1200 (etwa 4 %) |
|---|---|
| Nach 12:31 | Jeder Checkout hält seinen Thread nun unbegrenzt: Jede Sekunde hängen 100 weitere Threads fest |
| Zeit bis zur Erschöpfung | (1200 − 15 − 35) ÷ 100 ≈ 11,5 Sekunden: Innerhalb von etwa 12 Sekunden wartet jeder Thread auf den Zahlungsanbieter |
| Folge | Bestellhistorie, Warenkorb und Sendungsverfolgung, die Zahlungen nie aufrufen, warten jetzt auf Threads und laufen am Gateway in den Timeout. Ein Ausfall in 25 % des Verkehrs hat 100 % lahmgelegt |
| Und weiter oben | Die Verbindungen des Gateways zum Bestellservice stauen sich auf dieselbe Weise; Health Checks laufen in den Timeout, Instanzen werden als ungesund markiert, und wenn die Liveness-Probe betroffen ist, werden sie neu gestartet, kalt, in dieselben Bedingungen hinein |
| Mit einem Timeout von 2 Sekunden | Belegte Checkout-Threads: 100 × 2 s = 200 über die ganze Flotte (Littles Gesetz). Etwa 985 Threads bleiben für den übrigen Verkehr, also scheitert nur der Checkout, und zwar mit einem klaren Fehler nach 2 s |
| Ein Circuit Breaker kommt hinzu | Sobald etwa die Hälfte der letzten 20 Aufrufe gescheitert ist, öffnet der Breaker: Checkouts scheitern in Millisekunden und belegen fast keine Threads; ein Testaufruf alle 30 s erkennt die Erholung |
| Ein Bulkhead kommt hinzu | Die Begrenzung der Zahlungsaufrufe auf 40 Threads pro Instanz (240 insgesamt) garantiert mindestens 960 Threads für alles andere, unabhängig von den gesetzten Timeouts |
| Ein Fallback kommt hinzu | Der Checkout bietet dem Kunden eine andere Zahlungsart an oder erklärt, dass Kartenzahlungen verzögert sind, statt eines allgemeinen Fehlers |
Die Rechnung ist wieder Littles Gesetz: belegte Threads = Anfragerate × Zeit, die jede Anfrage einen Thread hält. Ohne Timeout ist die Haltezeit unbegrenzt, also auch die Zahl der benötigten Threads.
Timeouts begrenzen, wie lange; Bulkheads begrenzen, wie viele; Circuit Breaker beenden die Aufrufe ganz. Jedes Muster schließt eine andere Lücke, deshalb werden sie zusammen eingesetzt.
Nygards Release It! beschreibt echte Ausfälle, die genau diesem Muster folgten, oft ausgelöst durch einen Integrationspunkt, den niemand für kritisch hielt.
Erschöpfung des Thread-Pools
Wiederholungen mit exponentiellem Backoff und Jitter
Viele Fehler sind vorübergehend: ein verlorenes Paket, ein Verbindungsabbruch während eines Failovers, ein kurzes 503 von einer überlasteten Instanz. Eine Wiederholung (Retry) verwandelt sie in Erfolge, die der Benutzer nie bemerkt. Doch Wiederholungen sind auch Last, und schlecht entworfene Wiederholungen gehören zu den häufigsten Ursachen von Ausfällen. Vier Regeln machen sie sicher. Wiederholen Sie nur vorübergehende Fehler (Timeouts, Verbindungsfehler, 502, 503, 504 und 429 nach der angegebenen Wartezeit), niemals Fehler, die erneut auftreten werden, etwa 400 oder 404. Wiederholen Sie nur idempotente Operationen oder solche mit einem Idempotenzschlüssel, denn eine Anfrage mit Timeout kann trotzdem erfolgreich gewesen sein. Wiederholen Sie nur wenige Male, typischerweise zwei- oder dreimal. Und warten Sie zwischen den Versuchen, mit exponentiellem Backoff: einer Wartezeit, die sich mit jedem Versuch verdoppelt, bis zu einer Obergrenze.
Backoff allein reicht nicht. Scheitern tausend Clients im selben Moment, wie es bei einem kurzen Aussetzer einer Abhängigkeit geschieht, wiederholen sie alle nach 100 ms, dann alle nach 200 ms und treffen in synchronisierten Wellen ein, die die Abhängigkeit unten halten können. Marc Brookers Artikel Exponential Backoff And Jitter (AWS Architecture Blog, 2015) hat das simuliert und gezeigt, dass Zufall, Jitter, sowohl die Gesamtarbeit als auch die Zeit bis zum Abschluss stark verringert. Sein empfohlenes Full Jitter
wartet eine zufällige Zeit zwischen null und der exponentiellen Schranke: sleep = random(0, min(cap, base × 2attempt)). Jitter gehört zu allem, was viele Clients gleichzeitig tun: Wiederholungen, Neuverbindungen, Ablaufzeiten von Cache-Einträgen und geplante Jobs.
import random, time
class TransientError(Exception):
"""Timeouts, connection resets, 502, 503 and 504 responses."""
def call_with_retries(fn, attempts=3, base=0.1, cap=2.0):
for attempt in range(attempts):
try:
return fn()
except TransientError:
if attempt == attempts - 1:
raise # give up: let the caller decide
ceiling = min(cap, base * 2 ** attempt)
time.sleep(random.uniform(0, ceiling)) # full jitterRetry-Stürme und Retry-Budgets
Wiederholungen multiplizieren sich über Schichten. Angenommen, ein Tippen in der mobilen App läuft durch das API-Gateway, den Bestellservice und den Zahlungsclient, und jede der drei Schichten unternimmt bis zu drei Versuche. Ist der Zahlungsanbieter ausgefallen, erzeugt eine Benutzeraktion 3 × 3 × 3 = 27 Aufrufe an ihn, und ein Ausfall, der eine gleichmäßige Fehlerrate hätte bedeuten sollen, wird zu einem Retry-Sturm mit der 27-fachen normalen Last, der den Anbieter auch dann noch unten hält, wenn die ursprüngliche Ursache behoben ist. Benutzer, die erneut tippen, fügen eine vierte Schicht hinzu.
Drei Abwehrmaßnahmen wirken zusammen. Erstens: Wiederholen Sie nur auf einer Schicht, meist der, die der fehlschlagenden Abhängigkeit am nächsten ist, und lassen Sie die anderen Schichten den Fehler nach oben weiterreichen; Googles SRE-Buch empfiehlt, dass ein überlasteter Server das ausdrücklich meldet, damit Aufrufer weiter oben nicht wiederholen. Zweitens: Nutzen Sie ein Retry-Budget: Das SRE-Buch beschreibt, Wiederholungen nur zuzulassen, solange sie unter 10 % der Anfragen eines Clients liegen, was die zusätzliche Last durch Wiederholungen auf etwa das 1,1-Fache des Normalen begrenzt statt auf bis zum Dreifachen. AWS-SDKs implementieren ein ähnliches Kontingent an Retry-Tokens, und die Retry-Policy von gRPC enthält Throttling zum selben Zweck. Drittens: Kombinieren Sie Wiederholungen mit einem Circuit Breaker, damit eine Abhängigkeit, von der bekannt ist, dass sie ausgefallen ist, gar nicht erst erneut aufgerufen wird.
Circuit Breaker
Ein Circuit Breaker, von Nygard nach dem elektrischen Schutzschalter benannt, umhüllt Aufrufe an eine Abhängigkeit und beobachtet deren Ergebnisse. Solange die Abhängigkeit gesund ist, ist der Breaker geschlossen und Aufrufe gehen durch, während Fehler und Timeouts gezählt werden. Überschreiten die Fehler eine Schwelle, zum Beispiel die Hälfte der letzten 20 Aufrufe, öffnet der Breaker: Für einen festgelegten Zeitraum scheitert jeder Aufruf sofort, ohne das Netzwerk zu berühren, was die Threads des Aufrufers schützt und der Abhängigkeit Raum zur Erholung gibt. Läuft der Zeitraum ab, wird der Breaker halboffen und lässt einige Testaufrufe durch. Gelingen sie, schließt er; scheitert einer, öffnet er erneut für einen weiteren Zeitraum. Martin Fowlers Beschreibung von 2014 machte das Muster weithin bekannt, und Netflix’ Bibliothek Hystrix (heute im Wartungsmodus) machte es bei Microservices populär; Resilience4j für Java und Polly für .NET sind heute die gängigen Bibliotheken, und Service Meshes bieten verwandte Schutzmechanismen wie Nebenläufigkeitslimits und Outlier Detection (in Envoy heißen sie Circuit Breaking, obwohl sie anders funktionieren).
Der Zustandsautomat des Circuit Breaker
Die Einstellungen verdienen Überlegung. Was zählt als Fehler? Timeouts und 5xx-Antworten sollten zählen; 4xx-Antworten meist nicht, weil sie die Anfrage des Aufrufers widerspiegeln, nicht die Gesundheit der Abhängigkeit. Auch langsame Erfolge können zählen, sodass eine Abhängigkeit, die in 5 Sekunden antwortet, als fehlerhaft behandelt wird. Das Fenster muss groß genug sein, um nicht bei ein paar unglücklichen Aufrufen auszulösen (Bibliotheken verlangen eine Mindestzahl an Aufrufen), und der offene Zeitraum lang genug für die Erholung, aber kurz genug, um einen Ausfall nicht zu verlängern. Breaker sollten pro Abhängigkeit, manchmal pro Endpoint, eingerichtet werden, damit ein fehlerhafter Reporting-Endpoint nicht das Bestellen abschneidet. Wiederholungen sollten einen offenen Breaker als endgültige Antwort behandeln, nicht als etwas, das man wiederholt. Und jeder Zustandswechsel sollte geloggt und grafisch dargestellt werden, denn ein offener Breaker ist ein Symptom, das jemand sehen muss.
import time
class CircuitOpen(Exception):
pass
class CircuitBreaker:
def __init__(self, failure_threshold=5, reset_after=30.0):
self.failure_threshold = failure_threshold # failures in a row that trip it
self.reset_after = reset_after # seconds to stay open
self.failures, self.state, self.opened_at = 0, "closed", 0.0
def call(self, fn, *args, **kwargs):
if self.state == "open":
if time.monotonic() - self.opened_at < self.reset_after:
raise CircuitOpen("failing fast") # no remote call at all
self.state = "half-open" # let a trial call through
try:
result = fn(*args, **kwargs)
except Exception:
self.failures += 1
if self.state == "half-open" or self.failures >= self.failure_threshold:
self.state, self.opened_at = "open", time.monotonic()
raise
self.failures, self.state = 0, "closed" # success closes the circuit
return result
payments = CircuitBreaker()
# payments.call(provider.authorise, order_id, amount, timeout=2.0)Bulkheads
Der Rumpf eines Schiffs ist durch Schotten (Bulkheads) in wasserdichte Abteilungen geteilt, sodass ein Leck eine Abteilung flutet, statt das Schiff zu versenken. In Software partitioniert ein Bulkhead Ressourcen, sodass ein fehlerhafter oder überlasteter Teil nicht alles verbrauchen kann. Die häufigste Form gibt jeder Abhängigkeit einen eigenen begrenzten Thread-Pool, Connection-Pool oder Semaphor: Zahlungen dürfen höchstens 40 gleichzeitige Aufrufe pro Instanz nutzen, Restaurantabfragen 40, Kurierzuweisung 40. Werden Zahlungen langsam, füllt sich ihr Pool, und weitere Zahlungsaufrufe werden sofort abgelehnt, aber Restaurant- und Kurieraufrufe laufen weiter. Ein Timeout begrenzt, wie lange jeder Aufruf Ressourcen halten kann; ein Bulkhead begrenzt, wie viele gleichzeitig gehalten werden können, und schützt daher selbst dann, wenn Timeouts zu lang gesetzt sind.
Bulkheads wirken auch im größeren Maßstab. Getrennte Deployments desselben Service für verschiedene Aufrufer (ein Pool von Bestellservice-Instanzen für die Kunden-App, ein anderer für die Restaurantpartner-API) verhindern, dass eine fehlerhafte Partnerintegration die Kunden aushungert. Zellbasierte Architekturen, die Z-Achse des Scale Cube, machen jede Zelle zu einem Bulkhead für einen Teil der Kunden, und Shuffle Sharding, von Fachleuten bei AWS beschrieben, weist jedem Kunden eine zufällige kleine Kombination von Instanzen zu, sodass der giftige Verkehr eines Kunden nur sehr wenige andere trifft. Der Preis jedes Bulkheads ist Auslastung: Für eine Abteilung reservierte Kapazität kann nicht an eine andere verliehen werden, daher müssen Pools anhand des gemessenen Bedarfs dimensioniert werden.
Bulkheads innerhalb eines Service
Fallbacks und Graceful Degradation
Scheitert ein Aufruf oder ist ein Breaker offen, muss der Aufrufer trotzdem antworten. Ein Fallback legt fest, was er zurückgibt. Die Optionen, von der besten zur schlechtesten für den Benutzer, sind: ein zwischengespeicherter oder veralteter Wert (die zuletzt bekannte Speisekarte, mit Preisen, die im Bezahlvorgang bestätigt werden); ein sinnvoller Standardwert (eine geschätzte Lieferzeit von 30 bis 45 Minuten statt einer Live-Schätzung); eine eingeschränkte Funktion (beliebte Gerichte statt personalisierter Empfehlungen); die Arbeit aufschieben (eine Bewertung annehmen und veröffentlichen, wenn sich der Bewertungsservice erholt hat); und ein klarer, schneller Fehler, der dem Benutzer sagt, was zu tun ist. Diese pro Feature gemeinsam mit dem Product Owner zu entwerfen, ist das, was Graceful Degradation in der Praxis bedeutet: Die Kernabläufe (stöbern, bestellen, bezahlen, verfolgen) funktionieren weiter, während nicht wesentliche Features zurücktreten. Dasselbe Denken leitet Load Shedding (Abschnitt 9.3): Wissen Sie im Voraus, welche Features Sie zuerst abschalten.
Sicherheitsentscheidungen sind die Ausnahme: Ist der Autorisierungsservice nicht erreichbar, ist der sichere Fallback, zu verweigern (Fail-closed), nicht zu erlauben. Fachleute sind sich bei Fallbacks auch allgemein uneins. Nygard und die Fachleute bei Netflix befürworten sie breit. Fachleute bei Amazon haben im Builders’-Library-Artikel Avoiding fallback in distributed systems argumentiert, dass Fallback-Pfade selten durchlaufen werden, schwer zu testen sind und selbst Ausfälle auslösen können, indem sie eine große Flotte im ungünstigsten Moment in einen ungetesteten Modus schalten; sie ziehen es vor, den primären Pfad zuverlässiger zu machen und schnell fehlzuschlagen. Beide Positionen sind sich über die praktische Regel einig: Ein Fallback muss einfacher sein als das, was er ersetzt, darf nicht von der ausgefallenen Komponente abhängen und muss regelmäßig durchlaufen werden, zum Beispiel an Game Days (Abschnitt 9.8), sonst sollte es ihn nicht geben.
Idempotenz: was Wiederholungen und Hedging sicher macht
Idempotenz ist die Eigenschaft, dass etwas zweimal zu tun dieselbe Wirkung hat wie es einmal zu tun. Jedes Muster in diesem Abschnitt, das Arbeit wiederholt, hängt davon ab: Wiederholungen nach einem Timeout, Hedged Requests, erneute Zustellung von Nachrichten nach dem Absturz eines Consumers und Replays aus einer Dead Letter Queue. Abschnitt 6.5 hat gezeigt, wie ein Idempotenzschlüssel POST /orders sicher wiederholbar macht, und Abschnitt 6.4, wie idempotente Consumer Nachrichten deduplizieren. Die architektonische Regel ist einfach: Prüfen Sie vor dem Hinzufügen einer Wiederholung, ob die Operation idempotent ist, und wenn nicht, fügen Sie zuerst einen Schlüssel oder eine Duplikatprüfung hinzu. Eine Retry-Policy für einen nicht idempotenten Zahlungsaufruf ist kein Resilienzmuster, sondern ein Generator für Doppelabbuchungen.
Hedged Requests
Wiederholungen behandeln Fehler; Hedged Requests behandeln Langsamkeit. Abschnitt 2.4 hat sie nach Dean und Barrosos The Tail at Scale (2013) eingeführt: Senden Sie eine Anfrage an ein Replikat, und wenn es nicht innerhalb, sagen wir, der Latenz des 95. Perzentils geantwortet hat, senden Sie eine Kopie an ein zweites Replikat, verwenden Sie die Antwort, die zuerst kommt, und brechen Sie die andere ab. Weil nur die langsamsten paar Prozent der Anfragen dupliziert werden, ist die zusätzliche Last gering, während der Tail drastisch schrumpft, wie der in Abschnitt 2.4 zitierte Benchmark gezeigt hat.
Hedging hat Voraussetzungen. Die Operation muss idempotent sein, normalerweise ein Lesezugriff. Es muss unabhängige Replikate geben, damit die zweite Kopie wahrscheinlich nicht aus demselben Grund langsam ist. Die zusätzlichen Anfragen müssen budgetiert und unter Überlast abgeschaltet werden, denn wenn jedes Replikat langsam ist, verdoppelt Hedging die Last auf einem System, das ohnehin kämpft, genau wie eine unbudgetierte Wiederholung. Sorgfältig eingesetzt auf leselastigen Fan-out-Pfaden wie Suche und Speisekartenabfragen, ist es eines der wirksamsten verfügbaren Werkzeuge gegen Tail-Latenz.
Die Muster auf einen Blick
| Muster | Schützt vor | Hauptkosten oder -risiko |
|---|---|---|
| Timeout | Unbegrenztes Warten; Ressourcen, die von toten Aufrufen gehalten werden | Zu kurz lässt gesunde langsame Aufrufe scheitern; zu lang schützt nichts |
| Deadline-Weitergabe (Deadline Propagation) | Arbeit, die weiterläuft, nachdem der Aufrufer aufgegeben hat | Braucht Unterstützung in jedem Service und jeder Bibliothek auf dem Pfad |
| Wiederholung mit Backoff und Jitter | Vorübergehende Fehler | Zusätzliche Last; Duplikate, sofern nicht idempotent |
| Retry-Budget | Retry-Stürme, die die Last vervielfachen | Manche vorübergehenden Fehler werden in schlechten Phasen nicht wiederholt |
| Circuit Breaker | Wiederholte Aufrufe einer ausgefallenen Abhängigkeit | Feinabstimmung; kann eine nur teilweise fehlerhafte Abhängigkeit abschneiden |
| Bulkhead | Eine Abhängigkeit oder ein Aufrufer verbraucht alle Ressourcen | Reservierte Kapazität kann nicht geteilt werden |
| Fallback und Degradation | Totalausfall eines Features | Ungetestete Codepfade; veraltete oder ungefähre Ergebnisse |
| Hedged Request | Tail-Latenz durch langsame Replikate | Zusätzliche Last; nur Lesezugriffe |
Checkliste: jeder Remote-Aufruf
Prüfen Sie jeden Integrationspunkt, einschließlich Datenbanken, Caches, Broker und Drittanbieter-APIs, anhand dieser Liste.
- Connect- und Request-Timeouts sind explizit gesetztGewählt anhand der gemessenen hohen Perzentile der Abhängigkeit und der eigenen Deadline des Aufrufers, nie auf einem Bibliotheksstandard belassen.
- Die verbleibende Deadline wird weitergegebenNachgelagerte Aufrufe erhalten die Restzeit, und die Arbeit stoppt, wenn sie abläuft.
- Wiederholungen sind begrenzt und sicherNur vorübergehende Fehler, nur idempotente Operationen oder solche mit Idempotenzschlüssel, auf einer Schicht, mit begrenztem exponentiellem Backoff und Jitter.
- Ein Retry-Budget begrenzt die zusätzliche LastWiederholungen bleiben ein kleiner Anteil des Verkehrs (zum Beispiel 10 %), sodass ein Ausfall die Last nicht vervielfachen kann.
- Ein Circuit Breaker schützt die AbhängigkeitSchwellen, Wartezeit und was als Fehler zählt, sind abgestimmt, und sein Zustand wird als Metrik exportiert.
- Die Nebenläufigkeit zur Abhängigkeit ist begrenztEin Bulkhead (eigener Pool oder Semaphor) verhindert, dass diese Abhängigkeit jeden Thread oder jede Verbindung verbraucht.
- Das Verhalten im Fehlerfall ist entschiedenEin Fallback, eine eingeschränkte Antwort oder ein klarer Fehler, mit dem Product Owner abgestimmt, und Fail-closed bei Sicherheitsentscheidungen.
- Timeouts, Wiederholungen, ausgelöste Breaker und Fallbacks sind beobachtbarGezählt, grafisch dargestellt und mit Alarmen versehen, damit stille Degradation bemerkt wird (Abschnitt 9.6).
- Der Fehlerpfad ist getestetFault Injection oder ein Game Day zeigt, dass Timeouts, Breaker und Fallback tatsächlich funktionieren (Abschnitt 9.8).
Prüfen Sie Ihr Verständnis
Ein Service erhält 50 Anfragen pro Sekunde, die jeweils eine Abhängigkeit mit einem Timeout von 4 Sekunden aufrufen. Wie viele Threads können diese Aufrufe belegen, wenn die Abhängigkeit hängt?
Nach Littles Gesetz etwa 50 × 4 = 200 Threads; deshalb müssen der Timeout und ein Bulkhead-Limit zur Poolgröße passen.
Warum ist Full Jitter besser als einfaches exponentielles Backoff, wenn viele Clients gleichzeitig scheitern?
Einfaches Backoff lässt alle Clients in denselben Momenten wiederholen, in synchronisierten Wellen; zufällige Wartezeiten verteilen die Wiederholungen, sodass die Abhängigkeit eine gleichmäßige, geringere Last sieht.
In welchem Zustand macht ein Circuit Breaker keine Remote-Aufrufe, und was beendet diesen Zustand?
Im offenen Zustand; wenn die Wartezeit abläuft, wird er halboffen und lässt Testaufrufe durch.
Warum sollte eine Autorisierungsprüfung nicht darauf zurückfallen, den Zugriff zu erlauben, wenn der Autorisierungsservice ausgefallen ist?
Sicherheitsentscheidungen müssen geschlossen fehlschlagen (Fail-closed): Zugriff während eines Ausfalls zu erlauben, würde jeden hereinlassen, sobald der Service ausfällt oder angegriffen wird.
Quellen und weiterführende Literatur
Haben Sie eine Frage zu diesem Abschnitt? ArchitectBot, der KI-Lernassistent des Kurses, antwortet zuerst aus dem Kurs.
Fragen: „Unser Checkout ruft drei externe Services synchron auf. Mit welchen Timeouts, Wiederholungen und Circuit-Breaker-Einstellungen sollten wir beginnen, und wie verhindern wir einen Retry-Sturm?“