各位技术同仁,大家好!
非常荣幸能在这里与大家共同探讨分布式系统领域一个永恒的挑战——如何实现分布式一致性。今天,我们将深入剖析 Raft 协议的核心引擎,并聚焦于在 Go 语言中实现这一协议时,我们必须面对的三个“生死时刻”。这三个时刻,正是 Raft 协议确保其强大容错性和一致性的基石,理解并正确处理它们,是构建任何可靠分布式系统的关键。
我们都知道,分布式系统因其固有的并发性、网络延迟、节点故障等问题,使得协调不同节点的状态变得异常复杂。Raft 协议的诞生,正是为了以一种更易于理解的方式解决这个难题。它将分布式一致性问题分解为几个相对独立的子问题:领导者选举、日志复制和安全性。我们将以 Go 语言为例,一步步揭示 Raft 如何在这些关键时刻做出决策,并最终达成一致。
在深入细节之前,我们先快速回顾一下 Raft 的基本概念,这将为我们后续的讨论打下坚实的基础。
Raft 协议核心概念速览
Raft 协议通过选举一个领导者 (Leader) 来管理集群中的日志复制,从而实现分布式一致性。集群中的每个节点都可以扮演以下三种角色之一:
- 跟随者 (Follower):被动接收领导者的请求,不主动发起任何操作。如果长时间未收到领导者的心跳,它会尝试成为候选者。
- 候选者 (Candidate):在领导者选举期间的角色。它会向其他节点请求投票,以期成为新的领导者。
- 领导者 (Leader):接收所有客户端请求,管理日志复制,并向所有跟随者发送心跳以维持其领导地位。
Raft 协议的关键机制包括:
- 任期 (Term):一个单调递增的整数,用于标识一个领导者的“任期”。每次选举都会开始一个新的任期。
- 日志 (Log):由一系列有序的日志条目组成,每个条目包含一个命令及其被创建时的任期。日志是复制的核心,它决定了状态机的最终状态。
- 状态机 (State Machine):一个确定性的状态机,Raft 协议通过复制日志并按顺序将其应用到状态机来保证集群状态的一致性。
- 安全性 (Safety):Raft 协议确保以下几点:
- 选举安全性 (Election Safety):在一个给定任期内,最多只有一个领导者被选举出来。
- 领导者只附加原则 (Leader Append-Only):领导者从不覆盖或删除自己的日志条目,只追加新的条目。
- 日志匹配原则 (Log Matching):如果两个日志在相同的索引和任期号上包含相同的条目,那么它们在该索引之前的所有条目都相同。
- 领导者完全性 (Leader Completeness):如果一个日志条目在给定任期内被提交,那么所有更高任期的领导者都必须拥有该条目。
理解了这些基本概念,我们就可以开始深入探讨 Raft 协议的三个“生死时刻”了。
第一个“生死时刻”:领导者选举 —— 谁来发号施令?
在分布式系统中,如果缺乏一个明确的指挥者,集群将陷入混乱。领导者选举是 Raft 协议最核心、也是最先需要解决的问题。它决定了哪个节点拥有发号施令的权力,从而统一处理客户端请求和管理日志复制。这个时刻的“生死”在于,如果不能高效、可靠地选出一个领导者,整个集群将无法对外提供服务,或者出现脑裂(split-brain)问题,导致数据不一致。
问题挑战
- 快速收敛:在旧领导者失效后,新领导者需要尽快被选出,以缩短服务中断时间。
- 避免脑裂:在任何给定的任期内,集群中只能有一个领导者。
- 容错性:即使部分节点故障,选举过程也应该能正常进行。
Raft 的解决方案
Raft 协议通过以下机制来解决这些挑战:
- 随机选举超时:每个跟随者都有一个选举超时时间。当这个时间到了仍未收到领导者的心跳,它就会转换为候选者并发起选举。随机化这个超时时间可以减少多个节点同时成为候选者并互相竞争的情况,从而加快选举收敛。
- 任期 (Term):每个选举周期都有一个唯一的、单调递增的任期号。节点在通信时会交换任期号,如果发现自己的任期号过时,会自动更新并回退为跟随者。这是防止脑裂的关键机制。
- 投票请求 (RequestVote RPC):候选者在发起选举时,会向其他节点发送
RequestVoteRPC 请求投票。 - 日志完整性检查:在投票时,节点会检查请求投票的候选者的日志是否至少和自己的日志一样新(即拥有相同的或更新的
lastLogTerm和lastLogIndex)。这是为了确保被选出的领导者拥有所有已提交的日志条目,满足“领导者完全性”原则。
Go 语言实现:核心逻辑与代码
首先,我们定义 Raft 节点的基本状态和 RPC 结构。
package raft
import (
"fmt"
"log"
"math/rand"
"sync"
"time"
)
// RaftState 定义了 Raft 节点在持久化存储中的状态
type RaftState struct {
CurrentTerm int // 节点已知的最新任期
VotedFor int // 在当前任期内,该节点投票给的候选者的 ID
Log []LogEntry // 日志条目,每个条目包含命令和任期
}
// LogEntry 代表一个日志条目
type LogEntry struct {
Term int // 日志条目被创建时的任期
Command interface{} // 要应用到状态机的命令
}
// RaftNode 结构体代表一个 Raft 节点
type RaftNode struct {
mu sync.Mutex // 保护 RaftNode 状态的互斥锁
peers []*ClientEnd // 与其他节点的 RPC 客户端连接
persister *Persister // 持久化存储接口
me int // 当前节点的 ID
// 所有的 Raft 状态变量都包含在 RaftState 中,便于持久化
RaftState
// 易失性状态 (所有服务器)
commitIndex int // 已知已提交的最高日志条目的索引
lastApplied int // 已应用到状态机的最高日志条目的索引
// 易失性状态 (领导者)
nextIndex []int // 对于每个服务器,下一个要发送给它的日志条目的索引
matchIndex []int // 对于每个服务器,已知已复制给它的最高日志条目的索引
state NodeState // 节点当前的角色 (Follower, Candidate, Leader)
electionTimer *time.Timer // 选举计时器
heartbeatTimer *time.Timer // 心跳计时器 (仅Leader使用)
applyCh chan ApplyMsg // 用于将已提交的日志条目传递给上层应用
killCh chan struct{} // 用于通知 Raft 节点停止运行
}
// NodeState 定义了 Raft 节点的角色
type NodeState int
const (
Follower NodeState = iota
Candidate
Leader
)
// ApplyMsg 是 Raft 节点向上层应用发送的已提交日志条目
type ApplyMsg struct {
CommandValid bool
Command interface{}
CommandIndex int
CommandTerm int
}
// RequestVoteArgs RPC 参数
type RequestVoteArgs struct {
Term int // 候选者的任期
CandidateId int // 请求投票的候选者 ID
LastLogIndex int // 候选者最新日志条目的索引
LastLogTerm int // 候选者最新日志条目的任期
}
// RequestVoteReply RPC 返回值
type RequestVoteReply struct {
Term int // 接收者当前的任期 (如果候选者任期过时,用于更新候选者)
VoteGranted bool // 如果候选者获得了投票,则为 true
}
// NewRaft 创建一个新的 Raft 节点
func NewRaft(peers []*ClientEnd, me int, persister *Persister, applyCh chan ApplyMsg) *RaftNode {
rf := &RaftNode{}
rf.peers = peers
rf.persister = persister
rf.me = me
rf.applyCh = applyCh
rf.killCh = make(chan struct{})
rf.RaftState = RaftState{
CurrentTerm: 0,
VotedFor: -1, // -1 表示尚未投票给任何人
Log: make([]LogEntry, 1), // Raft 日志从索引 1 开始,所以索引 0 是一个哑元条目
}
rf.commitIndex = 0
rf.lastApplied = 0
// 初始化为 Follower 状态
rf.state = Follower
// 读取持久化状态
rf.readPersist(persister.ReadRaftState())
// 启动选举计时器
rf.electionTimer = time.NewTimer(rf.getRandomElectionTimeout())
rf.heartbeatTimer = time.NewTimer(time.Duration(0)) // 初始时禁用心跳,只有Leader才需要
// 启动 Raft 节点的主循环
go rf.mainLoop()
return rf
}
// getRandomElectionTimeout 生成一个随机的选举超时时间
func (rf *RaftNode) getRandomElectionTimeout() time.Duration {
// 选举超时时间通常在 150ms 到 300ms 之间
return time.Duration(150 + rand.Intn(150)) * time.Millisecond
}
核心选举逻辑:startElection 函数
当一个跟随者在选举超时时间内没有收到领导者的心跳,它就会调用 startElection 方法。
// startElection 将节点转换为候选者,发起选举
func (rf *RaftNode) startElection() {
rf.mu.Lock()
defer rf.mu.Unlock()
// 转换为 Candidate 状态
rf.state = Candidate
rf.CurrentTerm++ // 增加当前任期
rf.VotedFor = rf.me // 投票给自己
rf.persist() // 持久化状态变更
log.Printf("Node %d: Starting election for term %d", rf.me, rf.CurrentTerm)
votesReceived := 1 // 已经投给自己一票
electionTerm := rf.CurrentTerm
lastLogIndex := len(rf.Log) - 1
lastLogTerm := rf.Log[lastLogIndex].Term
// 重置选举计时器
rf.electionTimer.Reset(rf.getRandomElectionTimeout())
// 向所有其他节点发送 RequestVote RPC
for i := 0; i < len(rf.peers); i++ {
if i == rf.me {
continue
}
go func(server int) {
args := RequestVoteArgs{
Term: electionTerm,
CandidateId: rf.me,
LastLogIndex: lastLogIndex,
LastLogTerm: lastLogTerm,
}
reply := RequestVoteReply{}
ok := rf.sendRequestVote(server, &args, &reply)
if !ok {
// RPC 调用失败,可能目标节点已下线或网络问题
return
}
rf.mu.Lock()
defer rf.mu.Unlock()
// 如果当前节点已经不是候选者,或者任期发生了变化,则忽略此回复
if rf.state != Candidate || rf.CurrentTerm != electionTerm {
return
}
// 如果收到更高的任期号,立即转换为 Follower
if reply.Term > rf.CurrentTerm {
log.Printf("Node %d: Received RequestVoteReply with higher term %d from %d. Stepping down to Follower.", rf.me, reply.Term, server)
rf.becomeFollower(reply.Term)
return
}
// 如果获得了投票
if reply.VoteGranted {
votesReceived++
// 如果获得了多数票,成为领导者
if votesReceived > len(rf.peers)/2 {
rf.becomeLeader()
}
}
}(i)
}
}
处理投票请求:RequestVote RPC Handler
// RequestVote RPC 处理函数
func (rf *RaftNode) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
reply.Term = rf.CurrentTerm
reply.VoteGranted = false
// 规则 1: 如果候选者的任期小于当前节点,拒绝投票
if args.Term < rf.CurrentTerm {
log.Printf("Node %d: Refusing vote for %d (term %d) because my term %d is higher.", rf.me, args.CandidateId, args.Term, rf.CurrentTerm)
return
}
// 规则 2: 如果候选者的任期大于当前节点,或者任期相同但尚未投票,则考虑投票
if args.Term > rf.CurrentTerm {
log.Printf("Node %d: Received RequestVote from %d with higher term %d. Stepping down to Follower.", rf.me, args.CandidateId, args.Term)
rf.becomeFollower(args.Term) // 转换为 Follower,并更新任期
}
// 规则 3: 投票条件:
// a. 在当前任期内尚未投票 (VotedFor 为 -1 或已投票给当前候选者)
// b. 候选者的日志至少和当前节点的日志一样新
if (rf.VotedFor == -1 || rf.VotedFor == args.CandidateId) && rf.isLogUpToDate(args.LastLogIndex, args.LastLogTerm) {
rf.VotedFor = args.CandidateId
rf.CurrentTerm = args.Term // 确保任期一致
rf.persist() // 持久化投票和任期变更
reply.VoteGranted = true
log.Printf("Node %d: Voted for %d in term %d.", rf.me, args.CandidateId, rf.CurrentTerm)
rf.electionTimer.Reset(rf.getRandomElectionTimeout()) // 投票后重置选举计时器
} else {
log.Printf("Node %d: Refusing vote for %d (term %d). VotedFor: %d, LogUpToDate: %t", rf.me, args.CandidateId, args.Term, rf.VotedFor, rf.isLogUpToDate(args.LastLogIndex, args.LastLogTerm))
}
}
// isLogUpToDate 检查候选者的日志是否至少和当前节点的日志一样新
func (rf *RaftNode) isLogUpToDate(candidateLastLogIndex int, candidateLastLogTerm int) bool {
myLastLogIndex := len(rf.Log) - 1
myLastLogTerm := rf.Log[myLastLogIndex].Term
// 如果候选者的最新日志条目任期号更大,则其日志更新
if candidateLastLogTerm > myLastLogTerm {
return true
}
// 如果任期号相同,但候选者的日志更长,则其日志更新
if candidateLastLogTerm == myLastLogTerm && candidateLastLogIndex >= myLastLogIndex {
return true
}
return false
}
状态转换辅助函数
// becomeFollower 将节点转换为 Follower 状态
func (rf *RaftNode) becomeFollower(newTerm int) {
rf.state = Follower
rf.CurrentTerm = newTerm
rf.VotedFor = -1 // 重置投票
rf.persist()
log.Printf("Node %d: Became Follower in term %d.", rf.me, newTerm)
rf.electionTimer.Reset(rf.getRandomElectionTimeout())
rf.heartbeatTimer.Stop() // 作为Follower不需要心跳计时器
}
// becomeCandidate 将节点转换为 Candidate 状态
// (通常由startElection调用,这里只是一个辅助函数)
func (rf *RaftNode) becomeCandidate() {
rf.state = Candidate
rf.CurrentTerm++
rf.VotedFor = rf.me
rf.persist()
log.Printf("Node %d: Became Candidate in term %d.", rf.me, rf.CurrentTerm)
rf.electionTimer.Reset(rf.getRandomElectionTimeout())
rf.heartbeatTimer.Stop()
}
// becomeLeader 将节点转换为 Leader 状态
func (rf *RaftNode) becomeLeader() {
rf.state = Leader
log.Printf("Node %d: Became Leader in term %d.", rf.me, rf.CurrentTerm)
// 初始化 nextIndex 和 matchIndex
rf.nextIndex = make([]int, len(rf.peers))
rf.matchIndex = make([]int, len(rf.peers))
for i := 0; i < len(rf.peers); i++ {
rf.nextIndex[i] = len(rf.Log) // 新 Leader 认为所有 Follower 的日志都落后于自己,从自己的日志末尾开始发送
rf.matchIndex[i] = 0 // 初始时,Leader 认为 Follower 的匹配索引为 0
}
rf.electionTimer.Stop() // Leader 不需要选举计时器
rf.heartbeatTimer.Reset(time.Duration(50) * time.Millisecond) // 启动心跳计时器
// 立即发送一次心跳,宣示主权
go rf.sendHeartbeats()
}
mainLoop 负责处理定时器事件
// mainLoop 是 Raft 节点的主循环
func (rf *RaftNode) mainLoop() {
for {
select {
case <-rf.killCh:
return // 节点被杀死,退出循环
case <-rf.electionTimer.C:
rf.mu.Lock()
if rf.state != Leader { // 只有 Follower 和 Candidate 才需要处理选举超时
rf.startElection()
}
rf.mu.Unlock()
case <-rf.heartbeatTimer.C:
rf.mu.Lock()
if rf.state == Leader { // 只有 Leader 才需要发送心跳
rf.sendHeartbeats()
rf.heartbeatTimer.Reset(time.Duration(50) * time.Millisecond) // 重置心跳计时器
}
rf.mu.Unlock()
}
}
}
在这个“生死时刻”,Raft 通过精巧的计时器、任期机制和日志完整性检查,确保了集群在面对领导者失效时能够快速、安全地选出新的领导者,从而避免了服务的长时间中断和数据的不一致。
第二个“生死时刻”:日志复制 —— 如何确保步调一致?
一旦领导者被选举出来,它的核心职责就是处理客户端请求,并将这些请求以日志条目的形式复制到集群中的所有跟随者。这个过程的“生死”在于,如果日志复制不能可靠且一致地完成,那么即使选出了领导者,不同节点的状态也会发散,最终导致系统无法提供正确的数据或服务。日志复制是 Raft 协议达成强一致性的核心。
问题挑战
- 可靠性:即使部分跟随者故障或网络不稳定,日志也应能最终复制到大多数节点。
- 一致性:所有已提交的日志条目必须以相同的顺序应用到所有节点的状态机。
- 效率:日志复制应尽可能高效,以降低请求延迟。
- 处理不一致:当领导者和跟随者的日志不一致时,领导者需要有机制强制跟随者与自己保持一致。
Raft 的解决方案
Raft 协议通过 AppendEntries RPC 来解决这些挑战:
- 心跳与日志复制合一:领导者会周期性地向所有跟随者发送
AppendEntriesRPC。如果 RPC 中不包含任何新的日志条目,它就作为心跳使用,维持领导者的权威。 - 日志匹配原则:
AppendEntriesRPC 包含PrevLogIndex和PrevLogTerm字段,表示新日志条目紧前一个条目的索引和任期。跟随者在接收到AppendEntries请求时,会检查自己的日志在PrevLogIndex处是否具有与PrevLogTerm相同的任期。如果匹配,则接受新日志;否则,拒绝。这是 Raft 确保日志一致性的关键。 - 强制一致性:如果跟随者拒绝
AppendEntries请求,领导者会递减nextIndex并重试,直到找到一个匹配的日志条目。这个过程会强制跟随者的日志与领导者保持一致。 - 提交 (Commit):当一个日志条目被复制到大多数节点后,领导者就可以安全地将其标记为“已提交”,并将其应用到自己的状态机。随后,领导者会在后续的
AppendEntriesRPC 中告知跟随者最新的commitIndex,跟随者也会将自己的日志应用到状态机。
Go 语言实现:核心逻辑与代码
首先,定义 AppendEntries RPC 的参数和返回值结构。
// AppendEntriesArgs RPC 参数 (心跳和日志复制)
type AppendEntriesArgs struct {
Term int // 领导者的任期
LeaderId int // 领导者的 ID
PrevLogIndex int // 紧前一个新日志条目的索引
PrevLogTerm int // 紧前一个新日志条目的任期
Entries []LogEntry // 要追加的日志条目 (可能为空,用于心跳)
LeaderCommit int // 领导者已知已提交的最高日志条目的索引
}
// AppendEntriesReply RPC 返回值
type AppendEntriesReply struct {
Term int // 接收者当前的任期 (如果领导者任期过时,用于更新领导者)
Success bool // 如果跟随者包含了 PrevLogIndex 和 PrevLogTerm,则为 true
XTerm int // 如果不匹配,冲突日志条目的任期
XIndex int // 如果不匹配,冲突日志条目的第一个索引
XLen int // 如果不匹配,跟随者日志的长度
}
领导者发送 AppendEntries RPC (心跳或日志复制)
sendHeartbeats 方法负责周期性发送心跳,并触发日志复制。
// sendHeartbeats 领导者向所有其他节点发送 AppendEntries RPC (心跳或日志复制)
func (rf *RaftNode) sendHeartbeats() {
rf.mu.Lock()
defer rf.mu.Unlock()
if rf.state != Leader {
return
}
for i := 0; i < len(rf.peers); i++ {
if i == rf.me {
continue
}
go func(server int) {
rf.mu.Lock()
if rf.state != Leader { // 再次检查是否仍是 Leader
rf.mu.Unlock()
return
}
// 构建 AppendEntriesArgs
// prevLogIndex 是 leader 认为 follower 拥有的最后一个日志条目的索引
prevLogIndex := rf.nextIndex[server] - 1
if prevLogIndex < 0 { // 日志从索引 1 开始,所以prevLogIndex不能小于0
prevLogIndex = 0
}
prevLogTerm := rf.Log[prevLogIndex].Term
// 要发送的日志条目
// 注意:这里需要复制 slice,避免并发修改问题
entriesToSend := make([]LogEntry, len(rf.Log[rf.nextIndex[server]:]))
copy(entriesToSend, rf.Log[rf.nextIndex[server]:])
args := AppendEntriesArgs{
Term: rf.CurrentTerm,
LeaderId: rf.me,
PrevLogIndex: prevLogIndex,
PrevLogTerm: prevLogTerm,
Entries: entriesToSend, // 如果是心跳,entriesToSend 为空
LeaderCommit: rf.commitIndex,
}
reply := AppendEntriesReply{}
rf.mu.Unlock() // 在 RPC 调用前释放锁,避免死锁
ok := rf.sendAppendEntries(server, &args, &reply)
if !ok {
return // RPC 调用失败
}
rf.mu.Lock()
defer rf.mu.Unlock()
// 如果当前节点已经不是 Leader,或者任期发生了变化,则忽略此回复
if rf.state != Leader || rf.CurrentTerm != args.Term {
return
}
// 规则 1: 如果收到更高的任期号,立即转换为 Follower
if reply.Term > rf.CurrentTerm {
log.Printf("Node %d: Received AppendEntriesReply with higher term %d from %d. Stepping down to Follower.", rf.me, reply.Term, server)
rf.becomeFollower(reply.Term)
return
}
// 处理 AppendEntries RPC 的回复
if reply.Success {
// 成功复制日志
newMatchIndex := prevLogIndex + len(args.Entries)
newNextIndex := newMatchIndex + 1
if newMatchIndex > rf.matchIndex[server] { // 只有当新匹配索引更大时才更新
rf.matchIndex[server] = newMatchIndex
}
if newNextIndex > rf.nextIndex[server] { // 只有当新下一索引更大时才更新
rf.nextIndex[server] = newNextIndex
}
rf.checkAndCommitLogEntries() // 检查是否可以提交新的日志条目
} else {
// 复制失败,可能是日志不匹配
// 根据 reply 的 XTerm, XIndex, XLen 来优化 nextIndex 的回退
if reply.XTerm != 0 { // Follower 告知了冲突日志的任期
// 查找 Leader 自己的日志中是否存在 XTerm
leaderHasXTerm := false
for i := len(rf.Log) - 1; i > 0; i-- {
if rf.Log[i].Term == reply.XTerm {
rf.nextIndex[server] = i + 1 // 从 Leader 自己的 XTerm 之后开始发送
leaderHasXTerm = true
break
}
}
if !leaderHasXTerm {
// Leader 没有 Follower 冲突任期的日志,直接回退到 XIndex
rf.nextIndex[server] = reply.XIndex
}
} else if reply.XIndex != 0 { // Follower 告知了冲突日志的索引
rf.nextIndex[server] = reply.XIndex
} else { // 简单回退一个条目
rf.nextIndex[server]--
if rf.nextIndex[server] < 1 { // nextIndex 不能小于 1
rf.nextIndex[server] = 1
}
}
// 立即重试发送 AppendEntries RPC
go rf.sendAppendEntriesToPeer(server)
}
}(i)
}
}
// sendAppendEntriesToPeer 辅助函数,用于在日志复制失败时立即重试
func (rf *RaftNode) sendAppendEntriesToPeer(server int) {
rf.mu.Lock()
if rf.state != Leader {
rf.mu.Unlock()
return
}
prevLogIndex := rf.nextIndex[server] - 1
if prevLogIndex < 0 {
prevLogIndex = 0
}
prevLogTerm := rf.Log[prevLogIndex].Term
entriesToSend := make([]LogEntry, len(rf.Log[rf.nextIndex[server]:]))
copy(entriesToSend, rf.Log[rf.nextIndex[server]:])
args := AppendEntriesArgs{
Term: rf.CurrentTerm,
LeaderId: rf.me,
PrevLogIndex: prevLogIndex,
PrevLogTerm: prevLogTerm,
Entries: entriesToSend,
LeaderCommit: rf.commitIndex,
}
reply := AppendEntriesReply{}
rf.mu.Unlock()
ok := rf.sendAppendEntries(server, &args, &reply)
if !ok {
return
}
rf.mu.Lock()
defer rf.mu.Unlock()
if rf.state != Leader || rf.CurrentTerm != args.Term {
return
}
if reply.Term > rf.CurrentTerm {
rf.becomeFollower(reply.Term)
return
}
if reply.Success {
newMatchIndex := prevLogIndex + len(args.Entries)
newNextIndex := newMatchIndex + 1
if newMatchIndex > rf.matchIndex[server] {
rf.matchIndex[server] = newMatchIndex
}
if newNextIndex > rf.nextIndex[server] {
rf.nextIndex[server] = newNextIndex
}
rf.checkAndCommitLogEntries()
} else {
// 再次失败,继续回退
if reply.XTerm != 0 {
leaderHasXTerm := false
for i := len(rf.Log) - 1; i > 0; i-- {
if rf.Log[i].Term == reply.XTerm {
rf.nextIndex[server] = i + 1
leaderHasXTerm = true
break
}
}
if !leaderHasXTerm {
rf.nextIndex[server] = reply.XIndex
}
} else if reply.XIndex != 0 {
rf.nextIndex[server] = reply.XIndex
} else {
rf.nextIndex[server]--
if rf.nextIndex[server] < 1 {
rf.nextIndex[server] = 1
}
}
// 再次重试,避免无限递归,实际实现中可能需要引入退避策略或限速
go rf.sendAppendEntriesToPeer(server)
}
}
// StartCommand 是客户端请求 Raft 节点将命令添加到日志时的入口
func (rf *RaftNode) StartCommand(command interface{}) (index int, term int, isLeader bool) {
rf.mu.Lock()
defer rf.mu.Unlock()
if rf.state != Leader {
return 0, 0, false
}
// 领导者追加新的日志条目到自己的日志
newEntry := LogEntry{Term: rf.CurrentTerm, Command: command}
rf.Log = append(rf.Log, newEntry)
rf.persist() // 持久化日志变更
log.Printf("Node %d: Leader appended command to log at index %d, term %d.", rf.me, len(rf.Log)-1, rf.CurrentTerm)
// 立即向所有其他节点发送 AppendEntries RPC 以复制新条目
// 这会触发 sendHeartbeats 逻辑中的日志复制部分
go rf.sendHeartbeats() // 优化:可以专门一个 goroutine 持续发送AppendEntries
return len(rf.Log) - 1, rf.CurrentTerm, true
}
跟随者处理 AppendEntries RPC
// AppendEntries RPC 处理函数
func (rf *RaftNode) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
reply.Term = rf.CurrentTerm
reply.Success = false
// 规则 1: 如果领导者的任期小于当前节点,拒绝
if args.Term < rf.CurrentTerm {
log.Printf("Node %d: Refusing AppendEntries from %d (term %d) because my term %d is higher.", rf.me, args.LeaderId, args.Term, rf.CurrentTerm)
return
}
// 收到 AppendEntries (包括心跳) 表示存在一个合法的 Leader,重置选举计时器
rf.electionTimer.Reset(rf.getRandomElectionTimeout())
// 规则 2: 如果领导者任期大于等于当前节点,则转换为 Follower
if args.Term > rf.CurrentTerm || rf.state != Follower {
log.Printf("Node %d: Received AppendEntries from %d with higher term %d. Stepping down to Follower.", rf.me, args.LeaderId, args.Term)
rf.becomeFollower(args.Term)
}
// 规则 3: 日志一致性检查
// 如果 PrevLogIndex 超出日志范围,或者 PrevLogIndex 处的任期与 PrevLogTerm 不匹配
if args.PrevLogIndex >= len(rf.Log) {
reply.XTerm = -1 // 表示日志太短
reply.XIndex = len(rf.Log)
reply.XLen = len(rf.Log)
log.Printf("Node %d: Log too short for AppendEntries from %d. PrevLogIndex %d, MyLogLength %d.", rf.me, args.LeaderId, args.PrevLogIndex, len(rf.Log))
return
}
if rf.Log[args.PrevLogIndex].Term != args.PrevLogTerm {
reply.XTerm = rf.Log[args.PrevLogIndex].Term
// 找到 XTerm 对应的第一个索引
for i := 1; i <= args.PrevLogIndex; i++ {
if rf.Log[i].Term == reply.XTerm {
reply.XIndex = i
break
}
}
if reply.XIndex == 0 { // 如果没找到,说明整个日志都和 XTerm 不匹配,从索引 1 开始
reply.XIndex = 1
}
reply.XLen = len(rf.Log)
log.Printf("Node %d: Log inconsistency for AppendEntries from %d. PrevLogIndex %d, PrevLogTerm %d, MyLogTerm %d.", rf.me, args.LeaderId, args.PrevLogIndex, args.PrevLogTerm, rf.Log[args.PrevLogIndex].Term)
return
}
// 规则 4: 追加新日志条目
// 找到 Leader 发送的日志与 Follower 日志首次冲突的地方
// 如果 Follower 日志在该点之后有冲突的条目,则截断
for i, entry := range args.Entries {
index := args.PrevLogIndex + 1 + i
if index < len(rf.Log) {
// 如果现有日志条目与 Leader 发送的条目不匹配,则从该点截断
if rf.Log[index].Term != entry.Term {
rf.Log = rf.Log[:index]
rf.Log = append(rf.Log, entry)
log.Printf("Node %d: Truncated log at index %d and appended new entry.", rf.me, index)
}
} else {
// 如果索引超出范围,直接追加
rf.Log = append(rf.Log, entry)
log.Printf("Node %d: Appended new entry at index %d.", rf.me, index)
}
}
rf.persist() // 持久化日志变更
// 规则 5: 更新 commitIndex
if args.LeaderCommit > rf.commitIndex {
newCommitIndex := min(args.LeaderCommit, len(rf.Log)-1)
if newCommitIndex > rf.commitIndex {
rf.commitIndex = newCommitIndex
go rf.applyEntries() // 异步应用已提交的日志条目
log.Printf("Node %d: Updated commitIndex to %d.", rf.me, rf.commitIndex)
}
}
reply.Success = true
log.Printf("Node %d: Successfully processed AppendEntries from %d. My LogLength: %d, CommitIndex: %d.", rf.me, args.LeaderId, len(rf.Log), rf.commitIndex)
return
}
领导者提交日志条目
// checkAndCommitLogEntries 领导者检查是否可以提交新的日志条目
func (rf *RaftNode) checkAndCommitLogEntries() {
// Raft 论文规则 3: 如果存在一个 N,使得 N > commitIndex,
// 并且大多数节点已经复制了 Log[N] (matchIndex[i] >= N),
// 并且 Log[N].Term == CurrentTerm,那么 Leader 提交 Log[N]。
// 注意:只能提交当前任期内的日志条目
for N := len(rf.Log) - 1; N > rf.commitIndex; N-- {
if rf.Log[N].Term != rf.CurrentTerm {
// 只能提交当前任期内的日志条目
// 这是一个重要的安全规则,防止提交旧任期中未完全复制的日志
continue
}
count := 1 // 领导者自己也复制了 Log[N]
for i := 0; i < len(rf.peers); i++ {
if i == rf.me {
continue
}
if rf.matchIndex[i] >= N {
count++
}
}
if count > len(rf.peers)/2 {
rf.commitIndex = N
log.Printf("Node %d: Leader committed log entry at index %d (term %d).", rf.me, N, rf.Log[N].Term)
go rf.applyEntries() // 异步应用已提交的日志条目
break // 从高到低遍历,找到最大的可提交索引后即可退出
}
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
日志复制的“生死时刻”在于确保所有节点最终收敛于相同的日志序列。Raft 通过 AppendEntries RPC 及其严格的日志匹配和提交规则,有效地解决了这个问题。领导者通过 nextIndex 和 matchIndex 跟踪每个跟随者的进度,并在必要时回退其 nextIndex 以强制日志一致。
第三个“生死时刻”:状态机应用与持久化 —— 如何将决策转化为持久成果?
Raft 协议的最终目标是构建一个高可用、强一致的分布式状态机。这意味着,所有已提交的日志条目必须以相同的顺序应用到每个节点上的应用程序状态机,并且 Raft 节点自身的关键状态(如 CurrentTerm, VotedFor, Log)必须能持久化存储,以应对节点崩溃重启。这个时刻的“生死”在于,如果不能正确地应用日志和持久化状态,那么 Raft 提供的强一致性就只是一纸空文,系统将无法正确地维护和恢复其状态。
问题挑战
- 状态机应用顺序:已提交的日志条目必须严格按照它们在日志中的顺序应用到状态机。
- 幂等性:应用程序命令应该是幂等的,以防日志条目被重复应用(虽然 Raft 协议本身会确保只应用一次)。
- 持久化:Raft 节点在崩溃前必须将所有关键状态持久化到稳定的存储介质上,以便重启后能恢复到崩溃前的状态。
- 性能:频繁的持久化操作可能影响性能,需要权衡。
Raft 的解决方案
- 提交后应用:只有当日志条目被标记为“已提交”后,才会被异步地应用到状态机。
- 单一应用通道:所有已提交的日志条目通过一个统一的通道 (
applyCh) 传递给上层应用程序,确保应用顺序。 - 原子性持久化:Raft 的核心状态 (
CurrentTerm,VotedFor,Log) 必须在状态变更后立即持久化。通常,这些状态会作为一个整体进行序列化和写入。 - 快照 (Snapshot):为了避免日志无限增长,Raft 引入了快照机制。节点会定期对已提交的状态机状态进行快照,然后丢弃快照点之前的日志。这不是核心协议的一部分,但对于生产系统至关重要。
Go 语言实现:核心逻辑与代码
状态机应用
// applyEntries 将已提交的日志条目应用到状态机
func (rf *RaftNode) applyEntries() {
rf.mu.Lock()
defer rf.mu.Unlock()
// 持续将 lastApplied 到 commitIndex 之间的日志条目应用到状态机
// 这是一个单独的 goroutine 异步执行,避免阻塞 Raft 主逻辑
for rf.lastApplied < rf.commitIndex {
rf.lastApplied++
entry := rf.Log[rf.lastApplied]
applyMsg := ApplyMsg{
CommandValid: true,
Command: entry.Command,
CommandIndex: rf.lastApplied,
CommandTerm: entry.Term,
}
// 通过 applyCh 将命令发送给上层应用
// 注意:这里不能在持有锁的情况下向 applyCh 发送,否则可能死锁
// 解决方法是:将要发送的消息收集起来,在释放锁后发送,或者在单独的 goroutine 中处理
// 为了简洁,这里直接发送,但在实际生产代码中,可能需要更精细的同步机制
rf.mu.Unlock() // 临时释放锁,允许 applyCh 阻塞
rf.applyCh <- applyMsg
rf.mu.Lock() // 重新获取锁
log.Printf("Node %d: Applied log entry at index %d, term %d.", rf.me, rf.lastApplied, entry.Term)
}
}
持久化存储
我们将 RaftState 结构体用于存储所有需要持久化的 Raft 状态。
// Persister 存储 Raft 状态和快照
type Persister struct {
mu sync.Mutex
raftstate []byte
snapshot []byte
}
// SaveRaftState 将 Raft 状态持久化
func (ps *Persister) SaveRaftState(state []byte) {
ps.mu.Lock()
defer ps.mu.Unlock()
ps.raftstate = state
}
// ReadRaftState 读取持久化的 Raft 状态
func (ps *Persister) ReadRaftState() []byte {
ps.mu.Lock()
defer ps.mu.Unlock()
return ps.raftstate
}
// SaveStateAndSnapshot 将 Raft 状态和快照持久化
func (ps *Persister) SaveStateAndSnapshot(state []byte, snapshot []byte) {
ps.mu.Lock()
defer ps.mu.Unlock()
ps.raftstate = state
ps.snapshot = snapshot
}
// ReadSnapshot 读取持久化的快照
func (ps *Persister) ReadSnapshot() []byte {
ps.mu.Lock()
defer ps.mu.Unlock()
return ps.snapshot
}
// RaftStateSize 返回持久化的 Raft 状态的大小
func (ps *Persister) RaftStateSize() int {
ps.mu.Lock()
defer ps.mu.Unlock()
return len(ps.raftstate)
}
// persist 将 Raft 节点的状态持久化到存储中
func (rf *RaftNode) persist() {
w := new(bytes.Buffer)
e := gob.NewEncoder(w)
e.Encode(rf.CurrentTerm)
e.Encode(rf.VotedFor)
e.Encode(rf.Log)
data := w.Bytes()
rf.persister.SaveRaftState(data)
}
// readPersist 从持久化存储中读取 Raft 节点的状态
func (rf *RaftNode) readPersist(data []byte) {
if data == nil || len(data) < 1 { // bootstrap without any state?
return
}
r := bytes.NewBuffer(data)
d := gob.NewDecoder(r)
var currentTerm int
var votedFor int
var logEntries []LogEntry
if d.Decode(¤tTerm) != nil ||
d.Decode(&votedFor) != nil ||
d.Decode(&logEntries) != nil {
log.Fatalf("Error decoding Raft state.")
} else {
rf.CurrentTerm = currentTerm
rf.VotedFor = votedFor
rf.Log = logEntries
}
// 恢复后需要确保 commitIndex 和 lastApplied 不会超出日志范围
// 它们在崩溃前未被持久化,因为它们是易失性状态,但会根据持久化的日志重新计算或初始化
rf.commitIndex = 0
rf.lastApplied = 0
log.Printf("Node %d: Recovered from persistence. Term: %d, VotedFor: %d, LogLength: %d.", rf.me, rf.CurrentTerm, rf.VotedFor, len(rf.Log))
}
何时进行持久化?
Raft 协议规定,每次 CurrentTerm、VotedFor 或 Log 发生变化时,都必须调用 persist() 方法。
CurrentTerm变化:在startElection中增加CurrentTerm,以及在RequestVote或AppendEntries中发现更高任期时更新CurrentTerm。VotedFor变化:在startElection中给自己投票,以及在RequestVote中投票给其他候选者时。Log变化:在领导者StartCommand追加新日志时,以及跟随者在AppendEntries中追加或截断日志时。
通过将日志条目以异步方式应用到状态机,并在关键状态变更时进行严格的持久化,Raft 协议确保了即使在节点崩溃重启后,系统也能恢复到一致的状态,并继续提供服务。这三个“生死时刻”环环相扣,共同构建了 Raft 协议的强大一致性保证。
三个“生死时刻”的精妙协同
领导者选举、日志复制和状态机应用与持久化,这三个“生死时刻”并非孤立存在,而是紧密相连、相互依赖的。
- 选举是复制的前提:没有选举出领导者,就没有日志复制的协调者。
- 复制是应用的基础:只有日志被安全地复制并提交,才能被应用到状态机。
- 持久化是容错的保障:所有关键状态的持久化,是 Raft 节点能够从故障中恢复并继续参与选举和复制的前提。
例如,在选举过程中,RequestVote RPC 会检查候选者的日志完整性,确保新的领导者拥有所有已提交的日志,这就是为了保证日志复制的“领导者完全性”原则。在日志复制过程中,跟随者收到 AppendEntries RPC 时,如果发现领导者的任期小于自己的任期,会直接拒绝并告知领导者回退,这体现了任期机制在维护一致性中的核心作用。而当日志条目被成功复制到大多数节点并被提交后,它们才会被安全地传递给状态机进行应用,并通过持久化确保应用结果不会丢失。
Raft 协议的美妙之处在于,它将这些复杂的问题分解,并通过简单、明确的规则将它们连接起来,形成一个强大而易于理解的分布式一致性算法。在 Go 语言中实现 Raft,我们利用 Go 的并发原语(goroutines, channels)和简洁的语法,能够清晰地表达这些规则和状态转换,构建出高效且健壮的分布式一致性引擎。
结语
分布式一致性是构建可靠分布式系统的核心挑战。Raft 协议以其清晰的逻辑和易于实现的特点,成为了 Paxos 之外的有力替代方案。通过深入理解并妥善处理领导者选举、日志复制以及状态机应用与持久化这三个“生死时刻”,我们不仅能够成功地在 Go 中实现 Raft 协议的核心引擎,更能掌握分布式系统设计与实现的关键原则,为构建更强大、更容错的分布式应用奠定基础。希望今天的分享能为大家在分布式系统领域的探索提供一些有益的启发。