Heim > Artikel > Backend-Entwicklung > Die Raft-Implementierung von etcd verstehen: Ein tiefer Einblick in Raft Log
In diesem Artikel werden das Design und die Implementierung des Raft-Log-Moduls in etcds Raft vorgestellt und analysiert, beginnend mit dem Protokoll im Raft-Konsensalgorithmus. Ziel ist es, den Lesern zu helfen, die Implementierung von etcds Raft besser zu verstehen und einen möglichen Ansatz für die Implementierung ähnlicher Szenarien bereitzustellen.
Der Raft-Konsensalgorithmus ist im Wesentlichen eine replizierte Zustandsmaschine mit dem Ziel, eine Reihe von Protokollen auf die gleiche Weise in einem Servercluster zu replizieren. Diese Protokolle ermöglichen es den Servern im Cluster, einen konsistenten Zustand zu erreichen.
In diesem Zusammenhang beziehen sich die Protokolle auf das Floßprotokoll. Jeder Knoten im Cluster verfügt über ein eigenes Raft-Protokoll, das aus einer Reihe von Protokolleinträgen besteht. Ein Protokolleintrag enthält normalerweise drei Felder:
Es ist wichtig zu beachten, dass der Index des Raft-Protokolls bei 1 beginnt und nur der Führungsknoten das Raft-Protokoll erstellen und auf Folgeknoten replizieren kann.
Wenn ein Protokolleintrag dauerhaft auf den meisten Knoten im Cluster gespeichert ist (z. B. 2/3, 3/5, 4/7), gilt er als festgeschrieben.
Wenn ein Protokolleintrag auf die Zustandsmaschine angewendet wird, gilt er als angewendet.
etcd Raft ist eine in Go geschriebene Raft-Algorithmusbibliothek, die häufig in Systemen wie etcd, Kubernetes, CockroachDB und anderen verwendet wird.
Das Hauptmerkmal von etcd Raft ist, dass es nur den Kernteil des Raft-Algorithmus implementiert. Benutzer müssen Netzwerkübertragung, Festplattenspeicher und andere am Raft-Prozess beteiligte Komponenten selbst implementieren (obwohl etcd Standardimplementierungen bereitstellt).
Die Interaktion mit der etcd-Raft-Bibliothek ist einigermaßen unkompliziert: Sie sagt Ihnen, welche Daten beibehalten werden müssen und welche Nachrichten an andere Knoten gesendet werden müssen. Es liegt in Ihrer Verantwortung, die Speicher- und Netzwerkübertragungsvorgänge abzuwickeln und diese entsprechend zu informieren. Es geht nicht um die Details, wie Sie diese Vorgänge implementieren; Es verarbeitet einfach die von Ihnen übermittelten Daten und teilt Ihnen basierend auf dem Raft-Algorithmus die nächsten Schritte mit.
Bei der Implementierung des etcd-Raft-Codes wird dieses Interaktionsmodell nahtlos mit der einzigartigen Kanalfunktion von Go kombiniert, was die etcd-Raft-Bibliothek wirklich unverwechselbar macht.
In etcd Raft befindet sich die Hauptimplementierung von Raft Log in den Dateien log.go und log_unstable.go, wobei die Primärstrukturen RaftLog und Unstable sind. Die instabile Struktur ist auch ein Feld innerhalb von RaftLog.
etcd Raft verwaltet die Protokolle innerhalb des Algorithmus, indem es RaftLog und Unstable koordiniert.
Um die Diskussion zu vereinfachen, konzentriert sich dieser Artikel nur auf die Verarbeitungslogik von Protokolleinträgen, ohne auf die Snapshot-Verarbeitung in etcd Raft einzugehen.
type raftLog struct { storage Storage unstable unstable committed uint64 applying uint64 applied uint64 }
Kernbereiche von RaftLog:
type unstable struct { entries []pb.Entry offset uint64 offsetInProgress uint64 }
Kernfelder von instabil:
Die Kernfelder in RaftLog sind unkompliziert und können leicht mit der Implementierung im Raft-Papier in Verbindung gebracht werden. Allerdings könnten die Felder in Unstable abstrakter erscheinen. Das folgende Beispiel soll zur Verdeutlichung dieser Konzepte beitragen.
Angenommen, wir haben bereits 5 Protokolleinträge in unserem Raft-Protokoll gespeichert. Jetzt haben wir 3 Protokolleinträge in Unstable gespeichert und diese 3 Protokolleinträge werden derzeit beibehalten. Die Situation ist wie folgt:
offset=6 gibt an, dass die Protokolleinträge an den Positionen 0, 1 und 2 in unstable.entries den Positionen 6 (0 6), 7 (1 6) und 8 (2 6) im tatsächlichen Raft Log entsprechen. Mit offsetInProgress=9 wissen wir, dass unstable.entries[:9-6], das die drei Protokolleinträge an den Positionen 0, 1 und 2 enthält, alle beibehalten werden.
Der Grund dafür, dass offset und offsetInProgress in Unstable verwendet werden, besteht darin, dass Unstable nicht alle Raft-Log-Einträge speichert.
Da wir uns nur auf die Raft-Log-Verarbeitungslogik konzentrieren, bezieht sich „wann interagiert“ hier darauf, wann etcd Raft die Protokolleinträge übergibt, die vom Benutzer beibehalten werden müssen.
etcd Raft interagiert mit dem Benutzer hauptsächlich über Methoden in der Node-Schnittstelle. Die Ready-Methode gibt einen Kanal zurück, der es dem Benutzer ermöglicht, Daten oder Anweisungen von etcd Raft zu empfangen.
type raftLog struct { storage Storage unstable unstable committed uint64 applying uint64 applied uint64 }
Die von diesem Kanal empfangene Ready-Struktur enthält Protokolleinträge, die verarbeitet werden müssen, Nachrichten, die an andere Knoten gesendet werden sollen, den aktuellen Status des Knotens und mehr.
Für unsere Diskussion über Raft Log müssen wir uns nur auf die Felder Entries und CommittedEntries konzentrieren:
type unstable struct { entries []pb.Entry offset uint64 offsetInProgress uint64 }
Nach der Verarbeitung der Protokolle, Nachrichten und anderen über Ready weitergeleiteten Daten können wir die Advance-Methode in der Node-Schnittstelle aufrufen, um etcd Raft darüber zu informieren, dass wir seine Anweisungen abgeschlossen haben, sodass es das nächste Ready empfangen und verarbeiten kann.
etcd Raft bietet eine AsyncStorageWrites-Option, die die Knotenleistung bis zu einem gewissen Grad verbessern kann. Allerdings ziehen wir diese Option hier nicht in Betracht.
Auf der Benutzerseite liegt der Fokus auf der Verarbeitung der Daten in der empfangenen Ready-Struktur. Auf der etcd-Raft-Seite liegt der Schwerpunkt darauf, zu bestimmen, wann eine Ready-Struktur an den Benutzer übergeben werden soll und welche Aktionen danach zu ergreifen sind.
Ich habe die wichtigsten an diesem Prozess beteiligten Methoden im folgenden Diagramm zusammengefasst, das die allgemeine Reihenfolge der Methodenaufrufe zeigt (beachten Sie, dass dies nur die ungefähre Reihenfolge der Aufrufe darstellt):
Sie können sehen, dass der gesamte Prozess eine Schleife ist. Hier skizzieren wir die allgemeine Funktion dieser Methoden und befassen uns in der anschließenden Schreibflussanalyse mit der Funktionsweise dieser Methoden in den Kernfeldern „raftLog“ und „unstable“.
Hier sind zwei wichtige Punkte zu beachten:
1. Beharrlich ≠ engagiert
Wie ursprünglich definiert, gilt ein Protokolleintrag nur dann als festgeschrieben, wenn er von der Mehrheit der Knoten im Raft-Cluster beibehalten wurde. Selbst wenn wir also die von etcd Raft zurückgegebenen Einträge über „Ready“ beibehalten, können diese Einträge noch nicht als festgeschrieben markiert werden.
Wenn wir jedoch die Advance-Methode aufrufen, um etcd Raft darüber zu informieren, dass wir den Persistenzschritt abgeschlossen haben, wertet etcd Raft den Persistenzstatus über andere Knoten im Cluster aus und markiert einige Protokolleinträge als festgeschrieben. Diese Einträge werden uns dann über das CommittedEntries-Feld der Ready-Struktur bereitgestellt, damit wir sie auf die Zustandsmaschine anwenden können.
Bei Verwendung von etcd Raft wird daher der Zeitpunkt für die Markierung von Einträgen als festgeschrieben intern verwaltet und Benutzer müssen nur die Persistenzvoraussetzungen erfüllen.
Intern wird die Verpflichtung durch Aufrufen der Methode „raftLog.commitTo“ erreicht, die „raftLog.committed“ entsprechend dem commitIndex im Raft-Papier aktualisiert.
2. Engagiert ≠ Angewendet
Nachdem die Methode „raftLog.commitTo“ in etcd „raft“ aufgerufen wurde, gelten die Protokolleinträge bis zum Index „raft.committed“ als festgeschrieben. Einträge mit Indizes im Bereich lastApplied < index <= commitedIndex wurden noch nicht auf die Zustandsmaschine angewendet. etcd Raft gibt diese festgeschriebenen, aber nicht angewendeten Einträge im CommittedEntries-Feld von Ready zurück, sodass wir sie auf die Zustandsmaschine anwenden können. Sobald wir Advance anrufen, markiert etcd Raft diese Einträge als angewendet.
Der Zeitpunkt zum Markieren von Einträgen als angewendet wird auch intern in etcd Raft verwaltet; Benutzer müssen nur die festgeschriebenen Einträge von „Bereit“ auf die Zustandsmaschine anwenden.
Ein weiterer subtiler Punkt ist, dass in Raft nur der Leader Einträge festschreiben kann, alle Knoten sie jedoch anwenden können.
Hier verbinden wir alle zuvor besprochenen Konzepte, indem wir den Fluss von etcd Raft analysieren, während es eine Schreibanfrage verarbeitet.
Um ein allgemeineres Szenario zu besprechen, beginnen wir mit einem Raft-Protokoll, das bereits drei Protokolleinträge festgeschrieben und angewendet hat.
In der Abbildung steht grün für RaftLog-Felder und die im Speicher gespeicherten Protokolleinträge, während rot für instabile Felder und die in Einträgen gespeicherten nicht persistenten Protokolleinträge steht.
Da wir drei Protokolleinträge festgeschrieben und angewendet haben, sind sowohl „festgeschrieben“ als auch „angewandt“ auf 3 gesetzt. Das Anwendungsfeld enthält den Index des höchsten Protokolleintrags aus der vorherigen Anwendung, der in diesem Fall ebenfalls 3 ist.
Zu diesem Zeitpunkt wurden keine Anfragen initiiert, daher ist unstable.entries leer. Der nächste Protokollindex im Raft-Protokoll ist 4, wodurch Offset 4 entsteht. Da derzeit keine Protokolle beibehalten werden, ist offsetInProgress ebenfalls auf 4 gesetzt.
Jetzt initiieren wir eine Anfrage zum Anhängen von zwei Protokolleinträgen an das Raft-Protokoll.
Wie in der Abbildung gezeigt, werden die angehängten Protokolleinträge in unstable.entries gespeichert. Zu diesem Zeitpunkt werden keine Änderungen an den in den Kernfeldern erfassten Indexwerten vorgenommen.
Erinnern Sie sich an die HasReady-Methode? HasReady prüft, ob nicht persistente Protokolleinträge vorhanden sind, und gibt in diesem Fall „true“ zurück.
Die Logik zur Bestimmung des Vorhandenseins nicht persistenter Protokolleinträge basiert darauf, ob die Länge von unstable.entries[offsetInProgress-offset:] größer als 0 ist. In unserem Fall gilt eindeutig:
type raftLog struct { storage Storage unstable unstable committed uint64 applying uint64 applied uint64 }
zeigt an, dass es zwei nicht persistente Protokolleinträge gibt, sodass HasReady „true“ zurückgibt.
Der Zweck von readyWithoutAccept besteht darin, die Ready-Struktur zu erstellen, die an den Benutzer zurückgegeben werden soll. Da wir zwei nicht persistente Protokolleinträge haben, fügt readyWithoutAccept diese beiden Protokolleinträge in das Feld „Einträge“ des zurückgegebenen „Ready“ ein.
acceptReady wird aufgerufen, nachdem die Ready-Struktur an den Benutzer übergeben wurde.
acceptReady aktualisiert den Index der Protokolleinträge, die gerade beibehalten werden, auf 6, was bedeutet, dass Protokolleinträge im Bereich [4, 6) jetzt als dauerhaft gespeichert werden.
Nachdem der Benutzer die Einträge im Status „Bereit“ gespeichert hat, ruft er Node.Advance auf, um etcd Raft zu benachrichtigen. Dann kann etcd Raft den in AcceptReady erstellten „Callback“ ausführen.
Dieser „Rückruf“ löscht die bereits persistenten Protokolleinträge in unstable.entries und setzt dann den Offset auf Storage.LastIndex 1, also 6.
Wir gehen davon aus, dass diese beiden Protokolleinträge bereits von der Mehrheit der Knoten im Raft-Cluster gespeichert wurden, sodass wir diese beiden Protokolleinträge als festgeschrieben markieren können.
Im weiteren Verlauf unserer Schleife erkennt HasReady das Vorhandensein von Protokolleinträgen, die festgeschrieben, aber noch nicht angewendet wurden, und gibt daher „true“ zurück.
readyWithoutAccept gibt ein Bereit zurück, das Protokolleinträge (4, 5) enthält, die festgeschrieben, aber nicht auf die Zustandsmaschine angewendet wurden.
Diese Einträge werden als niedrig, hoch := berechnet, indem 1 angewendet, 1 festgeschrieben wird, in einem links-offenen, rechts-geschlossenen Intervall.
acceptReady markiert dann die in „Ready“ zurückgegebenen Protokolleinträge [4, 5] als auf die Zustandsmaschine angewendet.
Nachdem der Benutzer Node.Advance aufgerufen hat, führt etcd Raft den „Callback“ aus und aktualisiert Aktualisierungen auf 5, was anzeigt, dass die Protokolleinträge bei Index 5 und früher alle auf die Zustandsmaschine angewendet wurden.
Damit ist der Verarbeitungsablauf für eine Schreibanforderung abgeschlossen. Der Endzustand ist wie unten dargestellt und kann mit dem Anfangszustand verglichen werden.
Wir begannen mit einem Überblick über das Raft Log, um ein Verständnis seiner Grundkonzepte zu erlangen, gefolgt von einem ersten Blick auf die etcd-Raft-Implementierung. Anschließend haben wir uns eingehender mit den Kernmodulen von Raft Log innerhalb von etcd Raft befasst und wichtige Fragen berücksichtigt. Schließlich haben wir alles durch eine vollständige Analyse des Ablaufs einer Schreibanfrage zusammengefügt.
Ich hoffe, dieser Ansatz hilft Ihnen dabei, ein klares Verständnis der etcd-Raft-Implementierung zu erlangen und Ihre eigenen Erkenntnisse zum Raft-Log zu entwickeln.
Damit ist dieser Artikel abgeschlossen. Wenn es Fehler oder Fragen gibt, können Sie uns gerne per Privatnachricht kontaktieren oder einen Kommentar hinterlassen.
Übrigens ist Raft-Foiver eine vereinfachte Version von etcd Raft, die ich implementiert habe, wobei die gesamte Kernlogik von Raft beibehalten und entsprechend dem Prozess im Raft-Papier optimiert wurde. Ich werde in Zukunft einen separaten Beitrag veröffentlichen, in dem ich diese Bibliothek vorstelle. Bei Interesse melden Sie sich gerne bei Star, Fork oder PR!
Das obige ist der detaillierte Inhalt vonDie Raft-Implementierung von etcd verstehen: Ein tiefer Einblick in Raft Log. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!