Messaging: Arbeit übergeben, ohne die Leitung zu halten
Synchrone Aufrufe gegen Nachrichtenwarteschlangen, was eine Warteschlange wirklich bringt, warum es genau-einmal-Zustellung nicht gibt und wie ein idempotenter Consumer aus mindestens einmal etwas Auslieferbares macht.
Die vorige Lektion endete bei Wiederholungen: ein Aufrufer, der nicht weiß, ob seine Anfrage verarbeitet wurde, und ein Idempotenzschlüssel, der die Wiederholung sicher macht. Diese Lektion nimmt diese Idee und folgt ihr aus einem einzelnen Dienst heraus in die Art, wie Dienste miteinander sprechen, denn eine Warteschlange ist der Ort, an dem das Wiederholungsproblem zum Normalfall statt zum Randfall wird.
Der Weg, den du zuerst gelernt hast, ist der synchrone Aufruf: Dienst A schickt eine Anfrage an Dienst B und wartet auf die Antwort. Er ist leicht zu durchdenken, weil die Antwort im selben Atemzug wie die Frage kommt und Fehler offensichtlich sind. Sein Problem ist nicht, dass er langsam ist. Sein Problem ist, dass er die beiden Dienste in der Zeit koppelt: B muss genau in dem Moment laufen, erreichbar und schnell sein, in dem A es braucht, sonst scheitert A ebenfalls. Setz drei Dienste in eine Kette und multipliziere diese Momente. Ist jeder zu 99,9 Prozent verfügbar, ist die Kette zu 99,7 Prozent verfügbar, was eine Verzehnfachung der Ausfallzeit für ein System bedeutet, dessen Teile einzeln hervorragend sind.
Die Alternative ist, aufzuhören zu warten. Der Bestelldienst schreibt die Bestellung, veröffentlicht eine kurze Nachricht, dass eine Bestellung aufgegeben wurde, und antwortet sofort. Etwas anderes, getrennt davon, nimmt die Nachricht auf und sendet die E-Mail. Die beiden Teile müssen nicht mehr im selben Moment leben, und der Kunde bekommt eine Antwort in Zehntelmillisekunden. Das Wort für den Mechanismus ist Warteschlange oder Broker, und die wichtige Verschiebung liegt nicht in der Technologie, sondern in der Richtung der Abhängigkeit: Der Bestelldienst hängt nun davon ab, dass die Warteschlange eine Nachricht annimmt, eine kleine, schnelle, langweilige Operation, statt davon, dass der Mailanbieter Erfolg hat, was langsam und außerhalb deiner Kontrolle ist.
Es lohnt sich, genau zu sagen, was die Warteschlange wirklich bringt, denn sie wird als Heilmittel für alles verkauft. Sie bringt vier Dinge. Entkopplung: Der Produzent weiß nicht und kümmert sich nicht darum, wer die Nachricht konsumiert, also kannst du einen zweiten Consumer hinzufügen, ohne den Produzenten anzufassen. Pufferung: Eine Spitze kommt als tiefe Warteschlange an statt als abgestürzter Consumer, und die Warteschlange leert sich mit dem Tempo, das der Consumer schafft. Wiederholungen: Die erneute Zustellung ist jetzt Aufgabe des Brokers, der Produzent muss also kein Backoff implementieren. Und Haltbarkeit: Ein guter Broker hält die Nachricht auf der Platte, ein neu gestarteter Consumer verliert also nicht die Arbeit, die auf ihn wartete.
SYNCHRONOUS CALL MESSAGE QUEUE
caller waits caller gets 202 now
both must be up consumer may be down
load hits the callee queue absorbs the spike
retry is the caller problem redelivery is the broker job
one failure, one error one failure, a growing depthDieselbe Bestellung, die eine E-Mail und eine Rechnung auslösen muss, auf zwei Arten angeordnet.Nun der Teil, der entscheidet, wie du den Consumer schreibst. Jedes Nachrichtensystem muss eine Frage beantworten: Was passiert, wenn der Consumer zwischen dem Empfang einer Nachricht und dem Abschluss seiner Arbeit abstürzt? Es gibt nur drei ehrliche Antworten. Höchstens einmal bedeutet, der Broker sendet die Nachricht und vergisst sie. Stürzt der Consumer mitten in der Arbeit ab, ist die Nachricht weg, und der Kunde bekommt nie eine Rechnung. Mindestens einmal bedeutet, der Broker behält die Nachricht, bis der Consumer sie bestätigt, und stellt sie erneut zu, wenn keine Bestätigung eintrifft. Die Arbeit geht nie verloren und kann zweimal getan werden, weil die Bestätigung selbst verloren gehen kann, nachdem die Arbeit gelang. Genau einmal ist das, worum alle bitten, und es ist überhaupt kein Zustellmodus.
AT MOST ONCE send, forget, never retry
risk: a lost payment
AT LEAST ONCE send, retry until acked
risk: a duplicate payment
this is what brokers give you
EXACTLY ONCE not a delivery mode
it is at-least-once plus a dedup keyZwei echte Zustellgarantien und eine, die tatsächlich eine Kombination der zweiten mit Anwendungslogik ist.Der Grund, warum mindestens einmal unvermeidbar ist, ist ein Fenster, das man nicht schließen kann. Der Consumer empfängt die Nachricht, erledigt die Arbeit und bestätigt dann. Stürzt er nach der Arbeit, aber vor der Bestätigung ab, kann der Broker unmöglich wissen, dass die Arbeit geschah, weil er die Bestätigung nie erhalten hat. Erneut zuzustellen ist die einzige sichere Wahl, denn die Alternative ist, eine Rechnung stillschweigend fallen zu lassen. Du kannst das Fenster verkleinern, indem du nach der Arbeit bestätigst statt davor, aber du kannst es nicht beseitigen, weil die Bestätigung selbst über denselben unzuverlässigen Weg reist wie alles andere. Das ist keine Einschränkung eines bestimmten Brokers. RabbitMQ, Kafka und SQS verhalten sich so, und kein Konfigurationsschalter ändert das.
Die praktische Antwort ist also, den Consumer idempotent zu machen, und das ist derselbe Idempotenzschlüssel aus der vorigen Lektion, verschoben auf die Consumerseite. Der Consumer schreibt die Nachrichten-ID in dieselbe Transaktion wie die Arbeit, die er verrichtet. Wird die Nachricht erneut zugestellt, scheitert das Einfügen der ID am Primärschlüssel, die Transaktion wird zurückgerollt, und der zweite Versuch tut nichts. Es ist bewusst langweilig: eine Tabelle, eine Eindeutigkeitsbedingung, eine Transaktion, die beides zusammen tut. Die Reihenfolge ist enorm wichtig. Wenn du erst die ID schreibst und dann die Arbeit machst, verliert ein Absturz dazwischen die Arbeit für immer. Wenn du erst die Arbeit machst und dann die ID schreibst, verdoppelt ein Absturz dazwischen die Arbeit. Nur beides in einem atomaren Schritt zu tun ist richtig, weshalb die Tabelle in derselben Datenbank wie der Effekt leben muss.
CREATE TABLE processed (
message_id text PRIMARY KEY,
handled_at timestamptz NOT NULL DEFAULT now()
);
-- one transaction: record the id and do the work together
BEGIN;
INSERT INTO processed (message_id) VALUES ('9f2c-1');
UPDATE invoices SET sent = true WHERE id = 9001;
COMMIT;
-- a redelivery hits the primary key and rolls backDie Struktur, die aus einer erneuten Zustellung ein Nichts macht. Beide Anweisungen werden zusammen festgeschrieben oder keine.Selbst ein idempotenter Consumer kann scheitern, und es ist wichtig, zwei Arten von Fehlern zu trennen. Ein vorübergehender Fehler ist ein ausgefallener Mailanbieter: wiederhole, und es klappt wahrscheinlich. Ein dauerhafter Fehler ist eine Nachricht mit fehlerhaftem Inhalt oder mit Bezug auf eine gelöschte Bestellung: wiederhole für immer, und es klappt nie. Einen dauerhaften Fehler für immer zu wiederholen ist das schlechteste verfügbare Ergebnis, denn diese eine Nachricht blockiert die Warteschlange hinter sich und stoppt damit auch jede gesunde Nachricht. Die übliche Antwort ist eine begrenzte Wiederholungszahl mit exponentiellem Backoff, und nach dem letzten Versuch geht die Nachricht in eine Dead-Letter-Queue: eine getrennte Warteschlange, aus der niemand automatisch konsumiert.
MAIN QUEUE orders.email
RETRY backoff 1 s, 10 s, 100 s, 1000 s
MAX DELIVERIES 5
DEAD LETTER QUEUE orders.email.dlq
ALERT ON dlq depth rises at allDie Konfiguration, die verhindert, dass eine Giftnachricht die ganze Pipeline mitreißt.Backpressure ist der Name dafür, was passiert, wenn Zugänge die Rate übersteigen, die der Consumer schafft, und die Warteschlange macht es zugleich einfacher und gefährlicher. Einfacher, weil der Produzent nicht blockiert wird: Er nimmt weiter Bestellungen an, und die Warteschlange wächst. Gefährlicher aus demselben Grund: Nichts drückt zurück, das System nimmt also Arbeit an, die es nicht beenden kann, und das einzige sichtbare Symptom ist eine steigende Zahl. Es gibt zwei ehrliche Antworten. Die Consumer skalieren, was funktioniert, wenn die Spitze schnell vorbei ist oder die Arbeit leicht zu parallelisieren. Oder neue Arbeit ablehnen, das heißt, der Produzent prüft die Warteschlangentiefe und liefert über einer Schwelle eine Beschäftigt-Antwort. Abzulehnen ist unangenehm und ab einem gewissen Punkt meist richtig, denn eine Bestellung anzunehmen, die du nächsten Dienstag lieferst, ist schlechterer Service als jetzt nein zu sagen.
Die Zahl, die man beobachten muss, ist das Alter der Warteschlange, nicht ihre Tiefe. Die Tiefe allein sagt wenig: 50 000 Nachrichten sind in Ordnung, wenn der Consumer sie in zehn Sekunden abarbeitet, und ein Notfall, wenn dieselben 50 000 vier Stunden brauchen. Das Alter beantwortet die einzige Frage, die für einen Kunden zählt: Wie lange wartet eine jetzt veröffentlichte Nachricht, bevor sich jemand um sie kümmert. Eine einzige Zahl, das Alter der ältesten unverarbeiteten Nachricht, verwandelt einen technischen Rückstand in ein Versprechen über eine Lieferzeit.
Eine letzte Eigenschaft, weil sie verändert, wie du Schlüssel entwirfst. Eine Warteschlange bewahrt die Reihenfolge der Veröffentlichung nicht automatisch, und zusätzliche Consumer machen es schlimmer: Mit drei Workern können zwei Nachrichten über dieselbe Bestellung in beliebiger Reihenfolge oder gleichzeitig bearbeitet werden. Die übliche Lösung ist Routing nach einem Schlüssel, damit alles zu einer Bestellung immer in dieselbe Partition geht und von einem Consumer der Reihe nach verarbeitet wird. Dieser Schlüssel ist eine Designentscheidung mit demselben Beigeschmack wie ein Shard-Schlüssel: Die Bestell-ID zu wählen gibt Reihenfolge je Bestellung, die Nutzer-ID zu wählen gibt Reihenfolge je Nutzer und stärkere Garantien über zusammenhängende Bestellungen, und nichts zu wählen bedeutet, dass jeder Consumer Nachrichten tolerieren muss, die außer der Reihe ankommen.
Praxis
Ein Bestelldienst veröffentlicht nach dem Speichern der Bestellung eine Nachricht in einer Warteschlange. Der Mail-Worker stürzt für zehn Minuten ab. Was erlebt der Kunde, und warum funktioniert das Design hier wie beabsichtigt?
Hinweise
- Niemand wartet auf den Worker, also kann keine Anfrage seinetwegen scheitern.
- Der Broker hält die Nachricht, bis jemand sie bestätigt.
- Die Bestellung ist haltbar; die Benachrichtigung ist nicht die Bestellung.
Eine Giftnachricht scheitert bei jedem Versuch und blockiert eine Partition. Bringe die Reaktionen in die Reihenfolge, in der ein sorgfältiges Team sie anwenden würde.
- 1Den Worker eine begrenzte Anzahl von Malen mit exponentiellem Backoff wiederholen lassen
- 2Auf die Dead-Letter-Tiefe alarmieren, damit ein Mensch sie sofort sieht
- 3Die Nachricht nach dem letzten Versuch in die Dead-Letter-Queue verschieben
- 4Erst danach die Warteschlange von bereits bearbeiteten Nachrichten befreien
- 5Die Ursache beheben und die Nachricht dann aus der Dead-Letter-Queue erneut einspielen
- 6Die fehlschlagende Nachricht lesen und feststellen, ob der Fehler vorübergehend oder dauerhaft ist
Hinweise
- Verstehe erst, warum diese Nachricht anders ist als die anderen, bevor du automatisierst.
- Wiederhole einige Male mit wachsender Verzögerung und schaff sie dann aus dem Weg.
- Eine Warteschlange, die niemand beobachtet, ist dasselbe wie die Nachricht fallen zu lassen.
Der Handler unten erhält dieselbe Nachricht zweimal, weil die Warteschlange mindestens einmal zustellt. Mach ihn idempotent: Die zweite Zustellung darf keine zweite E-Mail und keine zweite Rechnung senden.
Der Code läuft bei dir, nicht bei uns — wir führen ihn nicht aus. Prüfe deine Lösung an jeder Anforderung und markiere die erfüllten.
Hinweise
- Jede Nachricht trägt eine ID; der Zustand sollte sich merken, welche IDs bearbeitet wurden.
- Gib ein Ergebnis zurück, das sagt, ob diese Zustellung die Arbeit tat oder eine Wiederholung war.
- Eine Wiederholung muss trotzdem Erfolg melden und nicht einen Fehler.
Fragen zu dieser Lektion
Ist das kostenlos?
Ja. Die erste Ebene jedes Fachs ist kostenlos und braucht kein Konto. Tiefere Ebenen und die höheren Stufen sind kostenpflichtig — genau das hält das Projekt am Leben.
Für welches Alter ist das gedacht?
Für neugierige Jugendliche ab etwa 12 Jahren und für Erwachsene, die bei null anfangen. Programmiererfahrung wird nicht vorausgesetzt; die Lektionen zu Netzwerken und Physik brauchen keine Mathematik über Schulniveau.
Muss ich schon programmieren können?
Nein. Logik, Physik und Netzwerke beginnen bei null. Code schreibst du im Bereich Programmiersprachen — und auch der fängt vorne an.
Muss ich etwas installieren?
Nein. Alles läuft im Browser: Theorie, Übungen und die interaktiven Simulationen. Es gibt nichts herunterzuladen und nichts einzurichten.
Wie lange dauert ein Fach?
Eine Lektion dauert 10 bis 15 Minuten. Eine Junior-Stufe umfasst etwa 12 bis 20 Lektionen, also einige Wochen regelmäßiger Übung statt eines Abends.
Muss ich der Reihenfolge folgen?
Innerhalb einer Stufe öffnen sich die Ebenen nacheinander, weil jede auf der vorherigen aufbaut. Zwischen den Fächern bist du frei: beginne mit Logik, Physik oder Netzwerken.