Read in English · Leer en español · Lire en français
Abschnitt 9.3 von 9.8 · Mittelstufe · 29 Min. Lesezeit
Asynchronität, Queues und Backpressure
Videolektion
5 Min. · auf Englisch gesprochen, mit UntertitelnNur anhören (Audio)
Transkript lesen
Willkommen zu Abschnitt neun Punkt drei. Last kommt nie gleichmäßig an. In dieser Lektion geht es darum, was ein System tun sollte, wenn Arbeit schneller ankommt, als sie erledigt werden kann.
Denken Sie an eine gut besuchte Bäckerei zur Öffnungszeit. Sie verteilt Nummernzettel, damit die Kundschaft geordnet wartet und die Bäcker in gleichmäßigem Tempo arbeiten. Das ist eine Queue. Doch eine Schlange kann nur so lang werden, bis das Brot ausgeht, also hört eine gute Bäckerei auf, Nummern auszugeben, wenn die Schlange zu lang ist. Sie bedient Vorbestellungen zuerst und begrenzt jede Person auf eine vernünftige Zahl von Broten. Und sie sagt Nachzüglern ehrlich, sie sollen in zehn Minuten wiederkommen, statt sie vergeblich warten zu lassen.
Hier legt die Bestell-API Arbeit in eine Queue, und mehrere identische Worker nehmen sie heraus. Jede Nachricht geht an nur einen Worker. Man nennt sie Competing Consumers. Steigen die Ankünfte über das, was die Consumer bewältigen, wartet die zusätzliche Arbeit als Rückstand in der Queue. Die Worker arbeiten in ihrem gleichmäßigen Tempo weiter und bauen den Rückstand nach der Spitze ab. So können Consumer für den Durchschnitt dimensioniert werden, nicht für die Spitze. Der Preis ist Verzögerung. Ein Rückstand ist schlicht eine Wartezeit, die jedes Element dahinter erlebt. In Ordnung für eine E-Mail, inakzeptabel für eine Zahlung, auf die der Kunde schaut.
Stürzt ein Worker ab, bevor er eine Nachricht bestätigt, bekommt sie ein anderer Worker. Die Zustellung erfolgt also at least once, und Consumer müssen idempotent sein: Eine Nachricht zweimal zu verarbeiten, muss sicher sein. Eine Queue fängt einen vorübergehenden Überschuss auf, nie einen dauerhaften. Eine unbegrenzte Queue wächst, bis sie den Speicher erschöpft, oder hält Arbeit noch Stunden, nachdem sie niemanden mehr interessiert. Geben Sie also jeder Queue eine Grenze. Littles Gesetz macht aus der Wartezeit, die Sie akzeptieren, eine maximale Länge. Und messen Sie neben der Länge auch das Alter der ältesten Nachricht. Das Alter ist das, was Benutzer erleben.
Dimensionieren wir eine Queue. Konzerttickets gehen um zehn Uhr in den Verkauf. Vierzig Worker brauchen je zweihundertfünfzig Millisekunden pro Bestellung, schaffen also hundertsechzig Bestellungen pro Sekunde. Die Prognose lautet sechshundert pro Sekunde für zwei Minuten. Damit warten zweiundfünfzigtausendachthundert Bestellungen, und die letzte wartet fünfeinhalb Minuten. Versprochen war eine Antwort innerhalb von zwei Minuten. Um dieses Versprechen zu halten, darf die Queue laut Littles Gesetz höchstens hundertsechzig mal hundertzwanzig fassen: neunzehntausendzweihundert Bestellungen. Mehr Worker? Der Zahlungsanbieter erlaubt nur dreihundert Anfragen pro Sekunde, also höchstens fünfundsiebzig Worker. Die Wartezeit beträgt dann genau zwei Minuten. Gerade so eingehalten, ohne Reserve. Aber es gibt fünfundzwanzigtausend Tickets, zweieinhalb pro Bestellung: Nur zehntausend Bestellungen können erfolgreich sein. Den Rest in die Queue zu stellen, verzögert nur das unvermeidliche „ausverkauft“. Also nutzt das Team fünfundsiebzig Worker, einen Warteraum und eine auf zwölftausend begrenzte Queue: den Bestand plus zwanzig Prozent für fehlgeschlagene Zahlungen. Danach hören neue Bestellungen sofort „ausverkauft“.
Backpressure ist jeder Weg, mit dem eine langsamere Stufe eine schnellere bremst. Hier sendet der Producer fünfhundert Elemente pro Sekunde, der Consumer schafft dreihundert, und die begrenzte Queue ist voll. Sie können den Producer blockieren, sodass er auf Platz wartet. Das passt zu Pipelines und Batch-Jobs. Sie können neue Arbeit ablehnen, mit Fail fast über vier zwei neun oder fünf null drei, damit der Client es später erneut versucht. Oder Sie verwerfen Elemente, etwa die ältesten oder die mit der niedrigsten Priorität. Jede Option ist irgendwo richtig. Der Fehler ist, keine Wahl zu treffen.
Ein Server, der unter Überlast alles annimmt, verbraucht seine ganze Kapazität für Anfragen, deren Clients schon aufgegeben haben. Der Goodput, die rechtzeitig genug abgeschlossenen Anfragen, bricht gegen null ein. Load Shedding lehnt einige Anfragen bewusst ab. Es muss früh und kostengünstig sein, vor jeder teuren Arbeit. Es sollte priorisiert sein. In der Essenslieferplattform haben Checkouts und Kurierstandorte Vorrang vor Empfehlungen, und diese vor Analytics-Events. Und es sollte zeitbewusst sein: Eine Anfrage, die länger gewartet hat als der Timeout ihres Aufrufers, sollte verworfen werden, nicht verarbeitet.
Rate Limiting teilt Kapazität fair zwischen Clients auf. Der übliche Algorithmus ist der Token Bucket. Dieser Eimer fasst höchstens zwanzig Tokens und füllt sich mit fünf Tokens pro Sekunde. Jede Anfrage, pro API-Schlüssel gezählt, muss ein Token nehmen. Ist ein Token übrig, wird die Anfrage weitergeleitet. Wenn nicht, lautet die Antwort vier zwei neun, Too Many Requests, idealerweise mit einem Retry-After-Header. Ein ruhiger Client kann also einen Burst senden: Von dreißig Anfragen auf einmal werden zwanzig weitergeleitet und zehn abgelehnt. Aber in beliebigen sechzig Sekunden kann er höchstens zwanzig plus fünf mal sechzig senden: dreihundertzwanzig Anfragen.
Der vollständige Abschnitt vergleicht Algorithmen für Rate Limiting und behandelt Dead Letter Queues. Als Nächstes betrachten wir in Abschnitt neun Punkt vier die Muster, die verhindern, dass sich ein Ausfall ausbreitet. Bis gleich.
Was Sie wissen müssen
- Eine Queue zwischen Producer und Consumer verwandelt eine Lastspitze in einen Rückstand, den die Consumer in ihrem eigenen Tempo abarbeiten, sodass sie für den Durchschnitt dimensioniert werden können; der Preis ist Verzögerung für jedes Element hinter dem Rückstand.
- Competing Consumers skalieren Hintergrundarbeit durch zusätzliche Worker; die Zustellung erfolgt at least once, daher müssen Consumer idempotent sein, und Visibility Timeouts und Prefetch müssen zu den Verarbeitungszeiten passen.
- Eine Queue kann keinen dauerhaften Überschuss aufnehmen; begrenzen Sie jede Queue mit Littles Gesetz (maximale Länge = Verarbeitungsrate × akzeptable Wartezeit) und überwachen Sie neben der Länge auch das Alter der ältesten Nachricht.
- Backpressure bringt schnellere Stufen dazu, langsamer zu werden; ist eine begrenzte Queue voll, muss das System den Producer blockieren, neue Arbeit ablehnen oder Arbeit verwerfen, und es sollte diese Wahl bewusst treffen.
- Load Shedding lehnt einen Teil der Arbeit früh und kostengünstig ab, nach Priorität und Alter, damit der Goodput unter Überlast hoch bleibt.
- Rate Limiting teilt Kapazität pro Client auf: Der Token Bucket erlaubt Bursts bis zu seiner Kapazität und eine gleichmäßige Nachfüllrate, der Leaky Bucket glättet die Ausgabe, und feste Fenster erlauben doppelte Bursts an den Fenstergrenzen.
- Beantworten Sie Limits mit 429 (oder 503, wenn der ganze Server überlastet ist) plus Retry-After, und leiten Sie wiederholt fehlschlagende Nachrichten in eine Dead Letter Queue, die jemandem gehört und überwacht wird.
Queues verwandeln Lastspitzen in Rückstände
Last kommt nie gleichmäßig an. Mittagsbestellungen ballen sich um 12:30, Ticketkäufer drücken alle um 10:00:00 auf den Knopf, und ein Batch-Job kippt um Mitternacht eine Million Events ab. Ein synchroner Service muss entweder Kapazität für die höchste Spitze haben oder während dieser Spitze ausfallen. Eine Queue zwischen dem Producer der Arbeit und dem Service, der sie erledigt, ändert die Rechnung: Während einer Spitze wartet die zusätzliche Arbeit in der Queue, und die Consumer verarbeiten sie in ihrem eigenen gleichmäßigen Tempo und holen auf, wenn die Spitze vorbei ist. Microsofts Katalog von Cloud-Mustern nennt das Queue-Based Load Leveling. Abschnitt 2.4 hat gezeigt, warum sich Warteschlangen bilden, sobald Ankünfte sich ballen; in diesem Abschnitt geht es darum, sie gezielt einzusetzen.
Der Nutzen ist real: Consumer können für den Durchschnitt statt für die Spitze dimensioniert werden, ein langsamer oder neu startender Consumer verliert keine Arbeit, und der Producer ist nicht mehr zeitlich an den Consumer gekoppelt (Abschnitt 6.5). Der Preis ist, dass die Arbeit nun später erledigt wird. Eine Queue verwandelt eine Spitze in einen Rückstand, und ein Rückstand ist schlicht eine Verzögerung, die jedes Element dahinter erfährt. Für den Versand von Bestätigungs-E-Mails oder die Aktualisierung eines Suchindex ist das in Ordnung, für die Zuweisung eines Kuriers zu einer Bestellung fragwürdig und für eine Zahlung, deren Bestätigung die Kundin oder der Kunde erwartet, inakzeptabel. Die erste Entwurfsfrage für jede Queue lautet daher: Wie lange darf ein Element warten, und was geschieht mit Elementen, die länger warten würden?
Eine Queue fängt eine Lastspitze ab
Work Queues und Competing Consumers
Am häufigsten ist eine Work Queue mit Competing Consumers: Mehrere identische Worker lesen aus einer Queue, jede Nachricht geht an genau einen von ihnen, und der Durchsatz wächst, indem man Worker hinzufügt. Das ist die X-Achse des Scale Cube, angewandt auf Hintergrundarbeit, und sie skaliert natürlich anhand des richtigen Signals: Rückstand pro Worker oder Alter der ältesten Nachricht.
Für die Korrektheit kommt es auf die Mechanik an. Ein Worker nimmt eine Nachricht, verarbeitet sie und bestätigt sie dann; stürzt der Worker vorher ab, macht der Broker die Nachricht nach einem Timeout wieder sichtbar (SQS nennt das den Visibility Timeout), und ein anderer Worker verarbeitet sie. Die Zustellung erfolgt daher at least once, also müssen Consumer idempotent sein (Abschnitt 6.4). Ein Visibility Timeout, der kürzer als die tatsächliche Verarbeitungszeit ist, führt dazu, dass dieselbe Nachricht zweimal parallel verarbeitet wird. Die Reihenfolge geht verloren, sobald es mehr als einen Consumer gibt, es sei denn, die Nachrichten werden nach Schlüssel partitioniert; dann begrenzt die Zahl der Partitionen die Zahl sinnvoller Consumer. Und Prefetching (ein Worker nimmt mehrere Nachrichten auf einmal, um Roundtrips zu sparen) sollte bei langsamen Aufgaben niedrig bleiben, sonst hortet ein Worker Nachrichten, die untätige Worker hätten verarbeiten können.
Eine Queue schafft keine Kapazität
Eine Queue fängt einen vorübergehenden Überschuss auf, aber keinen dauerhaften. Kommt Arbeit lange genug schneller an, als sie erledigt wird, wächst der Rückstand ohne Grenze, und Littles Gesetz (L = λ × W) besagt, dass die Wartezeit mit ihm wächst. Eine unbegrenzte In-Memory-Queue erschöpft irgendwann den Speicher und bringt den Prozess zum Absturz, wobei alles darin verloren geht; eine unbegrenzte persistente Queue füllt ihre Festplatte oder hält Arbeit noch Stunden, nachdem sich niemand mehr dafür interessiert. Schlimmer noch: Die Queue verbirgt das Problem. Producer erhalten schnelle Bestätigungen, die Fehlerraten bleiben bei null, und das einzige Symptom ist, dass Ergebnisse immer später eintreffen. Fachleute bei Amazon beschreiben solche Rückstände als potenziell unüberwindbar, weil ein Service, der weit genug zurückfällt, seine gesamte Kapazität für Arbeit aufwenden kann, die seine Clients längst aufgegeben haben.
Daraus folgen zwei Praktiken. Erstens: Messen Sie neben der Queue-Länge auch das Alter der ältesten Nachricht; das Alter ist das, was Benutzer erleben, und es reagiert sofort, wenn Consumer ins Stocken geraten. Zweitens: Geben Sie jeder Queue eine Grenze, die Sie mit Littles Gesetz aus der akzeptablen Wartezeit ableiten, und legen Sie im Voraus fest, was passiert, wenn die Grenze erreicht ist. Um diese Entscheidung geht es in den nächsten drei Teilen dieses Abschnitts.
Ausgearbeitetes Beispiel
Ausgearbeitetes Beispiel: die Bestell-Queue für einen Ticketverkaufsstart dimensionieren
Tickets für ein Stadionkonzert gehen um 10:00 in den Verkauf. Bestellungen landen in einer Queue; Worker autorisieren die Zahlung und weisen Plätze zu, jeweils in 250 ms. Die Prognose lautet 600 Bestellungen pro Sekunde in den ersten zwei Minuten. Den Kunden ist eine Antwort innerhalb von zwei Minuten versprochen. Es gibt 25.000 Tickets, und eine Bestellung umfasst im Durchschnitt 2,5 Tickets.
| Worker-Kapazität mit 40 Workern | 40 ÷ 0,25 s = 160 Bestellungen pro Sekunde |
|---|---|
| Rückstand nach der zweiminütigen Spitze | (600 − 160) × 120 s = 52.800 wartende Bestellungen |
| Wartezeit der letzten Bestellung (Littles Gesetz, W = L ÷ λ) | 52.800 ÷ 160 = 330 s, fünfeinhalb Minuten: Das Versprechen ist gebrochen |
| Längste Queue, die das Versprechen hält | L = λ × W = 160 × 120 = 19.200 Bestellungen |
| Mehr Worker? | Der Vertrag mit dem Zahlungsanbieter erlaubt 300 Anfragen pro Sekunde, also höchstens 75 Worker (300 Bestellungen pro Sekunde). Der Rückstand wird (600 − 300) × 120 = 36.000 und die Wartezeit 36.000 ÷ 300 = 120 s: Das Versprechen wird gerade so gehalten, ohne Reserve |
| Was der Bestand zulässt | 25.000 ÷ 2,5 = 10.000 Bestellungen können erfolgreich sein. Bei 600 pro Sekunde treffen so viele in weniger als 17 Sekunden ein; die übrigen rund 62.000 Bestellungen in die Queue zu stellen, verzögert nur das unvermeidliche „ausverkauft“ |
| Entscheidung | 75 Worker; ein Warteraum vor der Bestellseite lässt 300 Käufer pro Sekunde ein; die Bestell-Queue ist auf 12.000 begrenzt (der Bestand plus 20 % für fehlgeschlagene Zahlungen); sobald die Reservierungen den Bestand abdecken, erhalten neue Bestellungen sofort die Antwort „ausverkauft, tragen Sie sich in die Warteliste ein“, statt in die Queue zu gelangen |
Littles Gesetz verwandelt ein Versprechen über die Wartezeit in eine maximale Queue-Länge. Jede Queue, die darüber hinaus wachsen darf, ist ein gebrochenes Versprechen, das nur noch nicht verkündet wurde.
Das Rate Limit des nachgelagerten Systems, nicht die Zahl der Worker, die die Plattform starten könnte, bestimmte die tatsächliche Kapazität; zusätzliche Consumer stießen daher an eine harte Obergrenze.
Die beste Grenze für eine Queue ist oft eine geschäftliche: Es hat keinen Sinn, Arbeit vorzuhalten, die unmöglich erfolgreich sein kann.
Backpressure: Producer zum Verlangsamen bringen
Backpressure ist jeder Mechanismus, mit dem eine langsamere nachgelagerte Stufe eine schnellere vorgelagerte Stufe zum Verlangsamen bringt, statt Arbeit dazwischen auflaufen zu lassen. Der Begriff stammt von Flüssigkeiten in Rohren, und die Idee ist uns bei Pipes and Filters (Abschnitt 5.4) begegnet. Sie ist in viele Schichten des Stacks eingebaut, den Sie bereits nutzen. Die Flusskontrolle von TCP stoppt einen Sender, wenn der Puffer des Empfängers voll ist. Eine begrenzte blockierende Queue lässt das put des Producers auf freien Platz warten. Pull-basierte Consumer wie bei Kafka holen Arbeit erst, wenn sie bereit sind, sodass das Log des Brokers statt des Speichers des Consumers den Rückstand hält, und ein Kafka-Producer blockiert oder schlägt fehl, wenn sein eigener Sendepuffer voll ist. Die Reactive-Streams-Spezifikation lässt einen Subscriber eine bestimmte Anzahl von Elementen anfordern, sodass ein Stream nie mehr liefert, als der Subscriber verlangt hat.
Backpressure funktioniert gut innerhalb eines Prozesses oder entlang einer Pipeline. Über eine Request-Response-Grenze mit menschlichen Benutzern wird daraus meist Ablehnung: Ein überlasteter HTTP-Service kann ein Smartphone nicht höflich warten lassen, also antwortet er mit 429 Too Many Requests oder 503 Service Unavailable, idealerweise mit einem Retry-After-Header, und verlässt sich darauf, dass der Client zurückweicht. Ist eine begrenzte Queue voll, gibt es nur drei Möglichkeiten: den Producer blockieren, die neue Arbeit ablehnen oder etwas bereits Angenommenes verwerfen. Jede ist irgendwo richtig; der Fehler ist, keine Wahl zu treffen.
Drei Reaktionen auf eine volle Queue
Was passiert, wenn eine Queue voll ist: die Wahl
| Strategie | Verhalten | Geeignet für | Risiko |
|---|---|---|---|
| Producer blockieren | Der Producer wartet, bis Platz frei ist | Pipelines und Batch-Jobs, bei denen der Producer pausieren kann | Blockierte Producer halten Threads und Verbindungen; über Services hinweg pflanzt sich die Verlangsamung so nach oben fort |
| Neue Arbeit ablehnen | Fail fast mit einem Fehler wie 429 oder 503 | Interaktive Anfragen; der Client kann es erneut versuchen, warten oder eine Meldung anzeigen | Clients, die sofort erneut anfragen, verwandeln Ablehnung in mehr Last (Abschnitt 9.4) |
| Älteste verwerfen | Das Element verwerfen, das am längsten gewartet hat | Telemetrie, Standortupdates, Live-Preise: Die neuesten Daten sind am wertvollsten | Stiller Verlust; muss gezählt und sichtbar gemacht werden |
| Nach Priorität verwerfen | Die unwichtigsten Elemente zuerst verwerfen | Gemischte Lasten: Checkouts behalten, Analytics-Events verwerfen | Niedrige Prioritäten können verhungern; Prioritäten müssen ehrlich vergeben werden |
| In persistenten Speicher auslagern | Überlauf auf Festplatte oder in eine persistente Queue | Arbeit, die irgendwann erledigt werden muss, aber Stunden warten kann | Nur eine größere Queue: braucht trotzdem eine Grenze und eine Schätzung der Abbauzeit |
Load Shedding und Priorisierung
Ein Server unter mehr Last, als er bewältigen kann, schneidet schlechter ab, als wenn er seine Kapazität bedienen und den Rest ablehnen würde. Er nimmt alles an, jede Anfrage wartet in seinen internen Queues, die Antwortzeiten steigen über die Timeouts der Clients, und am Ende verwendet er seine gesamte Kapazität auf Anfragen, deren Clients bereits aufgegeben haben. Der Durchsatz bleibt hoch, während der Goodput, die Rate der Anfragen, die rechtzeitig genug abgeschlossen werden, um nützlich zu sein, gegen null einbricht. Load Shedding verhindert das, indem es einige Anfragen bewusst ablehnt, damit die übrigen erfolgreich sind.
Gutes Load Shedding hat drei Eigenschaften. Es ist früh und kostengünstig: Eine Anfrage abzulehnen muss weit weniger kosten, als sie zu bedienen; es geschieht daher an der Eingangstür, vor aufwendiger Authentifizierung oder Datenbankarbeit, anhand eines billigen Signals wie laufender Anfragen oder des Queue-Alters. Es ist priorisiert: Googles SRE-Buch beschreibt, jede Anfrage mit einer Kritikalität zu kennzeichnen (von CRITICAL_PLUS bis hinunter zu SHEDDABLE), damit Batch- und Prefetch-Verkehr vor nutzerseitigem Verkehr verworfen wird. In der Essenslieferplattform haben Checkouts und Standortupdates der Kuriere Vorrang vor Empfehlungen, die wiederum Vorrang vor Analytics-Events haben. Und es ist zeitbewusst: Eine Anfrage, die bereits länger gewartet hat als der Timeout ihres Aufrufers, sollte verworfen statt verarbeitet werden. Fachleute bei Facebook haben zwei Verfeinerungen beschrieben: eine Controlled-Delay-Queue, die nur eine kurze Wartezeit zulässt, wenn eine Queue in letzter Zeit nicht leer war, und den Wechsel zu Last-in-first-out unter Last, sodass die frischesten Anfragen, deren Benutzer noch warten, zuerst bedient werden.
Ein weiterer Schritt besteht darin, das Limit selbst anzupassen. Feste Nebenläufigkeitslimits sind an einem guten Tag entweder zu niedrig oder an einem schlechten zu hoch; adaptive Nebenläufigkeitsbegrenzer erhöhen die erlaubte Zahl laufender Anfragen, solange die Latenz niedrig bleibt, und senken sie, wenn die Latenz steigt – eine Idee, die der TCP-Überlastkontrolle entlehnt ist. Ganz am Eingang eines Systems kann Zugangskontrolle die Form eines virtuellen Warteraums annehmen, wie beim Ticketverkaufsstart oben: Benutzer werden am Rand in einer Warteschlange gehalten und in dem Tempo eingelassen, das der Kern bedienen kann – Load Shedding, präsentiert als Fairness.
Algorithmen für Rate Limiting
Rate Limiting beschränkt, wie viel Arbeit ein bestimmter Client, API-Schlüssel, Mandant oder eine IP-Adresse in einem Zeitraum einreichen darf. Es unterscheidet sich im Zweck von Load Shedding: Load Shedding schützt den Server vor völliger Überlast, egal wer sie verursacht, während Rate Limiting Kapazität fair verteilt und vor jedem einzelnen Client schützt, ob fehlerhaft oder böswillig. Beide antworten auf dieselbe Weise, meist mit HTTP 429. Vier Algorithmen dominieren.
Rate-Limiting-Algorithmen im Vergleich
| Algorithmus | So funktioniert es | Bursts | Zustand pro Schlüssel | Hinweise |
|---|---|---|---|---|
| Token Bucket | Tokens werden mit Rate r bis zur Kapazität b hinzugefügt; jede Anfrage verbraucht eines | Erlaubt Bursts bis b, dann im Mittel r | Zwei Zahlen: Tokens und Zeitpunkt der letzten Nachfüllung | Am weitesten verbreitet; Burst und Dauerrate werden getrennt eingestellt |
| Leaky Bucket | Anfragen gelangen in einen Eimer, der sich mit konstanter Rate leert; ist er voll, werden neue Anfragen abgelehnt | Die Ausgabe ist völlig gleichmäßig; Bursts warten oder werden abgelehnt | Eine Queue oder ein Zähler mit Zeitstempel | Für Traffic Shaping verwendet; das Request Limiting von NGINX basiert darauf |
| Fixed Window Counter | Anfragen pro Kalenderfenster zählen (pro Minute); Zurücksetzen an der Grenze | Bis zum Doppelten des Limits über eine Grenze hinweg | Ein Zähler | Am einfachsten; 100 Anfragen um 12:00:59 und 100 um 12:01:00 kommen beide durch |
| Sliding Window Log | Den Zeitstempel jeder Anfrage im letzten Fenster speichern; diese zählen | Exakt | Ein Eintrag pro Anfrage | Präzise, aber speicherhungrig bei hohen Raten |
| Sliding Window Counter | Den Zählerstand des vorigen Fensters danach gewichten, wie viel davon noch überlappt, und den aktuellen Zählerstand addieren | Gute Näherung | Zwei Zähler | Nach 30 s der Minute, 80 in der Vorminute, 40 aktuell: 80 × 0,5 + 40 = 80 |
Der Token Bucket verdient einen genaueren Blick, weil er der übliche Standard ist. Der Eimer fasst bis zu b Tokens und erhält r Tokens pro Sekunde. Jede Anfrage nimmt ein Token; ist keines übrig, wird sie abgelehnt (oder in der Shaping-Variante verzögert). Ein Client, der ruhig war, kann daher einen Burst von b Anfragen auf einmal senden, was zu echten Clients passt, etwa einer mobilen App, die einen Bildschirm öffnet, während er über jeden längeren Zeitraum im Mittel r nicht überschreiten kann. Der Leaky Bucket ist das Spiegelbild: Er glättet die Ausgabe auf eine konstante Rate, was nützlich ist, um ein nachgelagertes System zu schützen, das überhaupt keine Bursts verkraftet.
Der Token Bucket
Ausgearbeitetes Beispiel
Ausgearbeitetes Beispiel: ein Token Bucket für eine Partner-API
Ein API-Schlüssel für Restaurantpartner ist durch einen Token Bucket mit Kapazität b = 20 und Nachfüllrate r = 5 Tokens pro Sekunde begrenzt (im Mittel 300 pro Minute). Der Eimer ist bei t = 0 voll.
| t = 0: ein Burst von 30 Anfragen | Die ersten 20 nehmen die 20 Tokens und werden weitergeleitet; 10 werden mit 429 abgelehnt |
|---|---|
| t = 0 bis 1 s | 5 neue Tokens kommen hinzu (5 pro Sekunde) |
| t = 1 bis 10 s: gleichmäßig 8 Anfragen pro Sekunde | Etwa 5 pro Sekunde werden angenommen und 3 abgelehnt; der Eimer bleibt nahezu leer |
| t = 10 bis 14 s: keine Anfragen | Der Eimer füllt sich mit 5 pro Sekunde und ist bei t = 14 wieder voll (20) |
| Obergrenze über einen beliebigen Zeitraum T | Höchstens b + r × T Anfragen: in beliebigen 60 Sekunden 20 + 5 × 60 = 320, sodass der langfristige Mittelwert nie 5 pro Sekunde übersteigen kann |
| Retry-After für eine abgelehnte Anfrage | Das nächste Token kommt in 1 ÷ r = 0,2 s, aber der Header zählt ganze Sekunden, also lautet die Antwort Retry-After: 1 |
Die Kapazität bestimmt den Burst, den ein wohlerzogener Client nach einer ruhigen Phase senden darf; die Nachfüllrate bestimmt die Dauerrate. Wählen Sie beide getrennt.
Ein Client, der Retry-After beachtet, mit etwas Jitter, pendelt sich bei der Nachfüllrate ein; einer, der sofort erneut anfragt, sammelt nur weitere 429er.
import math, time
class TokenBucket:
def __init__(self, rate, capacity):
self.rate = rate # tokens added per second
self.capacity = capacity # largest burst allowed
self.tokens = capacity # start full
self.updated = time.monotonic()
def allow(self, cost=1):
now = time.monotonic()
elapsed = now - self.updated
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.updated = now
if self.tokens >= cost:
self.tokens -= cost
return True, 0
wait = (cost - self.tokens) / self.rate # seconds until enough tokens
return False, max(1, math.ceil(wait)) # for the Retry-After header
bucket = TokenBucket(rate=5, capacity=20) # one bucket per API key
allowed, retry_after = bucket.allow()HTTP 429, Retry-After und wo Limits durchgesetzt werden
HTTP hat Standardsignale für beide Situationen. 429 Too Many Requests, definiert in RFC 6585 (2012), teilt einem Client mit, dass er in einem bestimmten Zeitraum zu viele Anfragen gesendet hat. 503 Service Unavailable besagt, dass der Server als Ganzes vorübergehend überlastet ist oder gewartet wird. Beide können einen Retry-After-Header (RFC 9110) tragen, der eine Anzahl von Sekunden oder ein Datum angibt, nach dem ein erneuter Versuch sinnvoll ist. Viele APIs geben außerdem Header zurück, die das Limit, das verbleibende Kontingent und den Rücksetzzeitpunkt ankündigen, traditionell X-RateLimit-*; ein IETF-Entwurf standardisiert die Felder RateLimit und RateLimit-Policy. Wohlerzogene Clients beachten Retry-After, fügen Jitter hinzu, damit abgelehnte Clients nicht alle im selben Augenblick zurückkommen, und behandeln wiederholte 429er als Grund, langsamer zu werden, statt es heftiger zu versuchen (Abschnitt 9.4).
Limits lassen sich auf mehreren Schichten durchsetzen, und ausgereifte Systeme nutzen mehr als eine: am Edge oder API-Gateway pro IP-Adresse und API-Schlüssel, um Missbrauch und außer Kontrolle geratene Clients zu stoppen, bevor sie etwas kosten; pro Mandant, damit ein großer Kunde die anderen nicht aushungern kann; und innerhalb jedes Service, um die eigene Kapazität und die eigenen Abhängigkeiten zu schützen. Ein globales Limit über viele Gateway-Instanzen braucht gemeinsamen Zustand, meist atomare Zähler in Redis, was einen Netzwerkaufruf pro Anfrage hinzufügt; ein gängiger Kompromiss ist, jeder von N Instanzen ein lokales Limit von etwa 1 ÷ N des Gesamtwerts zu geben und eine gewisse Ungenauigkeit hinzunehmen. Limits sollten anhand gemessener Kapazität festgelegt und veröffentlicht werden, damit Partner darauf hin entwerfen können, und die Zahl der Ablehnungen sollte überwacht werden, denn ein plötzlicher Anstieg ist entweder ein Angriff, ein Fehler in einem Client oder ein Limit, das nicht mehr zur Realität passt.
Dead Letter Queues
Manche Nachrichten scheitern jedes Mal: eine fehlerhafte Nutzlast, eine Bestellung für ein Restaurant, das es nicht mehr gibt, ein Bug, den eine bestimmte Eingabe auslöst. Endlos wiederholt, blockiert eine solche Poison Message eine Partition oder verschwendet für immer einen Worker. Eine Dead Letter Queue (eingeführt in Abschnitt 6.4) nimmt Nachrichten auf, die eine festgelegte Anzahl von Malen gescheitert sind (SQS nennt das den Maximum Receive Count in einer Redrive Policy), damit die übrige Arbeit weiterlaufen kann.
Eine Dead Letter Queue ist ein Werkzeug für Defekte, nicht für Überlast: Landen Nachrichten dort, weil Consumer zu langsam sind, liegt die Lösung in Kapazität oder Backpressure, nicht in einer zweiten Queue. Jede Nachricht in einer DLQ bedeutet, dass etwas, das das Geschäft erwartet hat, nicht passiert ist. Die DLQ braucht daher einen Verantwortlichen, einen Alarm auf ihre Tiefe oder ihr Alter, Werkzeuge, um Nachrichten mit ihrem Fehler und ihrer Versuchszahl zu untersuchen, und einen Weg, sie erneut abzuspielen, sobald die Ursache behoben ist. Legen Sie im Voraus fest, was ein verspätetes Replay für die Reihenfolge bedeutet und für Nachrichten, deren geschäftlicher Moment vorbei ist: Eine Erstattung kann einen Tag später nachgeholt werden, eine Kurierzuweisung nicht.
Prüfen Sie Ihr Verständnis
Worker verarbeiten 200 Nachrichten pro Sekunde, und Nachrichten dürfen höchstens 30 Sekunden warten. Was ist die größte akzeptable Queue-Länge?
Nach Littles Gesetz 200 × 30 = 6000 Nachrichten; eine längere Queue bedeutet, dass manche Nachrichten länger als 30 Sekunden warten.
Warum ist das Alter der ältesten Nachricht ein besserer Alarm als die Queue-Länge?
Das Alter misst die Verzögerung, die Benutzer erleben, und steigt sofort, wenn Consumer stocken, während eine normal wirkende Länge eine festhängende Queue oder einen langsamen Abbau verbergen kann.
Ein Token Bucket hat die Kapazität 50 und füllt sich mit 10 pro Sekunde. Wie viele Anfragen kann ein Client in der ersten Sekunde nach einer langen ruhigen Phase stellen?
Etwa 60: die 50 Tokens, die bereits im Eimer liegen, plus die 10, die in dieser Sekunde hinzukommen.
Was ist der Unterschied zwischen Load Shedding und Rate Limiting?
Load Shedding schützt den Server vor völliger Überlast, egal wer sie verursacht; Rate Limiting begrenzt, was jeder Client senden darf, damit Kapazität fair geteilt 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: „Unsere Queue für den Ticketverkaufsstart wuchs auf 200.000 Bestellungen, und die Kunden warteten 40 Minuten auf ein Ergebnis. Wie hätten wir die Queue begrenzen sollen, und was sollte mit den überzähligen Bestellungen geschehen?“