枯木逢春不在茂
年少且惜镜边人
写在前面
lab2B已经完成的raft的大部分内容,2C主要是对currentterm,[]log,voteFor 进行持久化存储,后边3B也会修改这一部分的代码,还有就是2B中提到的心跳被拒后回退机制太慢,需要优化。
具体实现
首先是持久化函数,lab中给出的代码可以直接套用
// save Raft's persistent state to stable storage,
// where it can later be retrieved after a crash and restart.
// see paper's Figure 2 for a description of what should be persistent.
//
func (rf *Raft) persist() {
// Your code here (2C).
// 不用加锁,外层逻辑会锁
w := new(bytes.Buffer)
e := labgob.NewEncoder(w)
e.Encode(rf.currentTerm)
e.Encode(rf.votedFor)
e.Encode(rf.log)
data := w.Bytes()
DPrintf("RaftNode[%d] persist starts, currentTerm[%d] voteFor[%d] log[%v]", rf.me, rf.currentTerm, rf.votedFor, rf.log)
rf.persister.SaveRaftState(data)
}
//
// restore previously persisted state.
//
func (rf *Raft) readPersist(data []byte) {
if data == nil || len(data) < 1 { // bootstrap without any state?
return
}
// Your code here (2C).
r := bytes.NewBuffer(data)
d := labgob.NewDecoder(r)
rf.mu.Lock()
defer rf.mu.Unlock()
d.Decode(&rf.currentTerm)
d.Decode(&rf.votedFor)
d.Decode(&rf.log)
}
type AppendEntriesArgs struct {
Term int
LeaderId int
PrevLogIndex int
PrevLogTerm int
Entries []LogEntry
LeaderCommit int
}
type AppendEntriesReply struct {
Term int
Success bool
ConflictIndex int
ConflictTerm int
}
也就是增加了ConflictIndex和ConflictTerm这两个字段
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
DPrintf("RaftNode[%d] Handle AppendEntries, LeaderId[%d] Term[%d] CurrentTerm[%d] role=[%s] logIndex[%d] prevLogIndex[%d] prevLogTerm[%d] commitIndex[%d] Entries[%v]",
rf.me, rf.leaderId, args.Term, rf.currentTerm, rf.role, len(rf.log), args.PrevLogIndex, args.PrevLogTerm, rf.commitIndex, args.Entries)
reply.Term = rf.currentTerm
reply.Success = false
reply.ConflictIndex = -1
reply.ConflictTerm = - 1
defer func() {
DPrintf("RaftNode[%d] Return AppendEntries, LeaderId[%d] Term[%d] CurrentTerm[%d] role=[%s] logIndex[%d] prevLogIndex[%d] prevLogTerm[%d] Success[%v] commitIndex[%d] log[%v] ConflictIndex[%d]",
rf.me, rf.leaderId, args.Term, rf.currentTerm, rf.role, len(rf.log), args.PrevLogIndex, args.PrevLogTerm, reply.Success, rf.commitIndex, rf.log, reply.ConflictIndex)
}()
if args.Term < rf.currentTerm {
return
}
// 发现更大的任期,则转为该任期的follower
if args.Term > rf.currentTerm {
rf.currentTerm = args.Term
rf.role = ROLE_FOLLOWER
rf.votedFor = -1
rf.leaderId = -1
rf.persist()
// 继续向下走
}
// 认识新的leader
rf.leaderId = args.LeaderId
// 刷新活跃时间
rf.lastActiveTime = time.Now()
// appendEntries RPC , receiver 2)
// 如果本地没有前一个日志的话,那么false
if len(rf.log) < args.PrevLogIndex {
reply.ConflictIndex = len(rf.log)
return
}
// 如果本地有前一个日志的话,那么term必须相同,否则false
if args.PrevLogIndex > 0 && rf.log[args.PrevLogIndex - 1].Term != args.PrevLogTerm {
reply.ConflictTerm = rf.log[args.PrevLogIndex - 1].Term
for index := 1; index <= args.PrevLogIndex; index++ { // 找到冲突term的首次出现位置,最差就是PrevLogIndex
if rf.log[index - 1].Term == reply.ConflictTerm {
reply.ConflictIndex = index
break
}
}
return
}
// 保存日志
for i, logEntry := range args.Entries {
index := args.PrevLogIndex + i + 1
if index > len(rf.log) {
rf.log = append(rf.log, logEntry)
} else { // 重叠部分
if rf.log[index - 1].Term != logEntry.Term {
rf.log = rf.log[:index - 1] // 删除当前以及后续所有log
rf.log = append(rf.log, logEntry) // 把新log加入进来
} // term一样啥也不用做,继续向后比对Log
}
}
rf.persist()
// 更新提交下标
if args.LeaderCommit > rf.commitIndex {
rf.commitIndex = args.LeaderCommit
if len(rf.log) < rf.commitIndex {
rf.commitIndex = len(rf.log)
}
}
reply.Success = true
}
这个比2B增加的代码如下
if len(rf.log) < args.PrevLogIndex {
reply.ConflictIndex = len(rf.log)
return
}
// 如果本地有前一个日志的话,那么term必须相同,否则false
if args.PrevLogIndex > 0 && rf.log[args.PrevLogIndex - 1].Term != args.PrevLogTerm {
reply.ConflictTerm = rf.log[args.PrevLogIndex - 1].Term
for index := 1; index <= args.PrevLogIndex; index++ { // 找到冲突term的首次出现位置,最差就是PrevLogIndex
if rf.log[index - 1].Term == reply.ConflictTerm {
reply.ConflictIndex = index
break
}
}
return
}
分别对两种情况进行了优化
- 如果本地log小于leader的log则直接返回本地log的最后一个索引,告诉leader从这个索引出发送
- 如果本地有该日志条目但是周期不同的时候,找出本地日志中该周期的第一个产生矛盾的索引,因为该周期对于leader发送周期无效
func (rf *Raft) appendEntriesLoop() {
for !rf.killed() {
time.Sleep(10 * time.Millisecond)
func() {
rf.mu.Lock()
defer rf.mu.Unlock()
// 只有leader才向外广播心跳
if rf.role != ROLE_LEADER {
return
}
// 100ms广播1次
now := time.Now()
if now.Sub(rf.lastBroadcastTime) < 100*time.Millisecond {
return
}
rf.lastBroadcastTime = time.Now()
// 并发RPC心跳
type AppendResult struct {
peerId int
resp *AppendEntriesReply
}
for peerId := 0; peerId < len(rf.peers); peerId++ {
if peerId == rf.me {
continue
}
args := AppendEntriesArgs{}
args.Term = rf.currentTerm
args.LeaderId = rf.me
args.LeaderCommit = rf.commitIndex
args.Entries = make([]LogEntry, 0)
args.PrevLogIndex = rf.nextIndex[peerId] - 1
if args.PrevLogIndex > 0 {
args.PrevLogTerm = rf.log[args.PrevLogIndex - 1].Term
}
args.Entries = append(args.Entries, rf.log[rf.nextIndex[peerId]-1:]...)
DPrintf("RaftNode[%d] appendEntries starts, currentTerm[%d] peer[%d] logIndex=[%d] nextIndex[%d] matchIndex[%d] args.Entries[%d] commitIndex[%d]",
rf.me, rf.currentTerm, peerId, len(rf.log), rf.nextIndex[peerId], rf.matchIndex[peerId], len(args.Entries), rf.commitIndex)
// log相关字段在lab-2A不处理
go func(id int, args1 *AppendEntriesArgs) {
// DPrintf("RaftNode[%d] appendEntries starts, myTerm[%d] peerId[%d]", rf.me, args1.Term, id)
reply := AppendEntriesReply{}
if ok := rf.sendAppendEntries(id, args1, &reply); ok {
rf.mu.Lock()
defer rf.mu.Unlock()
defer func() {
DPrintf("RaftNode[%d] appendEntries ends, currentTerm[%d] peer[%d] logIndex=[%d] nextIndex[%d] matchIndex[%d] commitIndex[%d]",
rf.me, rf.currentTerm, id, len(rf.log), rf.nextIndex[id], rf.matchIndex[id], rf.commitIndex)
}()
// 如果不是rpc前的leader状态了,那么啥也别做了
if rf.currentTerm != args1.Term {
return
}
if reply.Term > rf.currentTerm { // 变成follower
rf.role = ROLE_FOLLOWER
rf.leaderId = -1
rf.currentTerm = reply.Term
rf.votedFor = -1
rf.persist()
return
}
// 因为RPC期间无锁, 可能相关状态被其他RPC修改了
// 因此这里得根据发出RPC请求时的状态做更新,而不要直接对nextIndex和matchIndex做相对加减
if reply.Success { // 同步日志成功
rf.nextIndex[id] = args1.PrevLogIndex + len(args1.Entries) + 1
rf.matchIndex[id] = rf.nextIndex[id] - 1
// 数字N, 让peer[i]的大多数>=N
// peer[0]' index=2
// peer[1]' index=2
// peer[2]' index=1
// 1,2,2
// 更新commitIndex, 就是找中位数
sortedMatchIndex := make([]int, 0)
sortedMatchIndex = append(sortedMatchIndex, len(rf.log))
for i := 0; i < len(rf.peers); i++ {
if i == rf.me {
continue
}
sortedMatchIndex = append(sortedMatchIndex, rf.matchIndex[i])
}
sort.Ints(sortedMatchIndex)
newCommitIndex := sortedMatchIndex[len(rf.peers) / 2]
if newCommitIndex > rf.commitIndex && rf.log[newCommitIndex - 1].Term == rf.currentTerm {
rf.commitIndex = newCommitIndex
}
} else {
nextIndexBefore := rf.nextIndex[id] // 仅为打印log
if reply.ConflictTerm != -1 { // follower的prevLogIndex位置term不同
conflictTermIndex := -1
for index := args1.PrevLogIndex; index >= 1; index-- { // 找最后一个conflictTerm
if rf.log[index - 1].Term == reply.ConflictTerm {
conflictTermIndex = index
break
}
}
if conflictTermIndex != -1 { // leader也存在冲突term的日志,则从term最后一次出现之后的日志开始尝试同步,因为leader/follower可能在该term的日志有部分相同
rf.nextIndex[id] = conflictTermIndex + 1
} else { // leader并没有term的日志,那么把follower日志中该term首次出现的位置作为尝试同步的位置,即截断follower在此term的所有日志
rf.nextIndex[id] = reply.ConflictIndex
}
} else { // follower的prevLogIndex位置没有日志
rf.nextIndex[id] = reply.ConflictIndex + 1
}
DPrintf("RaftNode[%d] back-off nextIndex, peer[%d] nextIndexBefore[%d] nextIndex[%d]", rf.me, id, nextIndexBefore, rf.nextIndex[id])
}
}
}(peerId, &args)
}
}()
}
}
相比于2B该部分多了这些代码
nextIndexBefore := rf.nextIndex[id] // 仅为打印log
if reply.ConflictTerm != -1 { // follower的prevLogIndex位置term不同
conflictTermIndex := -1
for index := args1.PrevLogIndex; index >= 1; index-- { // 找最后一个conflictTerm
if rf.log[index - 1].Term == reply.ConflictTerm {
conflictTermIndex = index
break
}
}
if conflictTermIndex != -1 { // leader也存在冲突term的日志,则从term最后一次出现之后的日志开始尝试同步,因为leader/follower可能在该term的日志有部分相同
rf.nextIndex[id] = conflictTermIndex + 1
} else { // leader并没有term的日志,那么把follower日志中该term首次出现的位置作为尝试同步的位置,即截断follower在此term的所有日志
rf.nextIndex[id] = reply.ConflictIndex
}
} else { // follower的prevLogIndex位置没有日志
rf.nextIndex[id] = reply.ConflictIndex + 1
}
DPrintf("RaftNode[%d] back-off nextIndex, peer[%d] nextIndexBefore[%d] nextIndex[%d]", rf.me, id, nextIndexBefore, rf.nextIndex[id])
将原来的-1回退优化了一些
这也分为两个条件
- 当矛盾周期是-1的时候那就说明follow的日志没有进行判断矛盾周期就直接返回,那就说明只是他的日志长度不够,那么直接发送矛盾周期的下一个,具体可以结合心跳处理函数进行参考
- 如果矛盾周期不是-1,那就说明日志长度保证,但是周期不一致导致错误,那这个时候,在leader的日志中找出最后一个出现矛盾的周期索引,如果没有该矛盾周期,那么直接截断peer的日志,这也就是peer为什么要找出该矛盾周期的第一个索引了,如果有该矛盾的周期,那么从最后往前发,这样做的原因就是peer中可能已经存在该周期一些已经存在的条目。
写在后面
需要注意的问题
- 回退优化原理:如果follower日志比leader短,那么leader可以直接从follower末尾的index开始尝试传日志,只有这样follower日志才不会出现空槽的情况;否则follower在prevLogIndex位置冲突的term,如果在leader中也有这个term的日志,则从leader日志中该term最后一次出现的位置开始尝试同步,避免给follower错过这个term的任何一条日志;如果冲突term在leader里压根不在,则从follower日志该term首次出现的下标开始同步,因为leader压根没有这个term的日志,相当于对follower截断。
- leader可能收到旧的appendEntries RPC应答,因此leader收到RPC应答时重新加锁后,应该注意检查currentTerm是否和RPC时的term一样,另外也不应该对nextIndex直接做-=1这样的相对计算(因为旧RPC应答之前可能新RPC已经应答并且修改了nextIndex),而是应该用RPC时的prevLogIndex等信息做绝对计算,这样是不会有问题的