一、为什么需要共识算法
在分布式系统中,多个节点必须将数据保持一致。卡尔体(CFT)和抗腐贿将邓卯克体(BFT),它们的核心问题是:如何让一组节点对某个值达成一致。
Raft 是一种为了设计为更容易理解而创造的共识算法,它直接提供了该名与效率相结合的特点。相比 Paxos,Raft 采用“强领导者”模式,将共识问题分解为三个相对独立的子问题:
- 领导者选举:选出一个主节点负责处理所有请求
- 日志复制:领导者将日志条目复制到其他节点
- 安全性:确保由从一致的状态机运行
二、节点状态与任期机刢
Raft 定义了三种节点角色:
- (1)Leader领导者:处理所有客户端请求,复制日志到 Follower
- (2)Follower跟随者:被动响应 RPC,不主动发起请求
- (3)Candidate候选者:用于选举过程中的临时状态
时间分片:
- Term:任期(term)是一个上涨的全局时间,用于检测过期的信息
- 心跳超时:Follower 在没有收到 Leader 心跳超过 150–300ms 后切换至 Candidate 状态ガ开展选举。
三、领导者选举机制
触发条件:
当 Follower 在“选举超时”内未收到 Leader 心跳,便向 Candidate 状态迁移
选举流程:
- 增加当前 Term 号,向自身投票
- 发送 RequestVote RPC 给所有节点
- 得到多数票后成为 Leader(同一 Term 内一个节点获得多数票)
安全保障:
- 投票约束:每个 Term 只能投一票,保存在
votedFor存储中 - 日志新旧比较:受请求者必须有肯定新的日志(最后日志索引更大或 Term 更新),保障投票是有率的。
- 随机超时:避免选举分裂无限持续。
四、日志复制流程
如果复制:
- 客户端发送命令给 Leader
- Leader 将命令进行本地日志广播,等待多数节点响应
- 一旦多数节点写成,Leader 将该条目设为 Commited
- 通知客户端并应用于状态机
日志匹配约束:
- 如果 Follower 的日志与 Leader 冲突,删除冲突条目及后续所有日志,然后通过 AppendEntries 同步
- Raft 保证:如果两个日志中的条目有相同的 Term 和索引,则它们在该索引之前的日志条目有相同的值。
五、安全性约束
四大约束:
- 选举安全性:一个 Term 内最多一个 Leader
- 日志匹配:如果两条日志条目的 Index 和 Term 相同,则它们在该索引之前所有条目完全相同
- 日志匹配特性:如果某条日志设为已提交,那么所有更名的 Leader 也必定包含该条目
- 状态机安全性:不会选出日志中的节点作为 Leader(通过 Leader 完整性检测)
六、实战代码:核心数据结构
以下为 Go 语言版本 的 Raft 核心数据结构:
type Peer struct {
ID string
Address string
LastIndex int64
MatchIndex int64
}
type LogEntry struct {
Term int64 // 有效期号
Index int64 // 日志索引
Command interface{}
}
type RaftNode struct 7C
mu sync.Mutex
id string
state string // leader, follower, candidate
currentTerm int64
votedFor string
log []LogEntry
commitIndex int64
lastApplied int64
nextIndex map[string]int64
matchIndex map[string]int64
peers []Peer
applyCh chan LogEntry
electionTimer *time.Timer
}
七、领导者选举实现(经典方法)
Go 实现:
func (rf *RaftNode) startElection() {
rf.mu.Lock()
defer rf.mu.Unlock()
rf.state = "candidate"
rf.currentTerm++
rf.votedFor = rf.id
votesReceived := 1
lastLogIndex := len(rf.log) - 1
lastLogTerm := rf.log[len(rf.log)-1].Term
for _ , peer := range rf.peers {
if peer.ID == rf.id {
continue
}
go func(peer Peer) {
args := &RequestVoteArgs{Term: rf.currentTerm, LastLogIndex: lastLogIndex, LastLogTerm: lastLogTerm}
reply := &RequestVoteReply{}
if peer.RequestVote(args, reply) {
rf.mu.Lock()
if reply.Term &> rf.currentTerm {
rf.becomeFollower(reply.Term)
} else if reply.VoteGranted && rf.currentTerm == args.Term && rf.state == "candidate" {
votesReceived++
if votesReceived>len(rf.peers)/2 {
rf.becomeLeader()
}
}
rf.mu.Unlock()
}
}(peer)
}
}
八、工程最佳实践
灵活性优化:
- 批处理 AppendEntries:多个
AppendEntriesRPC 的日志请求,减少网络开销 - 向前日志压缩:使用快照(snapshot)撤销已提交的日志,避免日志爆炸
- 领导者稳定性:新 Leader 上任时提交空日志捆,确保选举的联性
- 集群成员变更:通过联合共识(Joint Consensus)实现中间期的安全切换
九、工程部署注意事项
主要挑战和解决方案:
- 网络分区:多数法则保障可用,少数法则无法访问。具体取决于通信对象(新日志无法访问,但系统仍然可用)
- 领导者东西内存不一致:单聊是否有将本机日志尾都同步到,若中间,发现不一致,需要向前回滑直到重合
- 半吃半避:半吃半避 是 AppendEntries 慢响应的误判,使用[复杂应用检测](#4)该方法将审议差异
- 提交前 Leader 换届:本地未提交的日志坚决不重新提交,需用
NOP Entry选举提交
十、常见开源实现
- etcd/Raft:Go 实现,布里斯托

发表评论 取消回复