一、Raft 的设计哲学
Paxos 虽然被证明正确,但极其难懂、难以实现。Raft 的目标是 可理解性------把共识问题拆解成三个相对独立的子问题:
Raft 的三个子问题
┌──────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ Leader 选举 │ │ 日志复制 │ │ 安全性 │ │
│ │ │ │ │ │ │ │
│ │ • 任期 Term │ │ • AppendEntries │ │ • 选举限制 │ │
│ │ • 心跳 │ │ • 日志匹配 │ │ • 提交规则 │ │
│ │ • 超时随机化 │ │ • 批量与管道 │ │ • 领导者唯⼀ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
与 Paxos 的关键区别:
| 特性 | Paxos | Raft |
|---|---|---|
| 角色 | Proposer/Acceptor/Learner | Leader/Follower/Candidate |
| 领导者 | 无固定 Leader,多 Proposer 竞争 | 强 Leader,所有写入经过 Leader |
| 日志连续性 | 不保证连续 | 强制连续(日志空洞不允许) |
| 成员变更 | 复杂(需要联合共识) | 简单(两阶段成员变更) |
| 可理解性 | 极差 | 较好(论文被评为最易理解的共识算法) |
二、节点状态与角色转换
┌─────────────┐
│ │
┌───────────│ Follower │◄──────────┐
│ │ │ │
│ └──────┬──────┘ │
│ │ │
│ 选举超时 │ 发现更高任期 │
│ (无心跳) │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ │ │
├──────────►│ Candidate │───────────┤
│ │ │ 选举失败 │
│ └──────┬──────┘ │
│ │ │
│ 获得多数票 │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ │ │
└───────────│ Leader │───────────┘
│ │ 崩溃/网络分区
└─────────────┘
核心规则:
- 每个节点初始为 Follower
- 收不到 Leader 心跳 → 变成 Candidate,发起选举
- 获得多数派选票 → 成为 Leader
- 发现更高任期的节点 → 立即降级为 Follower
三、核心数据结构
// distributed/raft/types.go
package raft
import (
"fmt"
"sync"
"time"
)
// NodeRole 节点角色
type NodeRole int
const (
RoleFollower NodeRole = iota
RoleCandidate
RoleLeader
)
func (r NodeRole) String() string {
switch r {
case RoleFollower:
return "Follower"
case RoleCandidate:
return "Candidate"
case RoleLeader:
return "Leader"
default:
return "Unknown"
}
}
// LogEntry 日志条目
type LogEntry struct {
Index uint64 `json:"index"`
Term uint64 `json:"term"`
Command interface{} `json:"command"`
}
// RaftState Raft 节点状态
type RaftState struct {
mu sync.RWMutex
// 持久化状态(所有节点)
CurrentTerm uint64 `json:"current_term"` // 当前任期
VotedFor string `json:"voted_for"` // 当前任期投给谁
Log []*LogEntry `json:"log"` // 日志条目集合
// 易失状态(所有节点)
CommitIndex uint64 `json:"commit_index"` // 已提交的最大索引
LastApplied uint64 `json:"last_applied"` // 已应用到状态机的最大索引
// 易失状态(Leader 专用)
NextIndex map[string]uint64 `json:"next_index"` // 发给每个 Follower 的下一条日志索引
MatchIndex map[string]uint64 `json:"match_index"` // 已复制到每个 Follower 的最高日志索引
}
// RequestVoteRequest 请求投票 RPC
type RequestVoteRequest struct {
Term uint64
CandidateID string
LastLogIndex uint64
LastLogTerm uint64
}
// RequestVoteResponse 投票响应
type RequestVoteResponse struct {
Term uint64
VoteGranted bool
}
// AppendEntriesRequest 追加日志 RPC
type AppendEntriesRequest struct {
Term uint64
LeaderID string
PrevLogIndex uint64
PrevLogTerm uint64
Entries []*LogEntry
LeaderCommit uint64
}
// AppendEntriesResponse 追加日志响应
type AppendEntriesResponse struct {
Term uint64
Success bool
// 优化:快速回退
ConflictTerm uint64
ConflictIndex uint64
}
// RaftNode Raft 节点
type RaftNode struct {
ID string
PeerIDs []string
State *RaftState
Role NodeRole
Transport Transport
// 计时器
ElectionTimer *time.Timer
HeartbeatTimer *time.Timer
ElectionTimeout time.Duration
HeartbeatTimeout time.Duration
// 通道
ApplyCh chan *LogEntry // 应用到状态机的通道
// 内部
stopCh chan struct{}
doneCh chan struct{}
}
// Transport 网络传输抽象
type Transport interface {
SendRequestVote(target string, req *RequestVoteRequest) (*RequestVoteResponse, error)
SendAppendEntries(target string, req *AppendEntriesRequest) (*AppendEntriesResponse, error)
BroadcastRequestVote(req *RequestVoteRequest) map[string]*RequestVoteResponse
}
四、Leader 选举实现
// distributed/raft/election.go
package raft
import (
"fmt"
"log"
"math/rand"
"sort"
"time"
)
// StartElection 开始选举
func (rn *RaftNode) StartElection() {
rn.State.mu.Lock()
// 转为 Candidate
rn.Role = RoleCandidate
rn.State.CurrentTerm++
rn.State.VotedFor = rn.ID
term := rn.State.CurrentTerm
lastLogIndex := uint64(0)
lastLogTerm := uint64(0)
if len(rn.State.Log) > 0 {
lastLogIndex = uint64(len(rn.State.Log))
lastLogTerm = rn.State.Log[len(rn.State.Log)-1].Term
}
rn.State.mu.Unlock()
log.Printf("[RAFT] Node %s starting election for term %d", rn.ID, term)
// 构造投票请求
req := &RequestVoteRequest{
Term: term,
CandidateID: rn.ID,
LastLogIndex: lastLogIndex,
LastLogTerm: lastLogTerm,
}
// 向所有节点请求投票
votes := 1 // 自己投自己
responses := rn.Transport.BroadcastRequestVote(req)
for peerID, resp := range responses {
rn.State.mu.Lock()
// 如果发现更高任期,降级
if resp.Term > rn.State.CurrentTerm {
rn.State.CurrentTerm = resp.Term
rn.Role = RoleFollower
rn.State.VotedFor = ""
rn.State.mu.Unlock()
log.Printf("[RAFT] Node %s stepped down: higher term %d", rn.ID, resp.Term)
return
}
if resp.VoteGranted {
votes++
log.Printf("[RAFT] Node %s granted vote from %s (votes=%d/%d)",
rn.ID, peerID, votes, len(rn.PeerIDs)+1)
} else {
log.Printf("[RAFT] Node %s denied vote from %s", rn.ID, peerID)
}
rn.State.mu.Unlock()
}
// 检查是否获得多数票
majority := len(rn.PeerIDs)/2 + 1
if votes >= majority {
rn.State.mu.Lock()
rn.Role = RoleLeader
rn.State.mu.Unlock()
log.Printf("[RAFT] Node %s ELECTED as leader for term %d (votes=%d/%d)",
rn.ID, term, votes, len(rn.PeerIDs)+1)
// 初始化 Leader 状态
rn.initLeaderState()
// 停止选举计时器,启动心跳计时器
rn.ElectionTimer.Stop()
rn.sendHeartbeats()
} else {
log.Printf("[RAFT] Node %s election FAILED for term %d (votes=%d, need %d)",
rn.ID, term, votes, majority)
}
}
// initLeaderState 初始化 Leader 状态
func (rn *RaftNode) initLeaderState() {
rn.State.mu.Lock()
defer rn.State.mu.Unlock()
nextIndex := uint64(len(rn.State.Log)) + 1
rn.State.NextIndex = make(map[string]uint64)
rn.State.MatchIndex = make(map[string]uint64)
for _, peerID := range rn.PeerIDs {
rn.State.NextIndex[peerID] = nextIndex
rn.State.MatchIndex[peerID] = 0
}
}
// resetElectionTimer 重置选举超时(随机化)
func (rn *RaftNode) resetElectionTimer() {
// 随机化超时时间:150-300ms
timeout := time.Duration(150+rand.Intn(150)) * time.Millisecond
rn.ElectionTimeout = timeout
rn.ElectionTimer.Reset(timeout)
}
// HandleRequestVote 处理投票请求
func (rn *RaftNode) HandleRequestVote(req *RequestVoteRequest) *RequestVoteResponse {
rn.State.mu.Lock()
defer rn.State.mu.Unlock()
resp := &RequestVoteResponse{
Term: rn.State.CurrentTerm,
VoteGranted: false,
}
// 1. 如果请求的任期小于当前任期,拒绝
if req.Term < rn.State.CurrentTerm {
log.Printf("[RAFT] Node %s rejects vote: req term %d < current term %d",
rn.ID, req.Term, rn.State.CurrentTerm)
return resp
}
// 2. 如果请求的任期大于当前任期,更新并降级
if req.Term > rn.State.CurrentTerm {
rn.State.CurrentTerm = req.Term
rn.Role = RoleFollower
rn.State.VotedFor = ""
}
// 3. 检查是否已经投票
if rn.State.VotedFor != "" && rn.State.VotedFor != req.CandidateID {
log.Printf("[RAFT] Node %s already voted for %s in term %d",
rn.ID, rn.State.VotedFor, req.Term)
return resp
}
// 4. 检查候选人的日志是否至少和自己一样新
lastLogIndex := uint64(0)
lastLogTerm := uint64(0)
if len(rn.State.Log) > 0 {
lastLogIndex = uint64(len(rn.State.Log))
lastLogTerm = rn.State.Log[len(rn.State.Log)-1].Term
}
upToDate := false
if req.LastLogTerm > lastLogTerm {
upToDate = true
} else if req.LastLogTerm == lastLogTerm && req.LastLogIndex >= lastLogIndex {
upToDate = true
}
if !upToDate {
log.Printf("[RAFT] Node %s rejects vote: candidate log not up-to-date (candidate: term=%d index=%d, me: term=%d index=%d)",
rn.ID, req.LastLogTerm, req.LastLogIndex, lastLogTerm, lastLogIndex)
return resp
}
// 投票给候选人
rn.State.VotedFor = req.CandidateID
resp.VoteGranted = true
// 重置选举超时(收到合法候选人的请求,说明有其他节点也在选举)
rn.resetElectionTimer()
log.Printf("[RAFT] Node %s GRANTS vote to %s for term %d", rn.ID, req.CandidateID, req.Term)
return resp
}
五、日志复制实现
// distributed/raft/replication.go
package raft
import (
"log"
"sync"
"time"
)
// Propose 客户端提案(Leader 专用)
func (rn *RaftNode) Propose(command interface{}) error {
rn.State.mu.Lock()
if rn.Role != RoleLeader {
rn.State.mu.Unlock()
return fmt.Errorf("not the leader")
}
// 追加到本地日志
entry := &LogEntry{
Index: uint64(len(rn.State.Log)) + 1,
Term: rn.State.CurrentTerm,
Command: command,
}
rn.State.Log = append(rn.State.Log, entry)
logIndex := entry.Index
rn.State.mu.Unlock()
log.Printf("[RAFT] Leader %s proposed command at index %d", rn.ID, logIndex)
// 并行复制到所有 Follower
rn.replicateLog()
return nil
}
// replicateLog 复制日志到所有 Follower
func (rn *RaftNode) replicateLog() {
var wg sync.WaitGroup
for _, peerID := range rn.PeerIDs {
wg.Add(1)
go func(peer string) {
defer wg.Done()
rn.replicateToPeer(peer)
}(peerID)
}
wg.Wait()
}
// replicateToPeer 复制日志到单个 Follower
func (rn *RaftNode) replicateToPeer(peerID string) {
rn.State.mu.Lock()
// 获取要发送的日志条目
nextIndex := rn.State.NextIndex[peerID]
if nextIndex > uint64(len(rn.State.Log))+1 {
nextIndex = uint64(len(rn.State.Log)) + 1
}
prevLogIndex := nextIndex - 1
prevLogTerm := uint64(0)
if prevLogIndex > 0 && prevLogIndex <= uint64(len(rn.State.Log)) {
prevLogTerm = rn.State.Log[prevLogIndex-1].Term
}
var entries []*LogEntry
if nextIndex <= uint64(len(rn.State.Log)) {
entries = rn.State.Log[nextIndex-1:]
}
leaderCommit := rn.State.CommitIndex
term := rn.State.CurrentTerm
rn.State.mu.Unlock()
// 如果没有要复制的日志,发送心跳
if len(entries) == 0 {
rn.sendHeartbeat(peerID)
return
}
// 构造 AppendEntries 请求
req := &AppendEntriesRequest{
Term: term,
LeaderID: rn.ID,
PrevLogIndex: prevLogIndex,
PrevLogTerm: prevLogTerm,
Entries: entries,
LeaderCommit: leaderCommit,
}
// 发送并处理响应
resp, err := rn.Transport.SendAppendEntries(peerID, req)
if err != nil {
log.Printf("[RAFT] Failed to send AppendEntries to %s: %v", peerID, err)
return
}
rn.State.mu.Lock()
defer rn.State.mu.Unlock()
// 如果发现更高任期,降级
if resp.Term > rn.State.CurrentTerm {
rn.State.CurrentTerm = resp.Term
rn.Role = RoleFollower
rn.State.VotedFor = ""
log.Printf("[RAFT] Leader %s stepped down: higher term %d", rn.ID, resp.Term)
return
}
if resp.Success {
// 更新 MatchIndex 和 NextIndex
matchIndex := prevLogIndex + uint64(len(entries))
if matchIndex > rn.State.MatchIndex[peerID] {
rn.State.MatchIndex[peerID] = matchIndex
}
rn.State.NextIndex[peerID] = matchIndex + 1
log.Printf("[RAFT] Leader %s: peer %s replicated up to index %d",
rn.ID, peerID, matchIndex)
// 尝试推进 commitIndex
rn.advanceCommitIndex()
} else {
// 日志不一致,回退 NextIndex
if resp.ConflictTerm != 0 {
// 找到冲突 term 的最后一条日志
lastIndexInConflictTerm := uint64(0)
for i := len(rn.State.Log) - 1; i >= 0; i-- {
if rn.State.Log[i].Term == resp.ConflictTerm {
lastIndexInConflictTerm = uint64(i) + 1
break
}
}
if lastIndexInConflictTerm > 0 {
rn.State.NextIndex[peerID] = lastIndexInConflictTerm
} else {
rn.State.NextIndex[peerID] = resp.ConflictIndex
}
} else {
rn.State.NextIndex[peerID] = resp.ConflictIndex
}
log.Printf("[RAFT] Leader %s: peer %s log mismatch, retrying at index %d",
rn.ID, peerID, rn.State.NextIndex[peerID])
}
}
// advanceCommitIndex 尝试推进提交索引
func (rn *RaftNode) advanceCommitIndex() {
// 找出能被多数派复制的最高日志索引
matchIndices := make([]uint64, 0, len(rn.PeerIDs)+1)
matchIndices = append(matchIndices, uint64(len(rn.State.Log))) // Leader 自己的日志
for _, matchIndex := range rn.State.MatchIndex {
matchIndices = append(matchIndices, matchIndex)
}
// 排序取中位数
sort.Slice(matchIndices, func(i, j int) bool {
return matchIndices[i] < matchIndices[j]
})
majorityIndex := matchIndices[len(matchIndices)/2]
// 只能提交当前任期的日志
if majorityIndex > rn.State.CommitIndex &&
rn.State.Log[majorityIndex-1].Term == rn.State.CurrentTerm {
rn.State.CommitIndex = majorityIndex
log.Printf("[RAFT] Leader %s advanced commit index to %d", rn.ID, majorityIndex)
// 应用到状态机
rn.applyLogEntries()
}
}
// applyLogEntries 应用已提交的日志到状态机
func (rn *RaftNode) applyLogEntries() {
for rn.State.LastApplied < rn.State.CommitIndex {
rn.State.LastApplied++
entry := rn.State.Log[rn.State.LastApplied-1]
// 发送到应用通道
select {
case rn.ApplyCh <- entry:
log.Printf("[RAFT] Applied entry index=%d term=%d command=%v",
entry.Index, entry.Term, entry.Command)
default:
log.Printf("[RAFT] Apply channel full, dropping entry %d", entry.Index)
}
}
}
// HandleAppendEntries 处理 AppendEntries RPC
func (rn *RaftNode) HandleAppendEntries(req *AppendEntriesRequest) *AppendEntriesResponse {
rn.State.mu.Lock()
defer rn.State.mu.Unlock()
resp := &AppendEntriesResponse{
Term: rn.State.CurrentTerm,
Success: false,
}
// 1. 任期检查
if req.Term < rn.State.CurrentTerm {
return resp
}
// 2. 如果请求的任期大于等于当前任期,承认 Leader
if req.Term >= rn.State.CurrentTerm {
rn.State.CurrentTerm = req.Term
rn.Role = RoleFollower
rn.State.VotedFor = ""
}
// 3. 重置选举超时(收到合法 Leader 的心跳)
rn.resetElectionTimer()
// 4. 检查日志匹配
if req.PrevLogIndex > 0 {
if req.PrevLogIndex > uint64(len(rn.State.Log)) {
// 日志不够长
resp.ConflictIndex = uint64(len(rn.State.Log)) + 1
resp.ConflictTerm = 0
return resp
}
if rn.State.Log[req.PrevLogIndex-1].Term != req.PrevLogTerm {
// 日志 term 不匹配
resp.ConflictTerm = rn.State.Log[req.PrevLogIndex-1].Term
for i := req.PrevLogIndex - 1; i > 0; i-- {
if rn.State.Log[i-1].Term != resp.ConflictTerm {
resp.ConflictIndex = i + 1
break
}
}
if resp.ConflictIndex == 0 {
resp.ConflictIndex = 1
}
return resp
}
}
// 5. 追加新日志(删除冲突的)
logInsertIndex := req.PrevLogIndex
for i, entry := range req.Entries {
if logInsertIndex+uint64(i)+1 <= uint64(len(rn.State.Log)) {
// 检查已有日志是否冲突
if rn.State.Log[logInsertIndex+uint64(i)].Term != entry.Term {
// 截断冲突日志
rn.State.Log = rn.State.Log[:logInsertIndex+uint64(i)]
rn.State.Log = append(rn.State.Log, entry)
}
} else {
// 追加新日志
rn.State.Log = append(rn.State.Log, entry)
}
}
// 6. 更新 commitIndex
if req.LeaderCommit > rn.State.CommitIndex {
lastNewIndex := req.PrevLogIndex + uint64(len(req.Entries))
if req.LeaderCommit < lastNewIndex {
rn.State.CommitIndex = req.LeaderCommit
} else {
rn.State.CommitIndex = lastNewIndex
}
// 应用到状态机
rn.applyLogEntries()
}
resp.Success = true
return resp
}
// sendHeartbeats 发送心跳到所有 Follower
func (rn *RaftNode) sendHeartbeats() {
for _, peerID := range rn.PeerIDs {
rn.sendHeartbeat(peerID)
}
}
// sendHeartbeat 发送心跳到单个 Follower
func (rn *RaftNode) sendHeartbeat(peerID string) {
rn.State.mu.Lock()
prevLogIndex := uint64(0)
prevLogTerm := uint64(0)
if len(rn.State.Log) > 0 {
prevLogIndex = uint64(len(rn.State.Log))
prevLogTerm = rn.State.Log[len(rn.State.Log)-1].Term
}
req := &AppendEntriesRequest{
Term: rn.State.CurrentTerm,
LeaderID: rn.ID,
PrevLogIndex: prevLogIndex,
PrevLogTerm: prevLogTerm,
Entries: nil, // 心跳没有日志条目
LeaderCommit: rn.State.CommitIndex,
}
rn.State.mu.Unlock()
go func() {
resp, err := rn.Transport.SendAppendEntries(peerID, req)
if err != nil {
return
}
rn.State.mu.Lock()
if resp.Term > rn.State.CurrentTerm {
rn.State.CurrentTerm = resp.Term
rn.Role = RoleFollower
rn.State.VotedFor = ""
}
rn.State.mu.Unlock()
}()
}
六、Raft 节点主循环
// distributed/raft/node.go
package raft
import (
"log"
"time"
)
// Run 启动 Raft 节点
func (rn *RaftNode) Run() {
log.Printf("[RAFT] Node %s starting (role=%s, peers=%v)", rn.ID, rn.Role, rn.PeerIDs)
// 初始化选举超时
rn.resetElectionTimer()
for {
select {
case <-rn.ElectionTimer.C:
// 选举超时,发起选举
rn.State.mu.Lock()
role := rn.Role
rn.State.mu.Unlock()
if role != RoleLeader {
log.Printf("[RAFT] Node %s election timeout, starting election", rn.ID)
rn.StartElection()
}
case <-rn.stopCh:
log.Printf("[RAFT] Node %s stopping", rn.ID)
close(rn.doneCh)
return
}
}
}
// Stop 停止 Raft 节点
func (rn *RaftNode) Stop() {
close(rn.stopCh)
<-rn.doneCh
}
七、使用示例
// examples/raft_demo.go
package main
import (
"fmt"
"log"
"time"
"distributed/raft"
)
// SimpleTransport 简单的内存传输实现
type SimpleTransport struct {
nodes map[string]*raft.RaftNode
}
func (t *SimpleTransport) SendRequestVote(target string, req *raft.RequestVoteRequest) (*raft.RequestVoteResponse, error) {
node, exists := t.nodes[target]
if !exists {
return nil, fmt.Errorf("node %s not found", target)
}
return node.HandleRequestVote(req), nil
}
func (t *SimpleTransport) SendAppendEntries(target string, req *raft.AppendEntriesRequest) (*raft.AppendEntriesResponse, error) {
node, exists := t.nodes[target]
if !exists {
return nil, fmt.Errorf("node %s not found", target)
}
return node.HandleAppendEntries(req), nil
}
func (t *SimpleTransport) BroadcastRequestVote(req *raft.RequestVoteRequest) map[string]*raft.RequestVoteResponse {
responses := make(map[string]*raft.RequestVoteResponse)
for id, node := range t.nodes {
if id == req.CandidateID {
continue
}
responses[id] = node.HandleRequestVote(req)
}
return responses
}
func main() {
fmt.Println("=== Raft 共识算法演示 ===\n")
// 1. 创建 3 个节点
nodeIDs := []string{"node-1", "node-2", "node-3"}
nodes := make(map[string]*raft.RaftNode)
transport := &SimpleTransport{nodes: nodes}
for _, id := range nodeIDs {
node := &raft.RaftNode{
ID: id,
PeerIDs: nodeIDs,
State: &raft.RaftState{
CurrentTerm: 0,
VotedFor: "",
Log: make([]*raft.LogEntry, 0),
CommitIndex: 0,
LastApplied: 0,
NextIndex: make(map[string]uint64),
MatchIndex: make(map[string]uint64),
},
Role: raft.RoleFollower,
Transport: transport,
ElectionTimer: time.NewTimer(time.Hour), // 稍后重置
HeartbeatTimer: time.NewTimer(time.Hour),
HeartbeatTimeout: 50 * time.Millisecond,
ApplyCh: make(chan *raft.LogEntry, 100),
stopCh: make(chan struct{}),
doneCh: make(chan struct{}),
}
nodes[id] = node
}
// 更新 transport 引用
transport.nodes = nodes
// 2. 启动所有节点
for _, node := range nodes {
go node.Run()
}
// 3. 等待选举完成
fmt.Println("等待 Leader 选举...")
time.Sleep(2 * time.Second)
// 4. 找到 Leader
var leader *raft.RaftNode
for _, node := range nodes {
node.State.mu.RLock()
if node.Role == raft.RoleLeader {
leader = node
fmt.Printf("Leader elected: %s (term %d)\n", node.ID, node.State.CurrentTerm)
}
node.State.mu.RUnlock()
}
if leader == nil {
log.Fatal("No leader elected!")
}
// 5. 通过 Leader 提交提案
fmt.Println("\n=== 提交提案 ===")
commands := []string{"set x=1", "set y=2", "set z=3"}
for _, cmd := range commands {
fmt.Printf("Proposing: %s\n", cmd)
if err := leader.Propose(cmd); err != nil {
log.Printf("Propose error: %v", err)
}
time.Sleep(100 * time.Millisecond)
}
// 6. 查看各节点的日志状态
fmt.Println("\n=== 节点日志状态 ===")
for _, node := range nodes {
node.State.mu.RLock()
fmt.Printf("Node %s (role=%s):\n", node.ID, node.Role)
fmt.Printf(" Term: %d\n", node.State.CurrentTerm)
fmt.Printf(" Log length: %d\n", len(node.State.Log))
fmt.Printf(" CommitIndex: %d\n", node.State.CommitIndex)
fmt.Printf(" LastApplied: %d\n", node.State.LastApplied)
for i, entry := range node.State.Log {
fmt.Printf(" Log[%d]: term=%d command=%v\n", i+1, entry.Term, entry.Command)
}
node.State.mu.RUnlock()
}
// 7. 模拟 Leader 宕机
fmt.Println("\n=== 模拟 Leader 宕机 ===")
fmt.Printf("Stopping leader %s...\n", leader.ID)
leader.Stop()
delete(nodes, leader.ID)
delete(transport.nodes, leader.ID)
// 更新其他节点的 PeerIDs
remainingIDs := make([]string, 0, len(nodes))
for id := range nodes {
remainingIDs = append(remainingIDs, id)
}
for _, node := range nodes {
node.PeerIDs = remainingIDs
}
// 等待新 Leader 选举
fmt.Println("等待新 Leader 选举...")
time.Sleep(3 * time.Second)
// 8. 找到新 Leader
var newLeader *raft.RaftNode
for _, node := range nodes {
node.State.mu.RLock()
if node.Role == raft.RoleLeader {
newLeader = node
fmt.Printf("New leader elected: %s (term %d)\n", node.ID, node.State.CurrentTerm)
}
node.State.mu.RUnlock()
}
if newLeader != nil {
// 通过新 Leader 继续提案
fmt.Println("\n=== 通过新 Leader 提交提案 ===")
newCommands := []string{"set a=10", "set b=20"}
for _, cmd := range newCommands {
fmt.Printf("Proposing: %s\n", cmd)
if err := newLeader.Propose(cmd); err != nil {
log.Printf("Propose error: %v", err)
}
time.Sleep(100 * time.Millisecond)
}
// 查看最终状态
fmt.Println("\n=== 最终节点状态 ===")
for _, node := range nodes {
node.State.mu.RLock()
fmt.Printf("Node %s (role=%s): log=%d entries\n",
node.ID, node.Role, len(node.State.Log))
node.State.mu.RUnlock()
}
}
// 9. 清理
for _, node := range nodes {
node.Stop()
}
fmt.Println("\n演示完成!")
}
八、生产坑位
| # | 坑位 | 现象 | 解决方案 |
|---|---|---|---|
| 1 | 选举超时设置不当 | 频繁选举或选举迟迟无法完成 | 超时时间随机化(150-300ms);根据网络延迟调整 |
| 2 | PreVote 缺失 | 网络抖动导致 Leader 频繁被罢免 | 实现 PreVote 机制:Candidate 先问大家能不能选 |
| 3 | 日志空洞 | 日志索引不连续导致一致性破坏 | Raft 强制日志连续,不允许空洞 |
| 4 | 快照未及时压缩 | 日志无限增长,重启恢复慢 | 定期生成快照;安装快照 RPC |
| 5 | 只读请求也走 Raft | 读请求也走一遍共识,性能差 | ReadIndex / LeaseRead 优化 |
| 6 | 成员变更时脑裂 | 新旧配置同时生效 | 联合共识(Joint Consensus);单节点变更 |
九、Raft vs Paxos 对比
| 维度 | Paxos | Raft |
|---|---|---|
| 理解难度 | 极高 | 中等 |
| 实现复杂度 | ~2000 行 | ~600 行(核心) |
| 领导者 | 可选(Multi-Paxos 有 Leader) | 强 Leader |
| 日志连续性 | 不保证 | 强制连续 |
| 成员变更 | 联合共识(复杂) | 单节点变更(简单) |
| 工业应用 | Google Chubby | etcd, Consul, TiKV, MongoDB |
| 教学价值 | 理论基石 | 工程实践 |
十、本讲总结
┌──────────────────────────────────────────────────────────────────────────┐
│ 第3讲 核心收获 │
│ │
│ ✅ 理解了 Raft 的三个子问题: │
│ - Leader 选举(任期、心跳、随机超时) │
│ - 日志复制(AppendEntries、日志匹配、提交规则) │
│ - 安全性(选举限制、领导者唯一性) │
│ │
│ ✅ 实现了 Raft 核心逻辑: │
│ - 节点角色转换(Follower → Candidate → Leader) │
│ - 选举流程(RequestVote RPC) │
│ - 日志复制(AppendEntries RPC + 快速回退) │
│ - 提交与应用到状态机 │
│ │
│ ✅ 通过实验验证了: │
│ - Leader 宕机后自动选举出新 Leader │
│ - 新 Leader 继承旧 Leader 的日志 │
│ - 所有节点最终日志一致 │
│ │
│ ✅ 覆盖了 6 个生产坑位与解决方案 │
│ ✅ 对比了 Raft 与 Paxos 的异同 │
│ │
└──────────────────────────────────────────────────────────────────────────┘
十一、下一讲预告
第4讲:《分布式存储基石:复制状态机》
你将学到:
- 状态机复制的核心原理
- WAL(Write-Ahead Log)实现
- 状态机快照(Snapshot)
- 线性一致读(ReadIndex / LeaseRead)
- 从零实现一个 KV 存储引擎
🧰 开发之余的小工具推荐
在调试 Raft 集群的日志复制一致性时,zz365.top 的文本差异比较工具可以快速对比不同节点的日志序列,精确定位日志冲突的位置和原因。其 JSON 格式化工具则可以将 Raft 节点状态导出为结构化数据,便于分析选举过程中的投票分布。所有处理均在浏览器本地完成,不会泄露你的集群数据。