Mit6.824-lab2b-2022

Mit6.824-lab2b-2022

写在前面

2b是整个raft最重要的部分,这部分的完成度与严谨与否,直接决定了2c和2d实验的难易程度,如果2a和2b的代码可以完美符合论文的要求,那2c和2d只是额外添加一部分功能代码而已。但是2a和2b部分的测试并不能保证所有情况都被测试到,因此自己完成代码的时候一定要尽量多读几遍paper,保证自己逻辑通顺,所有的功能代码尽善尽美。

实验内容

实验内容总结起来很简单,实现append log和apply两个功能,

  1. Leader判断是否需要给Follower发送log,有则发送,无则发送心跳
  2. Follower根据情况接受log
  3. Leader在多数节点获得log以后,更新自己的CommitIndex,并发送给Follower
  4. 获取CommitIndex后进行apply

简单来说,AppendEntries方法是将client的log从Leader传递给Follower,将其提交给applyCh算做应用给自己的状态机。这里要注意区分paper和实验指导上的几个名词,client对应代码的测试部分,peer和server都是指raft类,状态机是指applyCh这个通道。而log是通过start方法传递给raft的。
实验的结构和逻辑都很简单,按照paper上Figure.2实现即可。

具体任务

  1. 提供一个api供以输入log
func (rf *Raft) Start(command interface{}) (int, int, bool) 
  1. 实现一个方法以发送log给follower
  2. 实现一个方法接受log
  3. 实现一个方法对log进行apply

实现技巧

  1. select和通道

这两者可以有一个很好的配合,以形成一个计时器,使得方法获得一个阻塞有两种条件之一达成才可通过:1)超时,2)完成

		time_out := make(chan bool)
		vote_done := make(chan bool)
		vote_succ := make(chan bool)
		// 检查是否超时
		go func(){
			time.Sleep(CTimeOut * 100 *  time.Millisecond)
			time_out <- true
		}()
		// 检查是否所有requestVote都返回
		go func(){
			waitFlag.Wait()
			vote_done <- true	
			
		}()
		// 检查是否已经可以成为Leader
		go func(){
			for readtime := 0;readtime < CTimeOut;readtime++{
				time.Sleep(100 * time.Millisecond)
				if voteForMe*2 >= len(rf.peers){
				vote_succ <- true	
				}
			}
		}()
		select{
		case <- time_out:
		case <- vote_done:
		case <- vote_succ:
		}

在这里,代码有三种方法通过,分别是1)超时,2)waitFlag所有线程都放锁,也就是所有requestvote都结束,3)检测到已有多数同意票

  1. 无限循环的goroutine

这个在2a的计时器中已经用到了,通过这种无限循环的goroutine 可以完成很多功能,比如发送log和进行apply,这样做的好处是不需要在每一处需要进行该操作的地方进行调用,因而考虑各种边界条件。坏处是可能会因为延迟执行出现各种错误,在2d中会有各种体现。

func (rf *Raft) Timer() {
	
	for {
		randTime := rand.Intn(50)
		time.Sleep(time.Duration(75+randTime) * time.Millisecond)
		rf.mu.Lock()
		// Your code here to check if a leader election should
		// be started and to randomize sleeping time using
		// time.Sleep().
		if rf.killed(){
			rf.mu.Unlock()
			return
		}
		if rf.state == Leader || rf.state == Candidate{
			rf.followerOut = 0
			rf.mu.Unlock()
			continue
		}else if rf.state == Follower{
			rf.followerOut += 1
			if rf.followerOut >= FTimeOut{
				rf.followerOut = 0
				rf.state = Candidate
				rf.mu.Unlock()
				go rf.BeCandidate()
			}else{
				rf.mu.Unlock()
			}
		}
	}
}

尽管在go中可以通过defer Unlock()方便地在函数执行结束进行解锁,但是由于方法可能会有其他的调用,以及各种奇怪的出口,甚至是crash,因此我建议在每个方法谨慎调整锁,每一处都手动解锁以保证线程安全

代码实现

注意 代码根据2a修改而来,同样这个代码只能通过2b的测试,仍然存在逻辑和参数错误,很多地方在后面两个实验都做了巨大的修改,最终代码查看这里这里

  1. 选举部分代码有所修改,可以查看2a
  2. 发送心跳和log
func (rf * Raft) BeLeader(){
	rf.mu.Lock()
	
	rf.state = Leader
	rf.votedFor = rf.me
	rf.candidateOut = 0
	rf.followerOut = 0

	for i:=0;i<len(rf.peers);i++{
		rf.nextIndex[i] = len(rf.log)
		rf.matchIndex[i] = 0
	}
	
	rf.mu.Unlock()
	rf.SendHeartBeat()
	for i:=0;i<len(rf.peers);i++{
		if i == rf.me{
			continue
		}
		go rf.SyncAppendEntry(i)
	}
}
func (rf *Raft) SyncAppendEntry(peer int){
	for{
		if rf.killed(){
			return
		}
		for{
			rf.mu.Lock()
			if rf.state != Leader{
				rf.mu.Unlock()
				return
			}
			if rf.nextIndex[peer]>=len(rf.log) && true{

				args:=&AppendEntriesArgs{
					Term:rf.currentTerm,
					LeaderId:rf.me,
					PrevLogIndex:len(rf.log),
					PrevLogTerm:rf.LastTerm(),
					Entries:make([]Entry,0),
					LeaderCommit:rf.commitIndex,
				}			
				reply:=&AppendEntriesReply{
					Term:0,
					Success:false,
					UpNextIndex:0,
				}
				rf.mu.Unlock()
				append_done := make(chan bool)
				ok := false
				go func(server int){
					ok = rf.sendAppendEntry(server, args, reply)
					append_done <-true
				}(peer)	
				select{
					case <- append_done:
				}			
				if ok{
				rf.mu.Lock()
					if reply.Term<=rf.currentTerm{

						if len(args.Entries) ==0 && rf.nextIndex[peer] >0{
							if reply.UpNextIndex == 0{
							}
							rf.nextIndex[peer] = Max(reply.UpNextIndex,rf.matchIndex[peer] + 1)
							
						}else{
						}
						rf.mu.Unlock()

					}else{
						rf.mu.Unlock()
						rf.BeFollower(reply.Term, NILL)
						return
					}					
				}
				break
			}
			var data = make([]Entry,len(rf.log)-rf.nextIndex[peer])
			if len(data) > 0{
				copy(data,rf.log[rf.nextIndex[peer]:len(rf.log)])
			}
			args:=&AppendEntriesArgs{
				Term:rf.currentTerm,
				LeaderId:rf.me,
				PrevLogIndex:rf.nextIndex[peer] - 1,
				PrevLogTerm:rf.log[rf.nextIndex[peer] - 1].Term,
				Entries:data,
				LeaderCommit:rf.commitIndex,
			}
			if args.PrevLogIndex<=0 {
				args.PrevLogIndex = 0
			}
			reply:=&AppendEntriesReply{
				Term:0,
				Success:false,
				UpNextIndex:0,
			}
			rf.mu.Unlock()
			time_out := make(chan bool)
			append_done := make(chan bool)
			ok := false
			go func(){
				ok = rf.sendAppendEntry(peer, args, reply)
				append_done<- true
			}()
			select{
				case <- time_out:
					//log.Println("timeout")
				case <- append_done:
					//log.Println("append_done")
			}
			if ok{
				rf.mu.Lock()
				if reply.Success{
					rf.nextIndex[peer] += len(data)
					rf.matchIndex[peer] = Max(rf.matchIndex[peer],rf.nextIndex[peer]-1)
					rf.mu.Unlock()	
				}else{
					if reply.Term<=rf.currentTerm{
						if len(args.Entries) !=0 && rf.nextIndex[peer] >0{
							if reply.UpNextIndex == 0{
							}
							rf.nextIndex[peer] = reply.UpNextIndex	
						}
						rf.mu.Unlock()
					}else{
						rf.mu.Unlock()
						rf.BeFollower(reply.Term, NILL)
						return
				}}
			}else{
				break
			}}
		time.Sleep(50 * time.Millisecond)
}}

这里发送心跳和log分开了不同的逻辑,这样分开写对我来说更清晰而且比较简单。

  1. 接收心跳和log

func (rf *Raft) AppendEntry(args *AppendEntriesArgs, reply *AppendEntriesReply) {
	// Your code here (2A, 2B).
	reply.Term = rf.currentTerm
	reply.Success = false
	reply.UpNextIndex = 1
	rf.mu.Lock()
	if  args.Term > rf.currentTerm{
		rf.followerOut = 0
		rf.mu.Unlock()
		rf.BeFollower(args.Term, args.LeaderId)
		return
	}else if args.Term == rf.currentTerm{
		rf.followerOut = 0
		rf.mu.Unlock()
		rf.BeFollower(args.Term, args.LeaderId)
		rf.mu.Lock()
		// 心跳,直接返回
		if len(args.Entries) == 0{
			if rf.LastTerm() == args.Term{
				if args.LeaderCommit > rf.commitIndex{
					rf.commitIndex = Min(args.LeaderCommit, len(rf.log)-1)
				}
			}
			reply.UpNextIndex = rf.lastApplied
			rf.mu.Unlock()
			return
		}
		if len(rf.log) - 1 < args.PrevLogIndex {
			reply.UpNextIndex = rf.lastApplied
			rf.mu.Unlock()
			return
		}
		if rf.log[args.PrevLogIndex].Term != args.PrevLogTerm {
			reply.UpNextIndex = rf.lastApplied
			rf.mu.Unlock()
			return
		}
		if args.PrevLogIndex < rf.lastApplied - 1{
			reply.UpNextIndex = rf.lastApplied
			rf.mu.Unlock()
			return
		}
		rf.log = rf.log[:args.PrevLogIndex + 1]
		rf.log = append(rf.log, args.Entries...)

		if args.LeaderCommit > rf.commitIndex{
			rf.commitIndex = Min(args.LeaderCommit, len(rf.log)-1)
		}
		reply.UpNextIndex = len(rf.log)
		reply.Success = true
		rf.mu.Unlock()
		return
	}else{
		reply.Success = false
		rf.mu.Unlock()
		return
	}
}

这里要格外注意每种情况判断的顺序,首先要判断Term,然后是否是心跳,之后再判断各种边界条件。在这已经做了一个发送log的简化,也就是Leader并非每次都将发送的log长度增加1,而是Follower返回自己的appliedIndex。这样做的好处是简单,减少RPC通讯次数。坏处是发送数据可能要更多。

  1. apply部分
func (rf *Raft)ApplyEntries(){
	for{
		time.Sleep(30 * time.Millisecond)
		if rf.killed(){
			return
		}
		if rf.state == Leader{
			rf.mu.Lock()
			middleMatch :=make([]int,len(rf.matchIndex))
			copy(middleMatch,rf.matchIndex)
			middleMatch[rf.me] = len(rf.log)-1
			sort.Ints(middleMatch)
			midIndex := len(middleMatch)/2
			if middleMatch[midIndex] == -1 || middleMatch[midIndex] == rf.commitIndex{
				rf.mu.Unlock()
				continue
			}
			rf.mu.Unlock()
			if len(rf.log) > 0{
				if rf.log[middleMatch[midIndex]].Term == rf.currentTerm{
					rf.commitIndex = Max(rf.commitIndex,middleMatch[midIndex])
					log.Printf("leader:%d update commit: %d", rf.me,rf.commitIndex)
					rf.SendHeartBeat()
				}			
			}
		}
		rf.mu.Lock()
		for rf.lastApplied <= rf.commitIndex{

			if rf.log[rf.lastApplied].IsEmpty == true{
				rf.lastApplied++
				continue
			}else{
				message := ApplyMsg{true,rf.log[rf.lastApplied].Command,rf.lastApplied,false,[]byte{'a'},0,0}
				rf.applyMessage <- message
				rf.appliedLog = append(rf.appliedLog,rf.log[rf.lastApplied])
			}
			rf.lastApplied++
		}
		rf.mu.Unlock()
	}
}

注意最后进行提交的时候,这里是加了锁的,但是老师在课上强调了这里应该放开锁,因为2d实验发送snapshot会有所冲突。所以这个地方后面还要再修改

测试内容

TestBasicAgree2B():最基础的追加日志测试。发送日志并查看提交情况
TestRPCBytes2B:基于RPC的字节数检查保证每个cmd都只对每个peer发送一次。空log不算在字节数里
For2023TestFollowerFailure2B:断联leader后剩余节点能否正常提交,断联所有节点后再追加log能否保证不提交
For2023TestLeaderFailure2B:3个peer断联第一个leader后保证提交,断联第二个leader后保证不提交
TestFailAgree2B:断连小部分,不影响整体Raft集群的情况检测追加日志。
TestFailNoAgree2B:断连过半数节点,保证无日志可以正常追加。然后又重新恢复节点,检测追加日志情况。
TestConcurrentStarts2B:模拟客户端并发发送多个命令
TestRejoin2B:Leader 1断连,再让旧leader 1接受日志,再给新Leader 2发送日志,2断连,再重连Leader 1,提交日志,再让2重连,再提交日志。
TestBackup2B:先给Leader 1发送日志,然后断连3个Follower(总共1Ledaer 4Follower),网络分区。提交大量命令给1。然后让leader 1和其Follower下线,之前的3个Follower上线,向它们发送日志。然后在对剩下的仅有3个节点的Raft集群重复上面网络分区的过程。
TestCount2B:检查无效的RPC个数,不能过多。

错误提示

apply error:提交错误,一般是有长log的Peer收到了commitIndex的更新,但是没有删除之前的log,或是Leader错误更新了commitIndex
one(x) failed to reach agreement:一般是长时间为选出leader或没有提交,可以查选举代码或是提交部分。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值