Zum Hauptinhalt springen

Wie ein Zaris-Cluster hochfährt: Mitgliedschaft, die Map und durch Konvergenz gesteuerte Readiness

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

Wie ein Zaris-Cluster hochfährt: Mitgliedschaft, die Map und durch Konvergenz gesteuerte Readiness

Die meisten Texte über verteilte Stores handeln vom Normalbetrieb — Lesen, Schreiben, Replikation — oder von den dramatischen Ausfällen: einem Failover, wenn ein Owner stirbt, oder einem Cut-over, wenn eine Partition umzieht. Doch es gibt einen leiseren Moment, der darüber entscheidet, ob all das funktioniert: der Moment, in dem der Cluster hochfährt. Bevor auch nur ein Key geroutet werden kann, müssen sich die Nodes einigen, wer zum Cluster gehört, wem welcher Ausschnitt des Keyspace gehört und — der Schritt, den die meisten Systeme falsch machen — ob es überhaupt sicher ist, mit dem Bedienen zu beginnen.

Dieser Beitrag folgt dieser Boot-Sequenz in der Reihenfolge, in der Zaris sie tatsächlich ausführt: Mitgliedschaft, Partition-Map und die Readiness-Gates, die zwischen „der Prozess läuft" und „der Node nimmt Traffic an" stehen. Außerdem ziehen wir ehrlich eine Grenze zwischen dem, was Zaris selbst implementiert, und dem, was es von der zugrunde liegenden Cluster-Laufzeit erbt, auf der es aufbaut.

Wer gehört zum Cluster: Mitgliedschaft​

Das Erste, was ehrlicherweise gesagt werden muss, ist, wo die Grenze verläuft. Der Low-Level-Mitgliedschaftstransport — Peer-Heartbeats, Fehlererkennung und Leader-Election — wird von der Schicht Clustron.Nodus bereitgestellt, auf der Zaris aufbaut. Zaris nutzt sie über ein INodusNode-Interface: Es fragt Nodus, wer die Mitglieder sind und wer die Leadership hält, und wird benachrichtigt, wenn der Heartbeat eines Peers ausbleibt oder fehlschlägt. Zaris implementiert Raft oder ein Gossip-basiertes Discovery-Protokoll nicht neu; es setzt seine eigene Partition- und Ownership-Steuerungsebene auf diesem Fundament auf.

Was Zaris hinzufügt, ist eine explizite Mitgliedschafts-Zustandsmaschine, die rohe Heartbeat-Ereignisse in Ownership-relevante Entscheidungen übersetzt. Sie lebt in MembershipStateTracker, und ihre Zustände sind bewusst differenzierter als „oben / unten":

Die Zustände fallen in zwei Gruppen, und genau diese Trennung ist der Kern. Alive, Suspected und Unreachable betreffen nur das Laufzeit-Ownership — sie können eine Replica-Promotion auslösen oder eine Partition vorübergehend als nicht verfügbar markieren, aber sie verändern niemals die Partition-Map. Nur Lost, Evicted und Decommissioned machen einen Node für eine Neuberechnung der Map zugänglich.

Diese Unterscheidung existiert, weil das Neuberechnen der Partition-Map teuer und, wenn zu voreilig, gefährlich ist — es kann gesunde Partitionen unnötig umverteilen. Wenn also der Heartbeat eines Nodes ausfällt, schreibt Zaris die Map nicht sofort neu. Es setzt den Node auf Unreachable und startet eine Karenzzeit (standardmäßig 30 Sekunden). Während dieses Fensters führt der Cluster nur ein Laufzeit-Failover durch — er befördert eine Replica, damit Lese- und Schreibzugriffe weiterfließen — und gibt dem Node die Chance zurückzukehren. Ein Flattern, eine GC-Pause, ein kurzer Netzwerkaussetzer: Nichts davon soll das Ownership neu schreiben. Erst wenn die Karenzzeit abläuft, wird der Node zu Lost, und erst wenn eine ganze Partitionsgruppe entfernt wurde, wird der einzige fehlergetriebene Pfad zur Map-Neuberechnung ausgelöst. Die Designregel, im Code, der sie verdrahtet, klar formuliert, lautet: Ein ausscheidender Node berechnet die Map niemals von sich aus neu.

Wie sich Nodes finden​

Das Discovery ist Roster-basiert, nicht Gossip-Discovery. Die maßgebliche Liste der Nodes ist Konfiguration, nichts, was der Cluster zur Laufzeit aufspürt. In einer handgeschriebenen Konfiguration ist es das nodes-Array — jeder Eintrag deklariert eine ID, einen Host, einen Cluster-Port (Peer) und einen Client-Port. Unter Kubernetes wird diese Roster stattdessen aus den StatefulSet-Pod-Ordinalzahlen synthetisiert, statt von Hand gepflegt zu werden; die Preferred-Primary-Rolle eines Nodes wird direkt aus seiner Ordinalzahl abgeleitet.

Es gibt einen Gossip-Mechanismus in Zaris, aber er erfüllt eine andere Aufgabe — er hält die Sicht der Nodes auf den Cluster-Zustand konvergent, er dient nicht dazu, die Existenz von Peers zu entdecken. Darauf kommen wir bei der Readiness zurück.

Wem gehört was: die Partition-Map​

Sobald die Mitgliedschaft bekannt ist, braucht der Cluster einen Plan für den Keyspace. Dieser Plan ist die Partition-Map — ein einziges, versioniertes Dokument, nach dem jeder Node routet.

Von einem Key zu einem Owner, in zwei Schritten​

Ein Key wird nicht direkt einer Partition zugeordnet. Zaris verwendet eine zweistufige Zuordnung, die bewusst Byte für Byte an Redis Cluster ausgerichtet ist, sodass dieselbe Routing-Mathematik für native Clients und das RESP-Front-End funktioniert:

key --CRC16--> slot (0..16383) --proportional--> segment (0..255) --SegmentToPartition[]--> partition

Der erste Schritt hasht den Key mit CRC16 (CCITT/XMODEM, Polynom 0x1021) modulo 16384 — exakt die Funktion, die Redis Cluster verwendet, einschließlich der Berücksichtigung der Hash-Tag-Co-Location: Ein Key wie user:{42}:profile hasht nur den Teil innerhalb der geschweiften Klammern, sodass zusammengehörige Keys beieinander landen. Die 16384 Slots werden dann proportional auf die Segmente des Clusters abgebildet — standardmäßig 256, für die Lebensdauer des Clusters fest — und eine SegmentToPartition-Tabelle weist jedes Segment einer Partition zu. Die Partition schließlich ist das, was Owner hat.

Warum die zusätzliche Segmentschicht, statt direkt auf Partitionen zu hashen? Weil Segmente eine stabile, feingranulare Einheit sind, die Sie zwischen Partitionen verschieben können, wenn der Cluster wächst oder neu balanciert wird, ohne jeden Key neu zu hashen. Der Schritt von CRC16 zum Slot ändert sich nie; nur die Zuordnung von Segment zu Partition ändert sich.

Was eine Partition-Map enthält​

Die Map selbst ist eine PartitionMap — ein MessagePack-serialisierter Record, dessen Struktur zeigt, wie sorgfältig das Ownership nachverfolgt wird. Die wissenswerten Felder:

  • Version — ein monotoner Zähler, der bei jeder Map-Änderung hochzählt. Das ist die Zahl, die den Commit sicher macht (mehr dazu unten).
  • TopologyVersion — zählt nur bei einer echten strukturellen Änderung hoch (ein Node oder eine Partition kommt hinzu oder geht, ein Re-Tile), nicht bei einem routinemäßigen Migrations-Cut-over. Die Trennung von „die Form hat sich geändert" und „ein Owner hat sich geändert" erlaubt dem Rest des Systems, verhältnismäßig zu reagieren.
  • TotalSegments — standardmäßig 256, für die Lebensdauer des Clusters konstant.
  • Partitions — die Liste der Zuweisungen, jeweils mit einer PrimaryNodeId (der Platzierungs-Primary), ReplicaNodeIds und einer stabilen PartitionIdentity (einer bei der Entstehung erzeugten GUID, die eine Umnummerierung überdauert) sowie Ownership-Lease-Feldern: ActiveOwnerNodeId, einer vom Leader erzeugten OwnershipGeneration und dem LeaderTerm, der die Lease erteilt hat — wodurch der Cluster einen veralteten Ex-Leader daran hindern kann, Ownership zu verfassen.

Beachten Sie die bewusste Trennung zwischen PrimaryNodeId (wo die Map eine Partition bevorzugt ansiedelt) und ActiveOwnerNodeId (wer sie tatsächlich gerade bedient). Beide können sich während eines Failovers oder einer Migration unterscheiden, und die Trennung in separate Felder macht diese Übergänge darstellbar, ohne den Plan für den Normalbetrieb zu verfälschen. Der Beitrag zum Konsistenzmodell erklärt, warum Lesezugriffe zu diesem einen aktiven Owner geroutet werden.

Wie die Map geplant wird​

Die Map wird nicht gierig zusammengesetzt. Sie wird von einem reinen, deterministischen Planer erzeugt — DesiredMapPlanner —, der die Node-IDs der Mitglieder, die Segmentanzahl, eine gewünschte Partitionsanzahl und den Replikationsfaktor entgegennimmt und einen Plan zurückgibt. Zwei Eigenschaften sind erwähnenswert:

  1. Ausgewogene, zusammenhängende Segmentaufteilung. Segmente werden so gleichmäßig wie möglich in zusammenhängenden Abschnitten verteilt; die ersten Partitionen nehmen den Rest auf, wenn die Teilung nicht glatt aufgeht.
  2. Platzierung in disjunkten Gruppen. Die ersten N Mitglieder werden die verschiedenen Partition-Primaries; die übrigen Mitglieder füllen die Replica-Slots reihum, sodass kein Node in zwei verschiedenen Partitionsgruppen landet.

Der Grund, warum der Planer als reine Funktion der Eingaben geschrieben ist, ist ein hart erkämpfter: Dadurch ist eine ganze Klasse fehlerhafter Maps — eine Partition mit doppeltem Primary, ein nicht zusammenhängender Segmentbereich, eine degenerierte Zuweisung — per Konstruktion nicht darstellbar, statt dass man sie nachträglich mit einem Validator abfängt. Ehrlicherweise sollte auch die aktuelle Einschränkung genannt werden: Die Platzierung ist heute positionsbasiert und folgt disjunkten Gruppen; eine Churn-minimierende, Rendezvous-artige Platzierung, die bei jeder Neuberechnung möglichst wenige Daten verschiebt, ist eine ausdrückliche künftige Verfeinerung, nicht etwas, das ausgeliefert ist. Ausgeliefert ist ein Sticky-Placement-Schritt, der bei einer Neuberechnung durch Mitgliedschafts-Churn einen live bedienenden Owner an Ort und Stelle belässt, sodass eine gesunde Partition nicht ohne Grund verschoben wird.

Was „Commit" bedeutet​

Die Map wird von einem Node — dem Leader der Steuerungsebene — und niemandem sonst verfasst. Der Leader erzeugt eine Kandidaten-Map, schickt sie durch eine Projektion des Ownership-Logs (der Leader ist der einzige Schreiber des Ownership-Logs, und die bediente Map ist eine Faltung dieses Logs) und committet sie dann lokal, bevor er jemandem davon erzählt. Der Commit folgt einer einzigen, strikten Regel, die in TopologyManager.ApplyMap durchgesetzt wird:

// A map is accepted only if it is strictly newer.
if (_currentMap != null && map.Version <= _currentMap.Version)
return false;

Eine Map mit einer Version kleiner oder gleich der bereits gehaltenen wird abgelehnt, Punkt. Erst nachdem der Leader committet hat, sendet er ein MapChangedEvent, schickt direkte Map-Snapshots an Peers und veröffentlicht die Routing-Änderung an Clients. Follower holen die Map ab (beim Kaltstart fordern sie sie von bereits bekannten Peers an und versuchen es erneut, bis einer antwortet) und wenden sie über dieselbe monotone Prüfung an. Da jeder Node nur strikt höhere Versionen akzeptiert, können sich zwei Nodes nie auf widersprüchliche Maps derselben Version einigen — die Versionsnummer ist der Serialisierungspunkt.

Eine ehrliche Besonderheit beim Kaltstart: Ein führerloser Node, der allein hochfährt, kann eine einzelne provisorische Ein-Node-Map erzeugen, damit er nicht handlungsunfähig ist; sie ist als provisorisch markiert und wird später durch die maßgebliche Multi-Node-Map ersetzt, sobald sich der Cluster gebildet hat. Das ist ein Verfügbarkeitsmittel, nicht der Pfad im Normalbetrieb.

Ist es sicher zu bedienen: durch Konvergenz gesteuerte Readiness​

Hier ist der Schritt, der einen vertrauenswürdigen Store von einem unterscheidet, der lediglich startet. Dass ein Prozess läuft und sogar eine Map vorliegt, heißt nicht, dass ein Node Traffic annehmen sollte. Beginnt ein Node zu bedienen, bevor sein Laufzeit-Ownership zu der Map aufgeschlossen hat, die er gerade angewendet hat, kann er eine Anfrage an eine Partition routen, die er noch gar nicht hält. Zaris weigert sich, das zu tun, und zwar mit zwei unterschiedlichen Gates.

Gate eins: Stabilität der Cluster-Bildung​

Das erste Gate entscheidet, wann der Leader die erste echte Map erzeugen darf. ClusterReadinessGate wartet, bis die Mitgliedschaft stabil ist — mindestens eine Mindestanzahl von Nodes ist durchgehend für ein Stabilitätsfenster anwesend (standardmäßig 3 Sekunden) —, bevor eine Map committet wird. Das verhindert, dass der Leader eine Map gegen einen halb gebildeten Cluster verfasst, in dem Nodes noch einer nach dem anderen beitreten, was eine sofortige Neuberechnung eine Sekunde später bedeuten würde. Wird die Stabilität nicht innerhalb des Timeouts erreicht (standardmäßig 30 Sekunden), führt das Gate trotzdem einen erzwungenen Bootstrap durch: Verfügbarkeit schlägt endloses Warten. Der Cluster fährt lieber etwas zu früh hoch, als hängen zu bleiben.

Gate zwei: das Traffic-Gate pro Node​

Das zweite Gate entscheidet tatsächlich, ob ein Node seinen Client-Listener öffnet. ClusterTrafficReadinessGate.IsReady() liefert nur dann true, wenn alle der folgenden Bedingungen erfüllt sind:

  • eine committete Map existiert,
  • ihre Segment-zu-Partition-Tabelle ist vollständig befüllt (length == TotalSegments),
  • eine Laufzeit-Ownership-Map existiert,
  • runtime.MapVersion == map.Version — das Laufzeit-Ownership des Nodes stimmt mit der exakten Version der von ihm committeten Map überein,
  • und jede Partition hat einen Laufzeiteintrag.

Die vierte Bedingung ist der Kern. Ein Node meldet sich nicht als bereit zum Bedienen, bevor seine eigene committete Map und sein eigenes Laufzeit-Ownership dieselbe Version haben. Darauf zeigt das Titelbild oben. Es gibt einen 60-Sekunden-Backstop, damit ein Node, der nicht konvergieren kann, den Start nicht ewig blockiert — danach fährt er mit einer Warnung fort, statt zu hängen —, aber im Normalfall hält das Gate die Tür geschlossen, bis beide Ebenen übereinstimmen.

Konvergent bleiben: Digests und Detect-and-Heal​

Readiness ist eine Eigenschaft der Startphase; konvergent zu bleiben ist eine des Normalbetriebs, und hier zahlt sich Zaris' Gossip tatsächlich aus. Jeder Heartbeat trägt einen kleinen ClusterStateDigest — die Epoche des Nodes, wen er für den Leader hält, seine Map-Version, seine Node-Anzahl und die Ownership-Generationen pro Partition. Keine zusätzlichen RPCs; er reist auf Traffic mit, den der Cluster ohnehin sendet.

Ein DigestAccumulator sammelt die Digests der Peers und vergleicht sie alle paar Sekunden. Weicht ein Node in zwei aufeinanderfolgenden Auswertungszyklen von der Mehrheit ab, löst er ein Drift-Signal aus, und ein HealingOrchestrator reagiert darauf — der Leader sendet korrigierende Direktiven (mit einer Abklingzeit, damit er nicht flattert), und ein Node, der sich als einsamer Ausreißer findet, heilt sich selbst durch erneutes Abholen. Das ist Konvergenz durch Erkennen und Reparieren, nicht durch eine Quorum-Abstimmung vor dem Bedienen.

Diese Unterscheidung ist für die Ehrlichkeit wichtig, daher möchte ich präzise sein. In Produktion wird die Übereinstimmung der Map-Version im gesamten Cluster durch das Traffic-Gate pro Node plus diese Digest-getriebene Heilungsschleife aufrechterhalten — nicht durch eine globale Barriere, die jedes Bedienen blockiert, bis jeder Node über eine Version abgestimmt hat. Die stärkere Aussage — jeder Node-Snapshot stimmt in einem Leader, einer Leader-Epoche und einer Map-Version überein — ist das, was das 12-Stunden-Chaos-Zertifizierungs-Harness fortlaufend als Pass/Fail-Invariante prüft. Die Zertifizierung belegt, dass die Eigenschaft gilt; die Laufzeit erreicht sie durch Gating und Heilung statt durch eine blockierende Konsensrunde auf dem Serve-Pfad.

Replikationsfaktor und ein ehrlicher Default​

Ein letztes Stück der Cluster-Bildung, denn es verändert, was „bereit" für Ihre Dauerhaftigkeit bedeutet. Der Replikationsfaktor ist standardmäßig 1. Mit RF=1 hat jede Partition einen Primary und keine Replica — eine einzige Kopie. Der Planer platziert RF - 1 Replicas pro Partition, im Default also null. Ein Cluster mit RF=1 bildet sich, konvergiert und bedient einwandfrei, aber der Verlust eines Nodes ist ein Datenverlust für die Partitionen, die er besaß — by Design, nicht zufällig. Wenn Sie möchten, dass eine Partition einen Node-Ausfall überlebt, setzen Sie RF >= 2 und benötigen genug Nodes, um diese Replicas tatsächlich auf verschiedenen Maschinen zu platzieren — der Beitrag zur Dimensionierung der Dauerhaftigkeit unter Kubernetes rechnet genau durch, wie viele Pods das erfordert. Die Partitionsanzahl hat einen ähnlich ehrlichen Default: Ist sie nicht gesetzt, verwendet der Cluster eine Partition pro Node.

Die Sequenz im Ganzen​

Zusammengenommen fährt ein Zaris-Cluster so hoch:

  1. Die Mitgliedschaft wird aufgelöst aus der Konfigurations-Roster (oder den k8s-Ordinalzahlen), und Nodus wählt einen Leader der Steuerungsebene.
  2. Der Leader wartet hinter dem Formations-Gate auf stabile Mitgliedschaft, plant dann eine Map mit DesiredMapPlanner und committet sie lokal unter der strikten Prüfung auf monotone Versionen.
  3. Der Leader sendet die Map per Broadcast; Follower holen sie ab und wenden sie unter derselben Prüfung an, wobei sie nur strikt höhere Versionen akzeptieren.
  4. Jeder Node wartet hinter seinem Traffic-Gate, bis runtime.MapVersion == map.Version gilt und das Routing vollständig befüllt ist — erst dann öffnet er seinen Client-Listener.
  5. Im Normalbetrieb halten Heartbeat-gebundene Digests die Sicht jedes Nodes konvergent, wobei Drift-Erkennung und Leader-getriebene Heilung jede Lücke schließen.

Keiner dieser Schritte ist für sich genommen dramatisch. Zusammen machen sie den Unterschied zwischen einem Cluster, der läuft, und einem, der bereit ist — und in der Lücke zwischen diesen beiden Worten verliert ein Store stillschweigend Daten, wenn er die Frage nicht ernst nimmt.


Zaris ist der verteilte Key/Value-Store von Clustron. Wenn Sie die Steuerungsebene aus Sicht des Operators sehen möchten, erstellt, startet und inspiziert die PowerShell-Anleitung live einen Cluster; und Elastic Scaling behandelt, was mit der Map passiert, wenn Sie einem bereits bedienenden Cluster einen Server hinzufügen.