Aufbau eines resilienten verteilten Systems mit Go und Raft-Konsens
Emily Parker
Product Engineer · Leapcell

Einleitung: Die Notwendigkeit des verteilten Konsenses
In der heutigen vernetzten Welt ist der Aufbau von Anwendungen, die horizontal skalieren, Ausfälle überstehen und hochverfügbar bleiben, nicht nur ein Luxus, sondern eine grundlegende Notwendigkeit. Zentralisierte Systeme, obwohl einfacher zu verwalten, stellen einen einzelnen Ausfallpunkt dar und werden oft zu Leistungsengpässen, wenn die Benutzeranforderungen wachsen. Verteilte Systeme hingegen verteilen Berechnungen und Daten über mehrere Maschinen und bieten so verbesserte Ausfallsicherheit und Skalierbarkeit. Die Verwaltung des Zustands und die Gewährleistung der Datenkonsistenz über diese unabhängigen Knoten hinweg führen jedoch zu erheblicher Komplexität. Hier kommen verteilte Konsensalgorithmen ins Spiel. Sie bieten einen Mechanismus für eine Gruppe von Maschinen, sich auf einen einzelnen Wert oder eine Sequenz von Operationen zu einigen, selbst bei Netzwerkpartitionen oder Maschinenausfällen. Unter diesen Algorithmen hat sich Raft als eine besonders beliebte und verständliche Wahl herauskristallisiert, die für ihre Klarheit und Sicherheitsgarantien bekannt ist. Dieser Artikel befasst sich damit, wie wir die Nebenläufigkeitsprimitiven und das robuste Ökosystem von Go nutzen können, um ein einfaches, aber leistungsstarkes verteiltes Konsenssystem mithilfe des Raft-Protokolls aufzubauen.
Verständnis von Raft und seiner Implementierung in Go
Bevor wir uns mit dem Code befassen, wollen wir ein klares Verständnis der Kernkonzepte schaffen, die dem Raft-Konsensalgorithmus zugrunde liegen, und wie wir diese in eine praktische Go-Implementierung übersetzen können.
Schlüsselkonzepte in Raft
Raft ist ein Konsensalgorithmus zur Verwaltung eines replizierten Logs. Er garantiert, dass das System Fortschritte machen und die Konsistenz aufrechterhalten kann, wenn eine Mehrheit der Server verfügbar ist. Raft erreicht dies durch mehrere Schlüsselrollen und Phasen:
- Leader, Follower und Candidate: Jeder Server in einem Raft-Cluster übernimmt eine dieser drei Rollen.
- Follower: Passiv. Sie reagieren auf Anfragen von Leadern und Kandidaten.
- Candidates: Server, die vom Follower zum Leader wechseln. Sie initiieren Wahlen, um der neue Leader zu werden.
- Leader: Der aktive Server. Er bearbeitet alle Client-Anfragen und repliziert Log-Einträge an Follower. Es gibt zu jeder Zeit immer nur einen Leader.
- Terms: Raft teilt die Zeit in Terms auf, die monoton steigende ganze Zahlen sind. Jeder Term beginnt mit einer Wahl.
- Log-Replikation: Der Leader empfängt Client-Befehle, hängt sie an sein lokales Log an und repliziert sie dann an Follower. Sobald ein Eintrag auf einer Mehrheit der Server repliziert ist, gilt er als commit und kann auf die State Machine angewendet werden.
- Heartbeats: Der Leader sendet periodisch AppendEntries RPCs (auch leere, als Heartbeats bekannte) an alle Follower, um seine Führung aufrechtzuerhalten und neue Wahlen zu verhindern.
- Election Timeout: Follower warten auf ein zufälliges Election Timeout. Wenn sie innerhalb dieses Timeouts keinen Heartbeat oder keine AppendEntries RPC vom Leader erhalten, wechseln sie zu einem Candidate und starten eine Wahl.
- RequestVote RPC: Kandidaten senden RequestVote RPCs an andere Server, um Stimmen zu sammeln. Ein Server stimmt für einen Kandidaten, wenn sein Log mindestens so aktuell ist wie das eigene.
- AppendEntries RPC: Der Leader verwendet diesen RPC, um Log-Einträge zu replizieren und Heartbeats zu senden.
Bausteine in Go
Go's eingebaute Nebenläufigkeitsfunktionen, wie Goroutinen und Channels, eignen sich hervorragend für die Implementierung von Raft. Wir werden diese nutzen, um gleichzeitige Operationen, die Kommunikation zwischen Knoten und Zustandsübergänge zu verwalten.
Lassen Sie uns die wesentlichen Komponenten unserer Go-Raft-Implementierung umreißen:
-
Serverzustand: Jeder Raft-Server muss seinen Zustand aufrechterhalten, einschließlich seines aktuellen Terms, votedFor (im aktuellen Term), Log-Einträgen und Wahl-/Heartbeat-Timern.
type LogEntry struct { Term int Command []byte } type RaftServer struct { mu sync.Mutex // Zum Schutz des gemeinsamen Zustands id int // Server-ID peers []string // Adressen anderer Server isLeader bool currentTerm int votedFor int // Peer-ID, für die dieser Server im aktuellen Term gestimmt hat log []LogEntry commitIndex int // Index des höchsten Log-Eintrags, der als commit bekannt ist lastApplied int // Index des höchsten Log-Eintrags, der auf die State Machine angewendet wurde nextIndex []int // Für jeden Peer, Index des nächsten zu sendenden Log-Eintrags matchIndex []int // Für jeden Peer, Index des höchsten Log-Eintrags, der bekanntermaßen auf dem Peer repliziert wurde // Channels für Kommunikation und Auslösung von Ereignissen electionTimeoutC chan time.Time heartbeatC chan time.Time applyC chan LogEntry // Channel zum Anwenden von commit-Einträgen auf die State Machine shutdownC chan struct{} }
-
RPCs und Kommunikation: Wir verwenden Go's
net/rpc
-Paket für die Kommunikation zwischen Servern. Dies erfordert die Definition von RPC-Methoden fürRequestVote
- undAppendEntries
-Nachrichten.// RequestVote RPC Argumente und Antwort type RequestVoteArgs struct { Term int // Aktueller Term des Kandidaten CandidateId int // ID des abstimmenden Kandidaten LastLogIndex int // Index des letzten Log-Eintrags des Kandidaten LastLogTerm int // Term des letzten Log-Eintrags des Kandidaten } type RequestVoteReply struct { Term int // Aktueller Term, damit der Kandidat sich aktualisieren kann VoteGranted bool // True, wenn der Kandidat die Stimme erhalten hat } // AppendEntries RPC Argumente und Antwort type AppendEntriesArgs struct { Term int // Aktueller Term des Leaders LeaderId int // Damit der Follower Clients umleiten kann PrevLogIndex int // Index des Log-Eintrags unmittelbar vor den neuen Einträgen PrevLogTerm int // Term des PrevLogIndex-Eintrags Entries []LogEntry // Zu speichernde Log-Einträge (leer für Heartbeats) LeaderCommit int // CommitIndex des Leaders } type AppendEntriesReply struct { Term int // Aktueller Term, damit der Leader sich aktualisieren kann Success bool // True, wenn der Follower einen Eintrag enthält, der mit PrevLogIndex und PrevLogTerm übereinstimmt }
-
State Machine Logic: Der Kern unseres Raft-Servers wird eine Goroutine sein, die kontinuierlich läuft und Ereignisse und Übergänge zwischen Rollen verarbeitet.
func (rs *RaftServer) Run() { // Anfangszustand ist Follower rs.becomeFollower() for { select { case <-rs.shutdownC: return // Server herunterfahren case <-rs.electionTimeoutC: rs.mu.Lock() // Wenn wir keinen Heartbeat erhalten haben, zum Kandidaten wechseln if rs.isFollower() && time.Since(lastHeartbeat) > rs.electionTimeout { rs.becomeCandidate() } rs.mu.Unlock() case <-rs.heartbeatC: rs.mu.Lock() if rs.isLeader { // Heartbeats an alle Follower senden rs.sendHeartbeats() } rs.mu.Unlock() // ... weitere Fälle für die Behandlung von RPCs und das Anwenden von commit-Einträgen ... } } }
-
RPC-Handler: Implementieren Sie die Logik für
RequestVote
- undAppendEntries
-RPC-Aufrufe. Diese Handler aktualisieren den Zustand des Servers basierend auf den eingehenden Anfragen und geben entsprechende Antworten zurück.func (rs *RaftServer) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) error { rs.mu.Lock() defer rs.mu.Unlock() reply.Term = rs.currentTerm reply.VoteGranted = false // Raft-Regeln für die Stimmabgabe... if args.Term < rs.currentTerm { return nil // Term des Kandidaten ist veraltet } if args.Term > rs.currentTerm { rs.becomeFollower() // Term aktualisieren und ggf. zurücktreten rs.currentTerm = args.Term rs.votedFor = -1 // Stimme zurücksetzen } if (rs.votedFor == -1 || rs.votedFor == args.CandidateId) && rs.isLogUpToDate(args.LastLogIndex, args.LastLogTerm) { reply.VoteGranted = true rs.votedFor = args.CandidateId // Election Timer hier zurücksetzen, da wir für einen gültigen Kandidaten stimmen rs.resetElectionTimeout() } return nil } func (rs *RaftServer) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) error { rs.mu.Lock() defer rs.mu.Unlock() reply.Term = rs.currentTerm reply.Success = false if args.Term < rs.currentTerm { return nil // Term des Leaders ist veraltet } // Wenn RPC von neuem Leader oder aktuellem Leader mit höherem Term, zurücktreten if args.Term > rs.currentTerm || rs.isCandidate() { rs.becomeFollower() rs.currentTerm = args.Term rs.votedFor = -1 } // Election Timer bei gültigem AppendEntries RPC immer zurücksetzen rs.resetElectionTimeout() // Log-Konsistenzprüfung und Log-Anhängelogik... // Dies ist eine vereinfachte Darstellung. Die tatsächliche Logik beinhaltet // Überprüfung von PrevLogIndex/PrevLogTerm und das Abschneiden/Anhängen von Logs. if len(rs.log) > args.PrevLogIndex && rs.log[args.PrevLogIndex].Term == args.PrevLogTerm { reply.Success = true // Neue Einträge anhängen und potenziell widersprüchliche Einträge abschneiden // commitIndex aktualisieren, wenn args.LeaderCommit > rs.commitIndex } return nil }
-
Client-Interaktion: Ein Client würde sich mit dem aktuellen Raft-Leader verbinden, um neue Befehle vorzuschlagen. Wenn sich ein Client mit einem Follower verbindet, sollte der Follower den Client zum Leader umleiten.
Anwendungsszenarien
Ein Raft-basiertes System eignet sich ideal für Szenarien, die starke Konsistenz und Fehlertoleranz für zustandsbehaftete Dienste erfordern. Gängige Anwendungen sind:
- Verteilte Key-Value Stores: Denken Sie an
etcd
oderZooKeeper
, die Varianten von Paxos oder Raft verwenden, um Metadaten und Konfigurationen von Clustern zu verwalten. - Verteilte Datenbanken: Gewährleistung der Konsistenz von Transaktionsprotokollen über Replikate hinweg.
- Verteilte Sperren: Bereitstellung eines zuverlässigen Mechanismus für den gegenseitigen Ausschluss beim Zugriff auf gemeinsam genutzte Ressourcen.
- Leader-Wahl für Hochverfügbarkeit: Wahl der primären Dienstinstanz in einem Cluster.
Fazit: Zuverlässigkeit durch Konsens
Der Aufbau eines verteilten Konsenssystems mit Go und Raft, selbst eines vereinfachten, demonstriert die Leistungsfähigkeit der Kombination einer robusten Programmiersprache mit einem klar definierten Algorithmus. Rafts klares Design und Go's Nebenläufigkeitsmodell machen sie zu einer ausgezeichneten Paarung für die Erstellung fehlertoleranter und hochverfügbarer Infrastrukturkomponenten. Während unser Beispiel nur an der Oberfläche kratzt, hebt es die grundlegenden Prinzipien der Leader-Wahl, der Log-Replikation und der Sicherheitsgarantien hervor, die allen Raft-Implementierungen zugrunde liegen. Die Beherrschung dieser Konzepte ist entscheidend für alle, die resiliente verteilte Systeme aufbauen möchten, die angesichts unvermeidlicher Ausfälle gedeihen können. Go und Raft bieten gemeinsam einen überzeugenden Weg zur Erreichung robuster verteilter Systemzuverlässigkeit.