Zum Hauptinhalt springen

Die eigenen Schreibvorgänge lesen: Das Zaris-Konsistenzmodell, ehrlich dargestellt

· 12 Minuten Lesezeit
Clustron Team
Engineering für verteilte Systeme

Die eigenen Schreibvorgänge lesen — ein Owner pro Partition

Es gibt eine Frage, die jeder Entwickler irgendwann an einen verteilten Store richtet, meist direkt nach einem verwirrenden Bug-Report: Wenn ich einen Wert schreibe und ihn sofort zurücklese, sehe ich dann garantiert, was ich geschrieben habe? Das klingt, als müsste es eine Ein-Wort-Antwort geben. Gibt es nicht — nicht für irgendeinen partitionierten, replizierten Store, und nicht für Zaris. Die ehrliche Antwort lautet „ja, innerhalb einer Partition, weil Lesevorgänge zum selben einzelnen Owner gehen wie Ihr Schreibvorgang" — und das Interessante ist alles, was in dieser Einschränkung steckt.

Dieser Beitrag ist das Konsistenzmodell von Zaris an einem Ort zusammengetragen. Über die Einzelteile haben wir separat geschrieben — optimistisches Compare-and-Swap, transparenter Retry bei Ownership-Churn, das wiederabspielbare Op-Log, das Replikate speist, Multi-Key-Transaktionen. Hier verbinden wir sie aus Sicht des Lesers und beantworten die einzige Frage, die an der Tastatur zählt: Was wird mir beim Lesen tatsächlich zugesichert? Einschließlich, und vor allem, wo die Zusicherung endet.

Das Modell in einem Satz​

Zaris ist ein AP-lastiger Store, bei dem die Konsistenzgrenze um die Ownership gezogen ist, nicht um jeden Schreibvorgang. Die Datenebene bleibt verfügbar — ein einzelner Node bedient weiter die Segmente, die er hält —, und die Single-Writer-Disziplin, die Lesevorgänge vernünftig hält, wird in der Ownership-Ebene durchgesetzt: Zu jedem Zeitpunkt gibt es genau einen aktiven Owner pro Partition. Schreibvorgänge auf eine Partition werden durch diesen einen Node serialisiert. Auf dem Schreibpfad gibt es bewusst kein Quorum und keinen Konsens, und Zaris behauptet nicht globale strikte Linearisierbarkeit.

Fast alles, was ein Leser beobachtet, folgt aus dieser einen strukturellen Tatsache, daher lohnt es, die Kette explizit zu machen:

key → segment → partition → one runtime active owner

Ein Schlüssel wird deterministisch auf ein Segment gehasht; ein Segment gehört zu einer Partition; eine Partition hat genau einen aktiven Owner, der ihre Schreibvorgänge annimmt und ihre Lesevorgänge bedient. Ownership ist eine Lease, die in der Partition-Map als (LeaderTerm, OwnershipGeneration) geführt wird — vom Cluster-Leader vergeben, über Neustarts hinweg dauerhaft und an jeden Node propagiert. Diese Lease garantiert das „genau einer" — sie ersetzte ein älteres Schema „höchste Epoch gewinnt", das bei einem Neustart zurückgesetzt werden konnte und kurzzeitig zwei Nodes hervorbrachte, die beide glaubten, dieselbe Partition zu besitzen.

Warum Owner-geroutete Lesevorgänge Read-your-writes liefern​

Hier kommt der Teil, der die Eingangsfrage beantwortet. In Zaris werden Lesevorgänge zum aktiven Owner geroutet, demselben Node, an den Ihr Schreibvorgang ging. Clients lesen standardmäßig nicht von Replikaten. Wenn Sie also einen Schlüssel mit PutAsync schreiben und dann mit GetAsync lesen, werden beide Requests — über dieselbe Routing-Tabelle, aufgebaut aus der Partition-Map plus dem Runtime-Ownership-Snapshot — zum selben Owner-Node aufgelöst, und dieser Node hat Ihren Schreibvorgang bereits in seinem lokalen Store.

Das ist der gesamte Mechanismus. Read-your-writes wird nicht als Session-Garantie oder clientseitiger Cache aufgesetzt; es ergibt sich aus dem Routing. Sie lesen Ihre Schreibvorgänge innerhalb einer Partition, weil ein einzelner Node die maßgebliche Instanz für Lese- und Schreibvorgänge dieser Partition ist und Schreibvorgänge darauf nacheinander serialisiert werden. Das Replikat in diesem Diagramm mag einige Millisekunden hinter dem Owner zurückliegen — die Replikation hat einen asynchronen Schwanz —, aber standardmäßig lesen Sie nie das Replikat, sodass dieser Lag für Sie unsichtbar bleibt.

Die Einschränkung „innerhalb einer Partition" ist tragend, und wir kommen darauf zurück, was sie ausschließt.

Die Version, die Sie sehen dürfen​

Jeder Wert, den der Owner ausliefert, trägt zwei monotone Zahlen, und sie machen „derselbe Wert" zu einer präzisen Aussage statt zu einem Gefühl:

  • eine vom Store verwaltete Revision — eine strikt steigende Sequenznummer, die der Owner jeder Mutation in Commit-Reihenfolge aufprägt, und
  • eine ItemVersion pro Item — die Version dieses konkreten Schlüssels, die IfMatchVersion in einem Compare-and-Swap-Schreibvorgang absichert.

Da der Owner diese in Commit-Reihenfolge vergibt und Lesevorgänge aus demselben committeten Zustand bedient, bewegen aufeinanderfolgende Lesevorgänge eines Schlüssels vom Owner die Version vorwärts, nie rückwärts. Sie lesen nicht Version 42 und einen Augenblick später Version 40. Diese Monotonie ist genau das, was die CAS-Schleife korrekt macht: Ein Leser sieht eine Version, rechnet dagegen und schreibt unter der Bedingung zurück, dass sie noch diese Version ist. Ließe das Modell Lesevorgänge zwischen Versionen flattern, stünde optimistische Nebenläufigkeit auf Sand. Ein Owner, in Commit-Reihenfolge vergebene Revisionen, vom Owner bediente Lesevorgänge — das ist das Fundament, auf dem die gesamte Koordinationsgeschichte im CAS-Beitrag steht.

Das Fenster, in dem ein Lesevorgang ehrlich „noch nicht" sagt​

Owner-geroutete Lesevorgänge sind im Normalbetrieb sauber. Die interessanten Momente sind die Übergänge — ein Failover, eine Migration, ein bevorzugter Primary, der zurückfällt —, denn während einer Übergabe ist der Node, zu dem Sie routen würden, möglicherweise gerade nicht die maßgebliche Instanz. Zaris trifft hier die ehrliche Wahl: Statt einen Lesevorgang von einem Node zu bedienen, der nicht der beständige Owner ist, liefert es einen wiederholbaren Status. Ein Lese- (oder Schreib-)Vorgang in einem solchen Fenster kann zurückkommen als:

  • Moved — Ihre Routing-Tabelle ist veraltet; der Schlüssel liegt jetzt auf einem anderen Node.
  • OwnershipChanging — der Besitz an diesem Segment befindet sich gerade aktiv im Übergang; es ist eine Übergabe, kein beständiger Routing-Fehler.
  • Migrating — das Segment wird gerade zu einem neuen Owner migriert.
  • ReplicaSyncPending — dieser Node bringt die Kopie eines Segments auf den neuesten Stand und bedient daraus noch keinen möglicherweise unvollständigen Lesevorgang.
  • Unavailable — die Partition hat in diesem Augenblick keinen erreichbaren Owner.

Keiner davon ist eine Exception, die Sie aus einem Stacktrace herauslesen müssen — es sind maschinenlesbare Ergebnisse, und der .NET-Client behandelt sie als vorübergehend und wiederholt mit Reroute, aktualisiert seine Cluster-Map und geht zu dem, den die Map nun als Owner nennt. Der Effekt: Ihr GetAsync dauert einfach ein paar Millisekunden länger und liefert dann den richtigen Wert. Die Designentscheidung darunter verdient klare Worte: Zaris lässt Sie lieber einen Moment warten, als Ihnen einen Lesevorgang zu geben, bei dem es sich nicht sicher ist, ob er maßgeblich ist. Ein wiederholbares „noch nicht" ist eine bessere Antwort als ein selbstsicheres Falsch.

Deshalb überlebt Read-your-writes auch ein Failover: Die Write-Fence-Arbeit sorgt dafür, dass ein bestätigter Schreibvorgang im Übergabefenster nicht zurückbleibt (zur ehrlichen Grenze davon kommen wir unten), und die Promotion macht einen Node erst zum Owner, wenn er dazu berechtigt ist — der Lesevorgang, der nach einem Failover umgeroutet wird, geht also zu einem Node, der Ihre Daten hat.

Der Regler, den Sie drehen können — und was er heute tatsächlich bewirkt​

Zaris stellt eine Oberfläche für Lesekonsistenz bereit. GetOptions trägt eine ReadConsistency von Leader, Quorum oder Any, und ein Store hat ein Flag AllowReplicaReads (standardmäßig aus). Die Absicht ist der klassische Handel: Lesevorgänge von Replikaten bedienen zu lassen, um die Leselast zu verteilen, im Austausch dafür, dass ein Replikat hinter dem Owner zurückliegen kann — eine begrenzte Veralterung, auf die Sie sich bewusst einlassen.

Hier der ehrliche aktuelle Stand, denn genau das würde Werbetext verwischen: Heute sind Lesevorgänge Owner-only. AllowReplicaReads und ReadConsistency.Any sind eine Konfigurations- und Request-Formungsoberfläche, die existiert und durch die Store-Definition fließt, aber es gibt noch keinen serverseitigen Codepfad, der einen Lesevorgang tatsächlich von einem Replikat bedient. In der Praxis heißt das: Sie bekommen derzeit das starke Ende des Reglers — jeder Lesevorgang ist ein Owner-Lesevorgang, also gilt Read-your-writes — und Sie bekommen nicht das Verhalten veralteter Replikat-Lesevorgänge, selbst wenn Sie das Flag umlegen.

Warum einen Regler erwähnen, der faktisch auf eine Einstellung festgenagelt ist? Weil er zeigt, was der Konsistenzvertrag Sie künftig tauschen lassen wird, und weil die Asymmetrie wichtig ist, wenn Sie über Veralterung nachdenken. Sobald Replikat-bedienende Lesevorgänge landen, ist das Einschalten von AllowReplicaReads der Moment, in dem Read-your-writes nicht mehr garantiert ist: Ein zu einem Replikat gerouteter Lesevorgang kann eine Version hinter der des Owners beobachten, solange der asynchrone Replikationsschwanz zurückliegt. Bis dahin ist die Garantie einfacher und stärker, als der Regler suggeriert. Wir möchten, dass Sie das wissen, statt anzunehmen, Sie verteilten Lesevorgänge bereits über Replikate, obwohl das nicht der Fall ist.

Was „bestätigt" für den nächsten Leser bedeutet​

Read-your-writes handelt davon, dass Sie Ihren eigenen Schreibvorgang auf dem Owner lesen. Eine andere, schärfere Frage lautet: Sobald der Owner Ihnen einen Schreibvorgang bestätigt, ist dieser sicher, wenn der Owner danach stirbt, sodass der Leser, der zum neuen Owner umgeroutet wird, ihn noch sieht?

Der ehrliche Bestätigungsvertrag heute: Ein Schreibvorgang gilt als bestätigt, wenn er im lokalen Store des aktiven Owners gelandet ist. Die Replikation auf die RF−1 Replikate läuft als asynchroner Schwanz hinter dieser Bestätigung. Die Konsequenzen sollte man einplanen, statt sie wegzuwinken:

  • Bei Redundanz 1 — kein synchronisiertes Replikat — ist ein Schreibvorgang wirklich Best-Effort. Er darf nicht als Single-Fault-dauerhaft betrachtet werden, denn es gibt genau eine Kopie.
  • Selbst mit Replikaten kann ein bestätigter Schreibvorgang verloren gehen, wenn der einzige aktive Owner im schmalen Fenster nach der Bestätigung, aber vor dem Eintreffen des Schreibvorgangs bei einem Replikat verloren geht.

Die stärkere Eigenschaft — erst bestätigen, wenn der Schreibvorgang auf mindestens einem synchronisierten Replikat liegt, sodass ein bestätigter Schreibvorgang den Verlust eines beliebigen Nodes überlebt — ist das Designziel (das „W1"-Ack-Gate), noch nicht die ausgelieferte Bestätigungssemantik. Es ist die Richtung, in die die Arbeit am Durability-Fundament konsolidiert, und dieser Beitrag ist bei derselben Unterscheidung sorgfältig: Die starke Aussage, die er validiert, ist auf bestätigte Schreibvorgänge, wenn ein synchronisiertes Replikat existiert, unter den vom Chaos-Lauf ausgelösten Fehlerklassen beschränkt — kein bedingungsloses „nichts kann je verloren gehen". Die Op-Log- und Migrationsmechanik ist, wie ein Replikat oder ein neuer Owner überhaupt zum committeten Zustand des Owners aufschließt; beim Ack-Gate geht es darum, wann der Owner „erledigt" sagen darf.

Die präzise Aussage für Leser lautet also: Read-your-writes auf dem Owner ist solide; die Fault-übergreifende Dauerhaftigkeit eines bestätigten Schreibvorgangs ist stark und wird stärker, mit dem oben genannten aktuellen Vorbehalt und einer harten Untergrenze bei RF1, die kein Store wegkonstruieren kann — eine Kopie ist eine Kopie.

Über Partitionen hinweg gibt es keine gemeinsame Uhr​

Die letzte Stelle, an der die Einschränkung „innerhalb einer Partition" zubeißt: Es gibt keine globale Ordnung und keinen globalen Snapshot über Partitionen hinweg. Jede Partition wird durch ihren eigenen Owner serialisiert; zwei verschiedene Partitionen sind zwei verschiedene Owner mit zwei unabhängigen Revisionsfolgen. Schreiben Sie Schlüssel A (Partition 1) und dann Schlüssel B (Partition 2), kann ein gleichzeitiger Leser den neuen Wert von B und den alten von A beobachten oder umgekehrt — es gibt keinen clusterweiten Zeitpunkt, an dem „beides geschehen" ist. Bulk- und Batch-Operationen sind darunter pro Schlüssel; sie sind bequem, nicht atomar.

Wenn eine Invariante wirklich Schlüssel überspannen muss — einen Saldo von A nach B verschieben, einen Index im Gleichschritt mit dem Datensatz halten, auf den er zeigt —, sind genau dafür Multi-Key-Transaktionen da. Sie bieten Serializable-Isolation über einen vom Client koordinierten Commit über optimistische Versionen und sind die einzige Oberfläche für schlüsselübergreifende Atomarität. Gewöhnliche Multi-Key-Lese- und Schreibvorgänge sind keine Transaktion und sollten nicht dafür gehalten werden.

Was garantiert ist und was nicht​

Klar zusammengefasst, damit Sie das ganze Modell auf einmal im Kopf behalten:

EigenschaftZaris heute
Owner pro PartitionGenau ein aktiver Owner, gehalten als dauerhafte Lease.
LesepfadRoutet zum aktiven Owner; Replikate werden standardmäßig nicht gelesen.
Read-your-writesJa, innerhalb einer Partition — derselbe Owner bedient Ihren Schreib- und Ihren Lesevorgang.
Monotone Lesevorgänge eines SchlüsselsJa — der Owner liefert in Commit-Reihenfolge vergebene Revisionen; Versionen bewegen sich vorwärts.
Lesevorgänge während der ÜbergabeWiederholbarer Status (Moved / OwnershipChanging / Migrating / ReplicaSyncPending), Client wiederholt und routet um.
Replikat-Lesevorgänge / Veralterungs-ReglerAllowReplicaReads + ReadConsistency.Any existieren als Oberfläche; heute praktisch Owner-only (noch kein Replikat-Bedienpfad).
Dauerhaftigkeit der BestätigungHeute Owner-lokal; Single-Fault-dauerhaft mit synchronisiertem Replikat ist das Ziel (W1-Ack-Gate).
Partitionsübergreifende OrdnungKeine — kein globaler Snapshot; nutzen Sie Transaktionen für schlüsselübergreifende Atomarität.
Globale LinearisierbarkeitNicht beansprucht.

Die ehrliche Form des Ganzen​

Ein gutes Konsistenzmodell ist nicht das mit der längsten Liste an Garantien, sondern das, dessen Garantien Sie tatsächlich benennen und dessen Grenzen Sie tatsächlich sehen können. Zaris zieht seine Grenze um die Ownership und ist offen über den Handel: Sie erhalten Read-your-writes und monotone Lesevorgänge innerhalb einer Partition gratis, weil ein Owner beide Seiten bedient und Ihnen in Commit-Reihenfolge vergebene Versionen gibt; Sie erhalten während einer Übergabe ein wiederholbares „noch nicht" statt einer selbstsicheren falschen Antwort; und Sie verzichten auf globale Linearisierbarkeit und partitionsübergreifende Ordnung, die Sie sich explizit mit einer Transaktion zurückkaufen, wenn eine bestimmte Invariante sie braucht.

Die beiden Vorbehalte, die Sie im Kopf behalten sollten, sind die, die dieser Beitrag benannt statt geglättet hat: Der Replikat-Lese-Regler ist heute faktisch auf die starke Einstellung festgenagelt, Sie sind also konsistenter, als der Regler vermuten lässt; und „bestätigt" bedeutet derzeit auf dem Owner, wobei volle Single-Fault-Dauerhaftigkeit ein Ziel ist, das die Durability-Arbeit gerade umsetzt, und es bei Redundanz 1 eine echte Untergrenze gibt. Wissen Sie diese zwei Dinge, ist der Rest des Modells genau so einfach, wie er aussieht: den Owner finden, die eigenen Schreibvorgänge lesen.

Für das tiefere Design siehe Konsistenz und Partitionierung und Replikation in der Dokumentation.