SkillGridInformatik durch Machen lernen

Konsistenz: was du aufgibst, wenn Daten an zwei Orten liegen

CAP als echter Kompromiss statt als Schlagwort, starke gegen eventuelle Konsistenz, Replikationsverzögerung und das Lesen der eigenen Schreibvorgänge, und warum Wiederholungen jeden Schreibvorgang idempotent machen müssen.

17 Min. · 55 XP

Die vorige Lektion endete mit einem unbehaglichen Ton. Du hast Leserepliken ergänzt, um mehr Verkehr zu bewältigen, und der allererste Kauf war eine zweite Kopie der Wahrheit, die der ersten widersprechen kann. Diese Lektion handelt von diesem Widerspruch: wann er auftritt, was er kostet und wie man verhindert, dass er einen Nutzer als Fehler erreicht.

In dem Moment, in dem Daten auf zwei Maschinen existieren, taucht eine Frage auf, die es vorher nicht gab: Welche Kopie ist richtig? Auf einer Maschine ist die Antwort trivial, weil es nur eine gibt. Auf zwei Maschinen musst du zwei getrennte Dinge entscheiden. Erstens: Was darf ein Lesevorgang zurückgeben, während ein Schreibvorgang sich noch ausbreitet. Zweitens: Was tun, wenn die Maschinen gar nicht miteinander sprechen können. Beide Entscheidungen haben echte Kosten, und keine lässt sich durch bessere Hardware vermeiden. Genau darüber streitet man unter dem Namen CAP, also lohnt es sich, diesen Streit exakt zu verstehen.

Du hast CAP wahrscheinlich als wähle zwei von drei gehört: Consistency, Availability, Partition tolerance. Diese Version ist einprägsam, wird überall wiederholt und führt in die Irre, und zwar so, dass sie zu schlechten Entscheidungen verleitet. Wörtlich gelesen legt sie nahe, man könne Konsistenz und Verfügbarkeit wählen und die Partitionstoleranz einfach weglassen. Das kann man nicht. Eine Partition ist keine Einstellung, die man ablehnt; sie ist ein gezogenes Netzwerkkabel, ein neu startender Switch, eine gesättigte Verbindung zwischen Rechenzentren, eine Garbage-Collector-Pause, die lang genug ist, dass die andere Maschine aufgibt. Läuft dein System auf mehr als einer über ein Netz verbundenen Maschine, werden Partitionen passieren, und Partitionstoleranz steht nicht auf der Speisekarte.

ClientNode ANode Bthe link between them is downwrite balance = 90acceptedA has 90, B still has 100read my balance100B could not have known about the writeno partition means no tradeoff at all

Beachte, was jeder Zweig wirklich kostet, denn sie versagen auf völlig verschiedene Weise. Die Antwort zu verweigern ist laut: Ein Nutzer sieht eine Fehlerseite, der Bezahlknopf liefert 503, und du erfährst davon innerhalb von Sekunden durch dein Monitoring. Kopien auseinanderlaufen zu lassen ist leise: Jede Anfrage gelingt, die Seite ist schnell, und zwei Nutzer betrachten zwei verschiedene Kontostände, ohne dass irgendetwas auf ein Problem hindeutet. Was du willst, hängt von der Funktion ab, nicht vom Unternehmen. Für eine Banküberweisung ist das laute Versagen offensichtlich richtig, denn eine selbstsicher falsche Antwort über das Geld von jemandem ist schlimmer als keine Antwort. Für einen Like- oder Aufrufzähler ist das leise Versagen offensichtlich richtig, denn niemand nimmt Schaden davon, dass ein Zähler zwei Sekunden lang um eins danebenliegt.

WHEN THE LINK BREAKS, PICK ONE BEHAVIOUR

  refuse the write        A returns 503 until the link returns
  accept and diverge      A returns 200, B stays stale
  accept a quorum only    A needs 2 of 3 nodes to answer
  last write wins         conflicts resolved by timestamp

  the partition is never optionalDie vier echten Optionen während einer Partition. Beachte, dass drei davon Varianten von Verweigern oder Auseinanderlaufen sind, keine dritte Achse.

Eine weitere Korrektur ist wichtig, weil sie täglich Designentscheidungen verändert. Konsistenz betrifft nicht nur Ausfälle; sie kostet auch Latenz, wenn alles gesund ist. Starke Konsistenz bedeutet, dass ein Schreibvorgang nicht abgeschlossen ist, bis genügend Kopien bestätigt haben, dass er stattgefunden hat. Liegen diese Kopien in drei Rechenzentren auf drei Kontinenten und beträgt der Netzwerk-Roundtrip zwischen ihnen 70 ms, dann bezahlt ein Schreibvorgang, der von einer Mehrheit bestätigt werden muss, diese Distanz bei jeder einzelnen Anfrage. Deshalb wird der CAP-Kompromiss manchmal als Wahl zwischen Konsistenz und Latenz umformuliert, selbst wenn nichts kaputt ist: Übereinstimmung braucht Zeit proportional zur Distanz zwischen den Kopien, und die Distanz ist nicht verhandelbar.

Nun die zwei Alltagswörter. Starke Konsistenz bedeutet, dass nach der Bestätigung eines Schreibvorgangs jeder spätere Lesevorgang ihn überall sieht. Es gibt keinen Moment, in dem ein korrekter Lesevorgang den alten Wert zurückgeben kann. Der Preis ist, dass jeder Lesevorgang entweder auf Übereinstimmung wartet oder über den einen Ort geleitet wird, der Autorität hat, und das ist genau so langsam und zerbrechlich, wie es klingt. Eventuelle Konsistenz verspricht das Gegenteil, und es lohnt sich, das präzise zu sagen, weil es meist schlecht gesagt wird: Nachdem die Schreibvorgänge aufgehört haben, konvergieren die Kopien, und wenn keine neuen Schreibvorgänge eintreffen, werden sie sich innerhalb einer begrenzten Zeit einig. Über das, was ein Lesevorgang zwischendurch sieht, gibt es kein Versprechen. Diese Lücke ist kein Fehler des Modells, sie ist das Modell.

BANK BALANCE                     LIKE COUNTER

  money leaves an account          a stranger taps a heart
  a wrong read costs money         a wrong read costs nothing
  must be strong                   eventual is fine
  cost: one writer at a time       cost: two users see 41 vs 42
  reject a write rather than lie   show 41 now, 42 in 800 msDasselbe verteilte System, das beide Datenarten gleichzeitig hält, mit zwei verschiedenen Versprechen darüber.

Die wichtige Folge ist, dass Konsistenz keine Eigenschaft des Systems ist, sondern eine Eigenschaft der Daten. Dieselbe Anwendung kann einen Kontostand unter starker Konsistenz und einen Like-Zähler unter eventueller Konsistenz halten, und niemand findet das seltsam, sobald man es ausspricht. Frag, was eine falsche Antwort kostet. Wenn ein veralteter Lesevorgang eines Kontostands es jemandem erlauben könnte, Geld zweimal abzuheben, muss dieser Lesevorgang stark sein, und es lohnt sich, dafür Latenz und Verfügbarkeit zu bezahlen. Wenn ein veralteter Lesevorgang eines Like-Zählers 800 Millisekunden lang 41 statt 42 zeigt, kostet das, dass es niemand merkt, und dort für starke Konsistenz zu bezahlen wäre reine Verschwendung. Die Wahl je Funktion ist die ganze Kunst, und sie ist ein Geschäftsurteil, technisch ausgedrückt.

Zwischen stark und eventuell gibt es eine mittlere Option, die die Mehrheit echter Beschwerden löst, und du hast sie in der vorigen Lektion getroffen: das Lesen der eigenen Schreibvorgänge. Sie ist schwächer als starke Konsistenz, weil andere Nutzer den alten Wert sehen können, und stärker als reine eventuelle, weil die Person, die die Änderung gemacht hat, sie immer sieht. Sie kostet eine Routing-Regel: Lesevorgänge eines Nutzers, der kürzlich geschrieben hat, gehen an den Primary. Die meisten Produkte brauchen genau das und nichts mehr, was man sich merken sollte, wenn jemand vorschlägt, das ganze System streng konsistent zu machen, um eine Profilseite zu reparieren.

Jetzt der Teil, der alles zusammenbindet, und das ist das Konzept, das ein funktionierendes verteiltes System von einer Demonstration unterscheidet. Jeder Netzwerkaufruf kann so fehlschlagen, dass du nicht weißt, was passiert ist. Die Anfrage kann auf dem Hinweg verloren gegangen sein, oder sie wurde verarbeitet und die Antwort ging auf dem Rückweg verloren. Für den Client sehen diese beiden Fälle identisch aus: ein Timeout. Also wiederholt der Client, und wenn der erste Versuch tatsächlich gelungen ist, ist die Wiederholung eine zweite echte Zahlung, eine zweite E-Mail, ein zweiter Versand. Nicht zu wiederholen ist auch keine Option, denn das macht aus jedem kurzen Ausschlag einen für den Nutzer sichtbaren Fehler. Das ist dieselbe Überlegung wie bei der Zustellung mindestens einmal, und sie gilt für jeden RPC, nicht nur für Nachrichtenwarteschlangen.

CREATE TABLE payments (
  id           bigserial PRIMARY KEY,
  idem_key     text NOT NULL UNIQUE,
  account_id   bigint NOT NULL,
  amount_cents integer NOT NULL
);

-- the retry sends the same idem_key, so the second insert loses
INSERT INTO payments (idem_key, account_id, amount_cents)
VALUES ('pay-9f2c-1', 42, 1290)
ON CONFLICT (idem_key) DO NOTHING;Die einzige Struktur, die einen wiederholten Schreibvorgang sicher macht: Der Client liefert einen Schlüssel, und die Datenbank erzwingt, dass der Schlüssel einmal verwendet wird.

Es gibt eine Abkürzung, die einen Teil dieser Maschinerie spart: Mach die Operation von vornherein idempotent. Einen Wert auf einen bestimmten Zustand zu setzen, etwa Status gleich shipped, lässt sich beliebig oft mit demselben Ergebnis wiederholen, braucht also keinen Schlüssel. Etwas zu einem Wert hinzuzuaddieren, etwa Kontostand gleich Kontostand plus 100, geht nicht, weil die Wiederholung das Ergebnis ändert. Beachte, dass das eine Eigenschaft der Operation ist, nicht des Codes, der sie aufruft. Wo die Geschäftsregel es erlaubt, bevorzuge die sicher wiederholbare Operation, und markiere die anderen mit einem ausdrücklichen Idempotenzschlüssel, damit der unsichere Fall eine bewusste, sichtbare Entscheidung ist und kein Versehen.

Das letzte harte Problem ist, zwei getrennte Systeme gemeinsam zu ändern. Innerhalb einer Datenbank gibt dir eine Transaktion Atomarität: Mehrere Anweisungen gelingen oder werden alle zurückgerollt, und du siehst nie einen halb angewandten Zustand. Über zwei Dienste mit zwei Datenbanken ist diese Garantie in derselben Form nicht verfügbar. Das klassische Protokoll, der Zweiphasen-Commit, lässt einen Koordinator jeden Teilnehmer um Vorbereitung bitten und ihnen dann allen sagen, sie sollen festschreiben. Es funktioniert und hat eine berühmte Schwäche: Der Koordinator ist ein einzelner Ausfallpunkt, und stirbt er nach der Vorbereitungsphase, bleiben Teilnehmer mit Sperren zurück und warten, manchmal lange. Deshalb ist seine Verwendung meist auf Systeme beschränkt, die sich die Verzögerung und die Abstimmungskosten leisten können.

Die praktische Alternative ist zu akzeptieren, dass es Zwischenzustände geben wird, und dafür zu entwerfen. Eine Saga zerlegt die Arbeit in lokale Transaktionen, eine je Dienst, die jeweils selbst festschreiben, plus eine Folge von kompensierenden Aktionen, die die früheren Schritte rückgängig machen, wenn ein späterer fehlschlägt. Eine Bestellung aufzugeben wird zu: Bestand reservieren, Karte belasten, und wenn das Erstellen des Versands fehlschlägt, die Karte erstatten und den Bestand freigeben. Beachte, worauf das ehrlich verzichtet. Es gibt ein Fenster, in dem das Geld genommen und die Bestellung noch nicht bestätigt ist, und Nutzer können dieses Fenster sehen, wenn du es nicht hinter einem Status wie in Bearbeitung versteckst. Das sind echte Kosten, und der Nutzen ist, dass kein Dienst eine Sperre hält, während er auf einen anderen wartet, sodass kein einzelner Ausfall den ganzen Ablauf einfriert. Die meisten Systeme, die das brauchen, greifen zur Saga, weil sie in kleinen, wiederherstellbaren Stücken versagt, statt in einem großen zu stocken.

Praxis

  1. Ein Zahlungsdienst und ein Betrugsprüfdienst laufen in zwei Rechenzentren. Die Verbindung zwischen ihnen bricht. Das Team muss entscheiden, was der Zahlungsdienst mit eingehenden Anfragen tut. Welche Überlegung ist richtig?

    Hinweise
    • Das Netz ist keine der Optionen, die man ablehnen kann.
    • Frag, was eine selbstsicher falsche Antwort über Geld kostet.
    • Laut verweigern oder falsch antworten sind die echten Alternativen.
  2. Ordne jeder Garantie das Versprechen zu, das sie wirklich gibt, und das, was sie kostet.

    Hinweise
    • Eine davon verspricht etwas nur über die Person, die geschrieben hat.
    • Konvergenz ist ein Versprechen über die Zukunft, nicht über den nächsten Lesevorgang.
    • Frag, wer den alten Wert sehen darf und wie lange.
  3. Die Zahlungs-API unten wird bei einem Timeout wiederholt aufgerufen. Mach sie sicher: Derselbe Schlüssel darf nie eine zweite Zahlung erzeugen, und ein wiederholter Aufruf muss trotzdem das Ergebnis des ersten melden.

    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
    • Der Speicher merkt sich, was er schon gesehen hat, geschlüsselt nach dem Idempotenzschlüssel.
    • Eine Wiederholung sollte das gespeicherte Ergebnis zurückgeben, nicht einen neuen Eintrag erzeugen.
    • Gib ein Objekt mit der Zahlungs-ID und der Angabe zurück, ob dieser Aufruf sie erzeugt hat.

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.