Konsens verteilter Datenbanken: 6 Kernmodule von Raft von der Theorie zur Implementierung
Verteilter Konsens ist das Fundament verteilter Datenbanken—TiDB, CockroachDB, etcd und Consul alle verlassen sich auf Konsensprotokolle, um Datenkonsistenz zu gewährleisten. Doch die Paxos-Veröffentlichung ist notorisch schwer zu verstehen, und ihre technische Implementierung ist noch höllenartiger; Split-Brain-Probleme lassen unzählige Teams nachts Brände löschen; Leader-Wahl-Instabilität verursacht zeitweilige Dienstunverfügbarkeit; Log-Replikationslatenz macht standortübergreifende Bereitstellungen praktisch nutzlos; und linearisierbare Konsistenzgarantien bereiten Entwicklern endlose Kopfschmerzen. Das Raft-Protokoll, das mit „Verständlichkeit" als Kernprinzip entworfen wurde, zerlegt das Konsensproblem in drei Teilprobleme—Leader-Wahl, Log-Replikation und Sicherheit—und ist damit das De-facto-Standard-Konsensprotokoll für verteilte Datenbanken im Jahr 2026.
Kernkonzepte auf einen Blick
| Konzept | Beschreibung |
|---|---|
| Raft | Verständliches verteiltes Konsensprotokoll, das Konsens in Wahl, Replikation und Sicherheit zerlegt |
| Leader-Wahl | Follower-Timeout, wird Candidate, initiiert Wahl, gewinnt Mehrheitsstimmen und wird Leader |
| Log-Replikation | Leader repliziert Client-Anfragen als Log-Einträge an alle Follower |
| Heartbeat-Timeout | Leader sendet periodische Heartbeats; Follower lösen Wahl bei Timeout aus |
| Term | Logische Uhr von Raft, bei jeder Wahl inkrementiert, zur Erkennung veralteter Informationen |
| Commit-Index | Log-Index, der von der Mehrheit der Knoten bestätigt wurde; Einträge vor diesem Index sind committet |
| Snapshot | Komprimiert committete Logs in einen Zustandssnapshot, um unbegrenztes Log-Wachstum zu verhindern |
| Mitgliedsänderung | Dynamisches Hinzufügen/Entfernen von Cluster-Knoten ohne Erzeugung zweier Leader |
| Linearisierbare Konsistenz | Lesevorgänge beobachten den neuesten Schreibvorgang; Raft implementiert dies über ReadIndex oder Lease Read |
Fünf Kernherausforderungen
Produktions-Raft-Konsens ist weit mehr als „wähle einen Leader und repliziere Logs". Sie müssen diese 5 Kernherausforderungen bewältigen:
1. Stabilität der Leader-Wahl — Netzwerkschwankungen verursachen häufige Wahlen und machen den Cluster während Leader-Übergängen unavailable. Wie vermeidet man Wahlstürme? Wie setzt man vernünftige Timeout-Parameter?
2. Konsistenz der Log-Replikation — Nach Netzwerkpartitionen können Logs divergieren. Wie konfliktbehaftete Logs nach Wiederherstellung abschneiden? Wie langsame Knoten aufholen lassen, ohne den Gesamtdurchsatz zu beeinträchtigen?
3. Umgang mit Netzwerkpartitionen — Die Minderheitspartition scheitert weiterhin bei Wahlen, während die Mehrheitspartition normal arbeitet. Wie sicher zusammenführen nach Partitions-Wiederherstellung?
4. Sicherheit bei Mitgliedsänderungen — Hinzufügen/Entfernen von Knoten in einem Schritt kann ein Fenster schaffen, in dem zwei Leader koexistieren. Wie sichere Mitgliedsänderungen implementieren?
5. Snapshot und Log-Kompaktierung — Unbegrenztes Log-Wachstum erschöpft Festplatte und Speicher. Wann Snapshots auslösen? Wie Snapshots übertragen ohne normale Replikation zu blockieren?
Modul 1: Raft-Zustandsmaschine und Leader-Wahl
Raft-Knoten haben drei Zustände: Follower, Candidate und Leader. Ein Follower, der innerhalb des Wahl-Timeouts keinen Heartbeat empfängt, wird Candidate und initiiert eine Wahl.
type NodeState int
const (
Follower NodeState = iota
Candidate
Leader
)
type RaftNode struct {
mu sync.Mutex
id int
state NodeState
currentTerm int
votedFor int
log []LogEntry
commitIndex int
lastApplied int
nextIndex map[int]int
matchIndex map[int]int
heartbeatCh chan struct{}
electionTimer *time.Timer
peers []string
}
func (rn *RaftNode) startElection() {
rn.mu.Lock()
rn.state = Candidate
rn.currentTerm++
rn.votedFor = rn.id
term := rn.currentTerm
lastLogIndex := len(rn.log) - 1
lastLogTerm := 0
if lastLogIndex >= 0 {
lastLogTerm = rn.log[lastLogIndex].Term
}
rn.mu.Unlock()
votesReceived := 1
voteCh := make(chan bool, len(rn.peers))
for i, peer := range rn.peers {
if i == rn.id {
continue
}
go func(peerAddr string) {
resp := rn.requestVote(peerAddr, &RequestVoteArgs{
Term: term,
CandidateID: rn.id,
LastLogIndex: lastLogIndex,
LastLogTerm: lastLogTerm,
})
voteCh <- resp.VoteGranted
}(peer)
}
for i := 0; i < len(rn.peers)-1; i++ {
if <-voteCh {
votesReceived++
}
}
rn.mu.Lock()
defer rn.mu.Unlock()
if votesReceived > len(rn.peers)/2 && rn.currentTerm == term {
rn.state = Leader
rn.nextIndex = make(map[int]int)
rn.matchIndex = make(map[int]int)
for i := range rn.peers {
rn.nextIndex[i] = len(rn.log)
rn.matchIndex[i] = 0
}
go rn.heartbeatLoop()
}
}
Kernpunkt: Wahl-Timeouts sollten randomisiert werden (150-300ms), um zu verhindern, dass alle Knoten gleichzeitig timen und Stimmen spalten. Ein Candidate muss eine Mehrheit der Stimmen gewinnen, um Leader zu werden.
Modul 2: Log-Replikation und Konsistenz
Wenn der Leader eine Client-Anfrage empfängt, hängt er die Operation an sein lokales Log an und repliziert sie dann an alle Follower. Sobald eine Mehrheit bestätigt, wird der Eintrag committet.
type LogEntry struct {
Term int
Index int
Command interface{}
}
func (rn *RaftNode) appendEntries(args *AppendEntriesArgs) *AppendEntriesReply {
rn.mu.Lock()
defer rn.mu.Unlock()
reply := &AppendEntriesReply{Term: rn.currentTerm}
if args.Term < rn.currentTerm {
reply.Success = false
return reply
}
if args.PrevLogIndex >= 0 {
if args.PrevLogIndex >= len(rn.log) {
reply.Success = false
return reply
}
if rn.log[args.PrevLogIndex].Term != args.PrevLogTerm {
rn.log = rn.log[:args.PrevLogIndex]
reply.Success = false
return reply
}
}
for i, entry := range args.Entries {
idx := args.PrevLogIndex + 1 + i
if idx < len(rn.log) {
if rn.log[idx].Term != entry.Term {
rn.log = rn.log[:idx]
rn.log = append(rn.log, entry)
}
} else {
rn.log = append(rn.log, entry)
}
}
if args.LeaderCommit > rn.commitIndex {
lastNewIdx := args.PrevLogIndex + len(args.Entries)
if args.LeaderCommit < lastNewIdx {
rn.commitIndex = args.LeaderCommit
} else {
rn.commitIndex = lastNewIdx
}
}
rn.currentTerm = max(rn.currentTerm, args.Term)
reply.Term = rn.currentTerm
reply.Success = true
return reply
}
func (rn *RaftNode) replicateLog() {
rn.mu.Lock()
defer rn.mu.Unlock()
if rn.state != Leader {
return
}
for i, peer := range rn.peers {
if i == rn.id {
continue
}
prevIdx := rn.nextIndex[i] - 1
prevTerm := 0
if prevIdx >= 0 && prevIdx < len(rn.log) {
prevTerm = rn.log[prevIdx].Term
}
entries := rn.log[rn.nextIndex[i]:]
go func(peerAddr string, peerID int) {
resp := rn.sendAppendEntries(peerAddr, &AppendEntriesArgs{
Term: rn.currentTerm,
LeaderID: rn.id,
PrevLogIndex: prevIdx,
PrevLogTerm: prevTerm,
Entries: entries,
LeaderCommit: rn.commitIndex,
})
rn.mu.Lock()
if resp.Success {
rn.nextIndex[peerID] = prevIdx + len(entries) + 1
rn.matchIndex[peerID] = rn.nextIndex[peerID] - 1
} else {
rn.nextIndex[peerID] = max(1, rn.nextIndex[peerID]-1)
}
rn.mu.Unlock()
}(peer, i)
}
}
Kernpunkt: Log-Konsistenzprüfungen verwenden PrevLogIndex und PrevLogTerm. Wenn das Log eines Followers vom Leader abweicht, dekrementiert der Leader nextIndex schrittweise, bis er den Übereinstimmungspunkt findet.
Modul 3: Heartbeat- und Timeout-Mechanismus
Heartbeats sind der Kern des Raft-Betriebs—der Leader erhält Autorität durch Heartbeats, und Follower erkennen Leader-Verfügbarkeit durch Heartbeats.
func (rn *RaftNode) heartbeatLoop() {
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
for {
<-ticker.C
rn.mu.Lock()
if rn.state != Leader {
rn.mu.Unlock()
return
}
rn.mu.Unlock()
rn.replicateLog()
}
}
func (rn *RaftNode) electionTimeoutLoop() {
for {
rn.resetElectionTimer()
select {
case <-rn.electionTimer.C:
rn.mu.Lock()
if rn.state != Leader {
rn.mu.Unlock()
rn.startElection()
continue
}
rn.mu.Unlock()
case <-rn.heartbeatCh:
continue
}
}
}
func (rn *RaftNode) resetElectionTimer() {
timeout := time.Duration(150+rand.Intn(150)) * time.Millisecond
if rn.electionTimer != nil {
rn.electionTimer.Stop()
}
rn.electionTimer = time.NewTimer(timeout)
}
Kernpunkt: Das Heartbeat-Intervall sollte viel kleiner sein als das Wahl-Timeout (typischerweise 1/5 bis 1/10), um zu verhindern, dass Follower timen, bevor der Heartbeat des Leaders eintrifft. Timeouts basierend auf RTT für standortübergreifende Bereitstellungen anpassen.
Modul 4: Snapshot und Log-Kompaktierung
Unbegrenztes Log-Wachstum erschöpft Ressourcen. Der Snapshot-Mechanismus komprimiert committete Logs in einen Zustandsautomaten-Snapshot und behält nur Logs nach dem Snapshot.
type Snapshot struct {
LastIncludedIndex int
LastIncludedTerm int
Data []byte
}
func (rn *RaftNode) takeSnapshot() {
rn.mu.Lock()
defer rn.mu.Unlock()
if rn.commitIndex <= 0 {
return
}
snapshotIdx := rn.commitIndex
snapshotTerm := rn.log[snapshotIdx].Term
stateData := rn.stateMachine.Serialize()
snap := Snapshot{
LastIncludedIndex: snapshotIdx,
LastIncludedTerm: snapshotTerm,
Data: stateData,
}
rn.log = rn.log[snapshotIdx+1:]
rn.lastApplied = snapshotIdx
rn.persistSnapshot(snap)
}
func (rn *RaftNode) installSnapshot(args *InstallSnapshotArgs) *InstallSnapshotReply {
rn.mu.Lock()
defer rn.mu.Unlock()
reply := &InstallSnapshotReply{Term: rn.currentTerm}
if args.Term < rn.currentTerm {
return reply
}
if args.LastIncludedIndex > rn.commitIndex {
rn.log = rn.log[args.LastIncludedIndex-rn.lastApplied:]
rn.commitIndex = args.LastIncludedIndex
rn.lastApplied = args.LastIncludedIndex
}
rn.stateMachine.Deserialize(args.Data)
rn.currentTerm = max(rn.currentTerm, args.Term)
reply.Term = rn.currentTerm
return reply
}
Kernpunkt: Die Snapshot-Größe sollte auf 10-100MB begrenzt werden. Snapshot-Übertragung darf die normale Log-Replikation nicht blockieren; einen separaten RPC-Kanal verwenden.
Modul 5: Mitgliedsänderung und Sicherheit
Direktes Wechseln von der alten zur neuen Konfiguration kann Split-Brain verursachen. Raft implementiert sichere Änderungen durch Joint Consensus.
type ConfigChange struct {
Type string // "add" or "remove"
NodeID int
Address string
}
func (rn *RaftNode) proposeConfigChange(change ConfigChange) error {
rn.mu.Lock()
if rn.state != Leader {
rn.mu.Unlock()
return fmt.Errorf("not leader")
}
rn.mu.Unlock()
entry := LogEntry{
Term: rn.currentTerm,
Command: change,
}
rn.mu.Lock()
rn.log = append(rn.log, entry)
rn.mu.Unlock()
rn.replicateLog()
return nil
}
func (rn *RaftNode) applyConfigChange(change ConfigChange) {
rn.mu.Lock()
defer rn.mu.Unlock()
switch change.Type {
case "add":
if !rn.containsPeer(change.NodeID) {
rn.peers = append(rn.peers, change.Address)
rn.nextIndex[change.NodeID] = len(rn.log)
rn.matchIndex[change.NodeID] = 0
}
case "remove":
newPeers := make([]string, 0)
for i, p := range rn.peers {
if i != change.NodeID {
newPeers = append(newPeers, p)
}
}
rn.peers = newPeers
delete(rn.nextIndex, change.NodeID)
delete(rn.matchIndex, change.NodeID)
}
}
Kernpunkt: Produktionsumgebungen empfehlen Einzelknotenänderungen (einen Knoten gleichzeitig hinzufügen/entfernen), um die Komplexität von Joint Consensus zu vermeiden. Sicherstellen, dass der Cluster vor dem Entfernen eines Knotens weiterhin eine Mehrheit hat.
Modul 6: Client-Lese-/Schreibzugriff und linearisierbare Konsistenz
Wenn Raft-Lesevorgänge den Leader umgehen, können veraltete Daten zurückgegeben werden. Linearisierbare Lesevorgänge erfordern ReadIndex oder Lease Read.
func (rn *RaftNode) linearizableRead() (interface{}, error) {
rn.mu.Lock()
if rn.state != Leader {
leaderID := rn.currentLeader
rn.mu.Unlock()
return nil, fmt.Errorf("not leader, redirect to %d", leaderID)
}
readIndex := rn.commitIndex
term := rn.currentTerm
rn.mu.Unlock()
confirmCh := make(chan bool, len(rn.peers))
confirmed := 1
for i, peer := range rn.peers {
if i == rn.id {
continue
}
go func(addr string) {
resp := rn.sendHeartbeat(addr, term)
confirmCh <- resp.Success
}(peer)
}
for i := 0; i < len(rn.peers)-1; i++ {
if <-confirmCh {
confirmed++
}
}
if confirmed <= len(rn.peers)/2 {
return nil, fmt.Errorf("lost leadership")
}
rn.mu.Lock()
for rn.lastApplied < readIndex {
rn.mu.Unlock()
time.Sleep(time.Millisecond)
rn.mu.Lock()
}
result := rn.stateMachine.Read()
rn.mu.Unlock()
return result, nil
}
func (rn *RaftNode) leaseRead() (interface{}, error) {
rn.mu.Lock()
defer rn.mu.Unlock()
if rn.state != Leader {
return nil, fmt.Errorf("not leader")
}
if time.Since(rn.leaseStart) > rn.leaseDuration {
return nil, fmt.Errorf("lease expired, fallback to ReadIndex")
}
return rn.stateMachine.Read(), nil
}
Kernpunkt: ReadIndex garantiert Linearisierbarkeit, erfordert aber einen RPC-Roundtrip. Lease Read basiert auf Uhr-Annahmen—bessere Leistung, aber mit Risiko. Produktion empfiehlt ReadIndex + 1-Sekunden-Lease-Optimierung.
5 Häufige Fallstricke
| # | Fallstrick | Konsequenz | Korrekter Ansatz |
|---|---|---|---|
| 1 | ❌ Feste Wahl-Timeouts | Mehrere Knoten timen gleichzeitig und spalten Stimmen | ✅ Wahl-Timeouts randomisieren (150-300ms), um Stimmenspaltung zu vermeiden |
| 2 | ❌ Follower beantworten Lesevorgänge direkt | Geben veraltete Daten zurück, verletzen linearisierbare Konsistenz | ✅ Lesevorgänge an Leader weiterleiten, ReadIndex für Konsistenz verwenden |
| 3 | ❌ Mitgliedsänderung in einem Schritt | Zwei Leader können während der Änderung koexistieren (Split-Brain) | ✅ Einzelknotenänderung oder Joint-Consensus-Phasenänderung |
| 4 | ❌ Snapshot blockiert Log-Replikation | Cluster-Schreibvorgänge stoppen während langsamer Snapshot-Übertragung | ✅ Separater RPC-Kanal für Snapshot-Übertragung, AppendEntries nicht blockieren |
| 5 | ❌ currentTerm und votedFor nicht persistent gespeichert | Knoten kann nach Neustart zweimal abstimmen und Wahlsicherheit brechen | ✅ Term/Vote bei jeder Aktualisierung synchron auf stabilen Speicher persistieren |
10 Fehlerbehebung
| # | Fehlersymptom | Mögliche Ursache | Fehlerbehebungsmethode |
|---|---|---|---|
| 1 | Häufige Leader-Wechsel | Wahl-Timeout zu kurz oder hohe Netzwerklatenz | Wahl-Timeout erhöhen, sicherstellen dass Heartbeat-Intervall < Wahl-Timeout / 5 |
| 2 | Hohe Log-Replikationslatenz | Langsame Knoten bremsen den Gesamtcommit | Asynchrone Replikation aktivieren, maxInflight-Batchgrößenlimit setzen |
| 3 | term mismatch |
Alter Leader versucht noch zu schreiben nach Netzwerkpartition | Prüfen ob Leader aktuellen Term hat; alter Leader stuft sich nach Partitions-Wiederherstellung automatisch herab |
| 4 | OOM bei Snapshot-Übertragung | Snapshot zu groß, auf einmal in Speicher geladen | Snapshot in Chunks übertragen, 1-4MB pro Chunk |
| 5 | Cluster nach Mitgliedsänderung unavailable | Mehrheit nach Änderung verloren | Sicherstellen dass Knoten nach Änderung noch Mehrheit bilden; ungerade Knotenanzahl bereitstellen |
| 6 | Veraltete Daten gelesen | Follower beantwortet Lesevorgänge ohne den Leader | ReadIndex oder Lease Read für linearisierbare Lesevorgänge aktivieren |
| 7 | Logs nach Knotenneustart verloren | Logs nicht auf stabilen Speicher persistiert | fsync nach jedem Log-Append; WAL für Dauerhaftigkeit verwenden |
| 8 | commitIndex not advancing |
Minderheit der Knoten ausgefallen, kann Mehrheit nicht erreichen | Prüfen ob lebende Knoten Mehrheit bilden; ausgefallene Knoten ggf. entfernen |
| 9 | Wahlstürme | Verhältnis zwischen Heartbeat-Intervall und Wahl-Timeout unausgewogen | Heartbeat-Intervall auf 1/10 des Wahl-Timeouts setzen; PreVote-Phase hinzufügen |
| 10 | Inkonsistenter Zustand nach Snapshot | Atomizitätsproblem zwischen Snapshot-Schreiben und Log-Trunkierung | Snapshot-Schreiben und Log-Trunkierung in derselben Transaktion abschließen |
Erweiterte Optimierung
1. PreVote-Phase verhindert Wahlstürme — Eine PreVote-Phase vor dem formellen RequestVote hinzufügen. Wahl nur initiieren, wenn Logs ausreichend aktuell sind, verhindert Wahlstürme nach Netzwerkpartitions-Wiederherstellung.
2. Batch-Log-Replikation für Durchsatz — Der Leader fasst mehrere Log-Einträge in einem einzigen AppendEntries-RPC zusammen und reduziert Netzwerk-Roundtrips. etcd verwendet standardmäßig ein Batch-Limit von 1024 Einträgen.
3. Asynchrone Snapshot-Übertragung — Einen separaten Streaming-RPC für Snapshot-Übertragung verwenden ohne den normalen Log-Replikationskanal zu blockieren. Ratenbegrenzung anwenden, um zu verhindern, dass Snapshot-Übertragung die gesamte Bandbreite verbraucht.
4. Learner-Knoten reduzieren Änderungsrisiko — Neue Knoten treten zuerst als Learner bei, holen Logs nach und werden dann zu Voters konvertiert—verhindert, dass neue Knoten Commits blockieren.
5. ReadIndex-Cache-Optimierung — Der Leader speichert den neuesten ReadIndex nach Heartbeat-Bestätigung im Cache. Nachfolgende Lesevorgänge verwenden den gecachten Wert und reduzieren die Heartbeat-Bestätigungshäufigkeit.
Vergleich: Raft vs Multi-Paxos vs EPaxos vs ZAB
| Dimension | Raft | Multi-Paxos | EPaxos | ZAB |
|---|---|---|---|---|
| Verständlichkeit | ✅ Für Klarheit entworfen | ❌ Obskure Veröffentlichung, komplexe Implementierung | ⚠️ Mittel, basiert auf Abhängigkeitsgraphen | ⚠️ Mittel, ähnlich wie Raft |
| Leader-Abhängigkeit | ✅ Starkes Leader-Modell | ✅ Hat Leader, aber optimierbar | ❌ Ohne Leader, jede Replika kann vorschlagen | ✅ Starkes Leader-Modell |
| Standortübergreifende Latenz | ❌ Schreibvorgänge erfordern Leader-Bestätigung | ⚠️ Optimierbar, aber komplex | ✅ Ohne Leader, auf nächstgelegene Replika schreiben | ❌ Schreibvorgänge erfordern Leader-Bestätigung |
| Log-Reihenfolge | ✅ Starke Ordnung, leicht zu reasoning | ⚠️ Erlaubt Unordnung, komplex | ❌ Abhängigkeitsgraph bestimmt Reihenfolge | ✅ Starke Ordnung |
| Mitgliedsänderung | ✅ Einzelknotenänderung ist einfach | ❌ Komplexe Implementierung | ⚠️ Mittel | ⚠️ Mittel |
| Ökosystem | ✅ etcd/Consul/TiKV | ⚠️ Chubby/Megastore | ❌ Begrenztes Ökosystem | ✅ ZooKeeper |
| Leistung | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
Auswahlliste: Einzelstandort-Bereitstellung → Raft (reifes Ökosystem, leicht verständlich); standortübergreifendes Active-Active → EPaxos (niedrige Latenz durch Nächst-Schreiben); ZooKeeper-Ökosystem → ZAB; Paxos nur bei Bestandsrestriktionen.
Zusammenfassung und Ausblick
Das Raft-Protokoll ist zum De-facto-Standard für Konsens verteilter Datenbanken im Jahr 2026 geworden—sein starkes Leader-Modell vereinfacht die Log-Verwaltung, randomisierte Wahl-Timeouts verhindern Stimmenspaltung, und der Snapshot-Mechanismus löst Log-Aufblähung. Aber Produktionsbereitstellung erfordert das Überwinden von 5 Kernherausforderungen: Stabilität der Leader-Wahl, Konsistenz der Log-Replikation, Umgang mit Netzwerkpartitionen, Sicherheit bei Mitgliedsänderungen und Snapshot/Log-Kompaktierung. Die 6 in diesem Artikel vorgestellten Kernmodule—Zustandsmaschine und Wahl, Log-Replikation, Heartbeat-Timeout, Snapshot-Kompression, Mitgliedsänderung und linearisierbare Lesevorgänge—decken die vollständige Kette von der Theorie bis zur Produktion ab. Denken Sie daran: Ein Konsensprotokoll ist nicht nur die Wahl eines Leaders—es ist ein vollständiges Engineering-System von der Wahlsicherheit bis zur linearisierbaren Konsistenz.
Empfohlene Online-Werkzeuge
- JSON-Formatierer — Raft-Logs und JSON-Konfiguration formatieren für schnelles Cluster-Zustands-Debugging
- Hash-Rechner — Prüfsummen-Fingerabdrücke für Snapshots und Logs generieren zur Gewährleistung der Datenintegrität
- cURL-zu-Code-Konverter — etcd/Consul-API-cURL-Befehle in Go-Client-Code umwandeln
- Base64-Kodierer — Binäre Snapshot-Daten in Raft-RPC für Übertragung kodieren
Probiere diese browser-lokalen Tools aus — keine Registrierung erforderlich →