MIT6.824(lab2C-日志持久化)

枯木逢春不在茂
年少且惜镜边人

写在前面

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
	}

分别对两种情况进行了优化

  1. 如果本地log小于leader的log则直接返回本地log的最后一个索引,告诉leader从这个索引出发送
  2. 如果本地有该日志条目但是周期不同的时候,找出本地日志中该周期的第一个产生矛盾的索引,因为该周期对于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. 当矛盾周期是-1的时候那就说明follow的日志没有进行判断矛盾周期就直接返回,那就说明只是他的日志长度不够,那么直接发送矛盾周期的下一个,具体可以结合心跳处理函数进行参考
  2. 如果矛盾周期不是-1,那就说明日志长度保证,但是周期不一致导致错误,那这个时候,在leader的日志中找出最后一个出现矛盾的周期索引,如果没有该矛盾周期,那么直接截断peer的日志,这也就是peer为什么要找出该矛盾周期的第一个索引了,如果有该矛盾的周期,那么从最后往前发,这样做的原因就是peer中可能已经存在该周期一些已经存在的条目。

写在后面

需要注意的问题

  1. 回退优化原理:如果follower日志比leader短,那么leader可以直接从follower末尾的index开始尝试传日志,只有这样follower日志才不会出现空槽的情况;否则follower在prevLogIndex位置冲突的term,如果在leader中也有这个term的日志,则从leader日志中该term最后一次出现的位置开始尝试同步,避免给follower错过这个term的任何一条日志;如果冲突term在leader里压根不在,则从follower日志该term首次出现的下标开始同步,因为leader压根没有这个term的日志,相当于对follower截断。
  2. leader可能收到旧的appendEntries RPC应答,因此leader收到RPC应答时重新加锁后,应该注意检查currentTerm是否和RPC时的term一样,另外也不应该对nextIndex直接做-=1这样的相对计算(因为旧RPC应答之前可能新RPC已经应答并且修改了nextIndex),而是应该用RPC时的prevLogIndex等信息做绝对计算,这样是不会有问题的
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值