MIT6.824(LAB2B-日志一致性检查)

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

写在前面

emmmmmmmmmmmmmmmmmmm,想了想没什么说的,说说lab2b,写lab2B之前我觉得应该先看一遍raft论文,当然两边更好,三遍更好好。

实现过程

也是MAKE函数打头阵

func Make(peers []*labrpc.ClientEnd, me int,
	persister *Persister, applyCh chan ApplyMsg) *Raft {
	rf := &Raft{}
	rf.peers = peers
	rf.persister = persister
	rf.me = me

	// Your initialization code here (2A, 2B, 2C).
	rf.role = ROLE_FOLLOWER
	rf.leaderId = -1
	rf.votedFor = -1
	rf.lastActiveTime = time.Now()

	// initialize from state persisted before a crash
	rf.readPersist(persister.ReadRaftState())

	// election逻辑
	go rf.electionLoop()
	// leader逻辑
	go rf.appendEntriesLoop()
	// apply逻辑
	go rf.applyLogLoop(applyCh)

	DPrintf("Raftnode[%d]启动", me)

	return rf
}

新添加了apply的逻辑。其他的基本也没变,之后变得最大的就是心跳机制了

func (rf *Raft) Start(command interface{}) (int, int, bool) {
	index := -1
	term := -1
	isLeader := true
 
	// Your code here (2B).
	rf.mu.Lock()
	defer rf.mu.Unlock()
	// 只有leader才能写入
	if rf.role != ROLE_LEADER {
		return -1, -1, false
	}
 
	logEntry := LogEntry{
		Command: command,
		Term:    rf.currentTerm,
	}
	rf.log = append(rf.log, logEntry)
	index = len(rf.log)
	term = rf.currentTerm
	rf.persist()
 
	DPrintf("RaftNode[%d] Add Command, logIndex[%d] currentTerm[%d]", rf.me, index, term)
	return index, term, isLeader
}

start函数会在3A的服务器端进行调用,进行日志的存储,然后进入一致性复制

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
						}
						if reply.Success {	// 同步日志成功
							rf.nextIndex[id] += len(args1.Entries)
							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
							}
							// rf.commitIndex = minMatchIndex
						} else {
							rf.nextIndex[id] -= 1
							if rf.nextIndex[id] < 1 {
								rf.nextIndex[id] = 1
							}
						}
					}
				}(peerId, &args)
			}
		}()
	}
}

我们来看看这个心跳处理机制比之前多了什么处理过程
心跳机制不再是发送一个空的请求,而是带有领导人前一条日志和前一条日志的周期去检查peer的一致性,并且带有领导人已经提交的最大日志索引,然后就是发送RPC请求,看看响应部分做了那些处理
周期检查都和之前的逻辑一样

  1. 如果同步成功,则修改nextindex,和matchindex,分别表示发送给该peer的下一条日志的索引,和peer的最大已经复制日志的索引,不出意外的话,matchindex=nextindex-1
  2. 然后就是提交日志了,看看这个日志是不是被集群中大于一半的peer所复制,这个检查机制,这个大佬做的挺机制的,就是把所有的peer的同步的最大索引拿出来,排序后,找出中间的那个 就是大多数peer复制的,然后进行周期检查,并且比较大小。周期检查有可能中位数不是这个周期发送的 ,则不能处理
  3. 同步失败的话,可能是由于follow进度过慢导致的,在lab2C中会优化这个过程使用矛盾周期和矛盾索引直接定位,2B中只要乖乖回退,重发就可
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
 
	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]",
			rf.me, rf.leaderId, args.Term, rf.currentTerm, rf.role, len(rf.log), args.PrevLogIndex, args.PrevLogTerm, reply.Success, rf.commitIndex, rf.log)
	}()
 
	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 {
		return
	}
	// 如果本地有前一个日志的话,那么term必须相同,否则false
	if args.PrevLogIndex > 0 && rf.log[args.PrevLogIndex - 1].Term != args.PrevLogTerm {
		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
}

接收心跳需要做的事情就细了起来

  1. 周期一致性检查
  2. 返回成功的前提就是 本地一定要有leader 的前一个日志,而且周期相同
  3. 如果本地日志和leader一样长,则直接添加到后边,如果本地日志大于leader日志,则删除掉周期不一致的条目。一致的条目不变
  4. 最后根据leadercommit进行更新自己的commitindex
func (rf *Raft) applyLogLoop(applyCh chan ApplyMsg) {
	for !rf.killed(){
		time.Sleep(10 * time.Millisecond)
 
		var appliedMsgs = make([]ApplyMsg, 0)
 
		func() {
			rf.mu.Lock()
			defer rf.mu.Unlock()
 
			for rf.commitIndex > rf.lastApplied {
				rf.lastApplied += 1
				appliedMsgs = append(appliedMsgs, ApplyMsg{
					CommandValid: true,
					Command:      rf.log[rf.lastApplied-1].Command,
					CommandIndex: rf.lastApplied,
					CommandTerm: rf.log[rf.lastApplied - 1].Term,
				})
				DPrintf("RaftNode[%d] applyLog, currentTerm[%d] lastApplied[%d] commitIndex[%d]", rf.me, rf.currentTerm, rf.lastApplied, rf.commitIndex)
			}
		}()
		// 锁外提交给应用层
		for _, msg := range appliedMsgs {
			applyCh <- msg
		}
	}
}

这个是提交检测,一旦有日志复制且大于最后一次复制的就进行提交。这里不用担心一致性问题,因为leader会提交的日志会先于peer提交的

func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
	//Your code here (2A, 2B).
	rf.mu.Lock()
	defer rf.mu.Unlock()

	reply.Term = rf.currentTerm
	reply.VoteGranted = false
	DPrintf("RaftNode[%d] Handle RequestVote, CandidatesId[%d] Term[%d] CurrentTerm[%d] LastLogIndex[%d] LastLogTerm[%d] votedFor[%d]", rf.me, args.CandidateId, args.Term, rf.currentTerm, args.LastLogIndex, args.LastLogTerm, rf.votedFor)
	defer func() {
		DPrintf("RaftNode[%d] Return RequestVote, CandidatesId[%d] VoteGranted[%v] ", rf.me, args.CandidateId, reply.VoteGranted)
	}()

	// 任期不如我大,拒绝投票
	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
	}
	if rf.votedFor == -1 || rf.votedFor == args.CandidateId {
		// candidate的日志必须比我的新
		// 1, 最后一条log,任期大的更新
		// 2,更长的log则更新
		lastLogTerm := 0
		if len(rf.log) != 0 {
			lastLogTerm = rf.log[len(rf.log)-1].Term
		}
		// if args.LastLogTerm >lastLogTerm || args.LastLogIndex < len(rf.log) {
		// 	DPrintf("sbbbbb")
		// 	return
		// }
		// rf.votedFor = args.CandidateId
		// reply.VoteGranted = true
		// rf.lastActiveTime = time.Now() // 为其他人投票,那么重置自己的下次投票时间
		//lastLogTerm := rf.lastTerm()
		// 这里坑了好久,一定要严格遵守论文的逻辑,另外log长度一样也是可以给对方投票的
		if args.LastLogTerm > lastLogTerm || (args.LastLogTerm == lastLogTerm && args.LastLogIndex >= len(rf.log)) {
			rf.votedFor = args.CandidateId
			reply.VoteGranted = true
			rf.lastActiveTime = time.Now() // 为其他人投票,那么重置自己的下次投票时间
		}
	}
	rf.persist()
}

这就是请求投票的RPC处理,比之前多的就是
args.LastLogTerm > lastLogTerm || (args.LastLogTerm == lastLogTerm && args.LastLogIndex >= len(rf.log))
这个判断条件,这个判断条件说白了就是两个

  • leader的日志要么比我的日志长或者相等且周期相等则投票
  • leader的日志周期大于我的最后一条的日志周期
func (rf *Raft) electionLoop() {
	for !rf.killed() {
		time.Sleep(1 * time.Millisecond)
		func() {
			rf.mu.Lock()
			defer rf.mu.Unlock()

			now := time.Now()
			timeout := time.Duration(200+rand.Int31n(150)) * time.Millisecond
			// 超时随机化
			elapses := now.Sub(rf.lastActiveTime)
			if rf.role == ROLE_FOLLOWER {
				if elapses >= timeout {
					rf.role = ROLE_CANDIDATES
					DPrintf("RaftNode[%d] Follower -> Candidate", rf.me)
				}
			}
			if rf.role == ROLE_CANDIDATES && elapses >= timeout {
				rf.lastActiveTime = now //重置下次选举时间

				rf.currentTerm += 1
				rf.votedFor = rf.me
				rf.persist()

				//reqvote
				args := RequestVoteArgs{
					Term:         rf.currentTerm,
					CandidateId:  rf.me,
					LastLogIndex: len(rf.log), //?
				}

				if len(rf.log) != 0 {
					args.LastLogTerm = rf.log[len(rf.log)-1].Term
				}
				rf.mu.Unlock()

				DPrintf("RaftNode[%d] RequestVote starts, Term[%d] LastLogIndex[%d] LastLogTerm[%d]", rf.me, args.Term,
					args.LastLogIndex, args.LastLogTerm)

				// 并发RPC请求vote
				type VoteResult struct {
					peerId int
					resp   *RequestVoteReply
				}
				/*
					type RequestVoteReply struct {
						// Your data here (2A).
						Term        int
						VoteGranted bool
					}
				*/
				voteCount := 1   // 收到投票个数(先给自己投1票)
				finishCount := 1 // 收到应答个数
				VoteResultChan := make(chan *VoteResult, len(rf.peers))
				for peerId := 0; peerId < len(rf.peers); peerId++ {
					go func(id int) {
						if id == rf.me {
							return
						}
						resp := RequestVoteReply{}
						if ok := rf.sendRequestVote(id, &args, &resp); ok {
							VoteResultChan <- &VoteResult{peerId: id, resp: &resp}
						} else {
							VoteResultChan <- &VoteResult{peerId: id, resp: nil}
						}
					}(peerId)
				}
				maxTerm := 0
				for {
					select {
					case VoteResult := <-VoteResultChan:
						finishCount += 1
						if VoteResult.resp != nil {
							if VoteResult.resp.VoteGranted {
								voteCount += 1
							}
							if VoteResult.resp.Term > maxTerm {
								maxTerm = VoteResult.resp.Term
							}
						}
						if finishCount == len(rf.peers) || voteCount > len(rf.peers)/2 {
							goto VOTE_END
						}
					}
				}
			VOTE_END:
				rf.mu.Lock()
				defer func() {
					DPrintf("RaftNode[%d] RequestVote ends, finishCount[%d] voteCount[%d] Role[%s] maxTerm[%d] currentTerm[%d]", rf.me, finishCount, voteCount,
						rf.role, maxTerm, rf.currentTerm)
				}()
				// 如果角色改变了,则忽略本轮投票结果
				if rf.role != ROLE_CANDIDATES {
					return
				}
				if maxTerm > rf.currentTerm {
					rf.role = ROLE_FOLLOWER
					rf.leaderId = -1
					rf.currentTerm = maxTerm
					rf.votedFor = -1
					rf.persist()
					return
				}
				if voteCount > len(rf.peers)/2 {
					rf.role = ROLE_LEADER
					rf.leaderId = rf.me

					rf.nextIndex = make([]int, len(rf.peers))
					for i := 0; i < len(rf.peers); i++ {
						rf.nextIndex[i] = len(rf.log) + 1
					}
					rf.matchIndex = make([]int, len(rf.peers))
					for i := 0; i < len(rf.peers); i++ {
						rf.matchIndex[i] = 0
					}
					rf.lastBroadcastTime = time.Unix(0, 0) // 令appendEntries广播立即执行
					return
				}
			}
		}()
	}
}

这个部分比上2A多了些这个

	rf.nextIndex = make([]int, len(rf.peers))
					for i := 0; i < len(rf.peers); i++ {
						rf.nextIndex[i] = len(rf.log) + 1
					}
					rf.matchIndex = make([]int, len(rf.peers))
					for i := 0; i < len(rf.peers); i++ {
						rf.matchIndex[i] = 0
					}
					rf.lastBroadcastTime = time.Unix(0, 0) // 令appendEntries广播立即执行

就是选举成功后初始化每个peer的nextindex和matchindex matchindex为什么是0 每次心跳成功后会被leader更新,而nextindex 则需要在心跳机制中用到,确保发送最新的日志条目。

以上就是lAB2B的全部了

写在后边

这是某位大佬的总结,个人觉得很有用,仅供日后复习所用

  • nextIndex是leader对follower日志同步进度的猜测,matchIndex则是实际获知到的同步进度,leader需要不断的appendEntries来和follower进行反复校对,直到PrevLogIndex、PrevLogTerm符合Raft论文约束。
  • Leader更新commitIndex需要计算大多数节点拥有的日志范围,通过对matchIndex排序找中位数的index,就是大多数节点都拥有的日志范围,将其设置为commitIndex。
  • Follower收到appendEntries时,一定要在处理完log写入后再更新commitIndex,因为论文中要求Follower的commitIndex是min(local log index,leaderCommitIndex)。
  • Leader在不断调整某个follower的nextIndex过程中,注意不要让nextIndex减小到1以下,因为nextIndex的语义是follower的下一个日志写入下标。
  • requestVote接收端,一定要严格根据论文判定发起方的lastLogIndex和lastLogTerm是否符合条件,这里很容易写错。
  • appendEntries接收端,一定要严格遵从prevLogIndex和prevLogTerm的论文校验逻辑,首先看一下prevLogIndex处是否有本地日志(prevLogIndex==0除外,相当于从头同步日志),没有的话则还需要leader来继续回退nextIndex直到prevLogIndex位置有日志。在prevLogIndex有日志的前提下,还需要进一步判prevLogIndex位置的Term是否一样。
  • appendEntries接收端返回success=true的前提必须是将leader传来的日志处理完了并且更新了commitIndex之后。
  • appendEntries接收端,要做好日志冲突时的后续全部截断逻辑。
  • 注意当log、voteFor、commitIndex三个状态变化后,返回RPC前一定要persist()持久化一下(Lab2B并不要求persist,但是Lab3C要求,所以在这里先做好,免得Lab2C回头加)
  • 日志提交通过一个独立的goroutine实现就行,定期判断lastApplied和commitIndex之间的有日志就提交即可,注意提交效率(copy出来释放掉锁,然后往应用层推)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值