Raft中的Log Entry包括以下内容
索引 Index 记录该日志条目在整个日志中的位置
任期号 Term 日志条目首次被领导者创建时的任期
命令 应用于状态机的命令
日志复制
Raft算法通过索引和任期号唯一标识一条日志记录
日志必须持久化存储,一个节点必须先把日志条目安全写入到磁盘中,才能向系统其他节点发送请求或回复请求。
如果一条日志条目被存储到超过半数节点上,则认为该记录已提交(committed)。如果一条记录已提交,状态机可以安全地执行该记录,这条记录就不能再改变了。
如图,第1条到第7条日志已提交,第8条未提交。

Raft通过AppendEntries消息复制日志,与心跳消息共用同一个RPC,不过用于心跳时不附加日志信息。
Raft算法正常运行时,日志复制流程为:
(1) 客户端向领导者发送命令
(2) 领导者先将命令追加到自己的日志中,确保日志持久化存储
(3) 领导者向其他节点发送AppendEntries消息,等待相应
(4) 如果收到超过半数节点响应,则认为新的日志记录已提交。接着领导者将命令应用(apply)到自己的状态机,然后向客户端返回响应。此外,一旦领导者提交了一个日志记录,将在后续的AppendEntries消息中通过LeaderCommit参数通知跟随者,告知领导者已提交的最大日志索引,跟随者也提交索引小于LeaderCommit的日志,并将命令应用到自己的状态机。
(5) 如果跟随者宕机或请求超时,日志没有复制成功,那么领导者将反复尝试发送AppendEntries消息。
(6) 性能优化:领导者不必等待所有跟随者做出响应,只要半数的跟随者成功响应(日志已经存储到超过半数的节点上)就可以回复客户端了。这样保证即使存在一个很慢的或故障节点,也不会成为系统的瓶颈。
Raft算法的日志通过索引和任期号唯一表示一个日志条目,为了保证安全性,Raft维持以下两个特性:
(1) 如果两个节点的日志在相同的索引位置上的任期号相同,则认为它们具有一样的命令,并且从日志开头到这个索引位置之间的日志也完全相同。
(2) 如果给定的日志已提交,那么之前所有的日志都已提交 .

一致性保证
Raft通过AppendEntries消息来检测之前的一个日志条目,每个AppendEntries请求包含新的日志条目之前的一个日志条目的索引和任期(记为prevLogIndex和prevLogTerm),追随者收到请求后,会检查自己最后一条日志的索引和任期号是否与请求消息中的prevLogIndex和prevLogTerm相同,相同则接收记录,否则拒绝。
可以通过数学归纳法证明以上方式能保证AppendEntries消息之前的日志都是相同的。
代码框架
package go_raft
type LogEntry struct {
Index int
Term int
Command interface{}
}
type AppendEntriesArgs struct {
Term int
LeaderId int
PrevLogIndex int
PrevLogTerm int
//需要复制的日志条目,发送心跳时为空
Entries []LogEntry
//领导者已提交最大日志索引,用于跟随着提交
LeaderCommit int
}
type AppendEntriesReply struct {
Term int
Success bool
}
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
reply.Term = rf.currentTerm
reply.Success = false
if args.Term < rf.currentTerm {
return
}
if args.Term > rf.currentTerm {
reply.Term = args.Term
rf.currentTerm = args.Term
}
//重置选举超时时间
rf.setState(Follower)
//日志一致性检查
lastLogIndex := len(rf.log) - 1
if args.PrevLogIndex > rf.log[lastLogIndex].Index || rf.log[args.PrevLogIndex].Term != args.PrevLogTerm {
return
}
reply.Success = true
//处理重复的RPC请求
//比较日志条目的任期,以确认是否能够安全地追加日志
//否则会导致重复应用命令
index := args.PrevLogIndex
for i, entry := range args.Entries {
index++
if index < len(rf.log) {
if rf.log[index].Term == entry.Term {
continue
}
rf.log = rf.log[:index]
}
rf.log = append(rf.log, args.Entries[i:]...)
break
}
if rf.commitIndex < args.LeaderCommit {
lastLogIndex = rf.log[len(rf.log)-1].Index
if args.LeaderCommit > lastLogIndex {
rf.commitIndex = lastLogIndex
} else {
rf.commitIndex = args.LeaderCommit
}
//将命令应用到自己的状态机
rf.apply()
}
//保险起见,再重置一次选举超时时间
rf.setState(Follower)
return
}
本文详细阐述了Raft协议中的LogEntry组成,包括索引、任期号和命令。重点讲解了日志复制过程以及如何通过AppendEntries消息确保一致性,强调了索引和任期号在保证数据安全性和正确性中的关键作用。
3301

被折叠的 条评论
为什么被折叠?



