Mit6.824-lab2b-2022
写在前面
2b是整个raft最重要的部分,这部分的完成度与严谨与否,直接决定了2c和2d实验的难易程度,如果2a和2b的代码可以完美符合论文的要求,那2c和2d只是额外添加一部分功能代码而已。但是2a和2b部分的测试并不能保证所有情况都被测试到,因此自己完成代码的时候一定要尽量多读几遍paper,保证自己逻辑通顺,所有的功能代码尽善尽美。
实验内容
实验内容总结起来很简单,实现append log和apply两个功能,
- Leader判断是否需要给Follower发送log,有则发送,无则发送心跳
- Follower根据情况接受log
- Leader在多数节点获得log以后,更新自己的CommitIndex,并发送给Follower
- 获取CommitIndex后进行apply
简单来说,AppendEntries方法是将client的log从Leader传递给Follower,将其提交给applyCh算做应用给自己的状态机。这里要注意区分paper和实验指导上的几个名词,client对应代码的测试部分,peer和server都是指raft类,状态机是指applyCh这个通道。而log是通过start方法传递给raft的。
实验的结构和逻辑都很简单,按照paper上Figure.2实现即可。
具体任务
- 提供一个api供以输入log
func (rf *Raft) Start(command interface{}) (int, int, bool)
- 实现一个方法以发送log给follower
- 实现一个方法接受log
- 实现一个方法对log进行apply
实现技巧
- 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)检测到已有多数同意票
- 无限循环的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的测试,仍然存在逻辑和参数错误,很多地方在后面两个实验都做了巨大的修改,最终代码查看这里这里
- 选举部分代码有所修改,可以查看2a
- 发送心跳和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分开了不同的逻辑,这样分开写对我来说更清晰而且比较简单。
- 接收心跳和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通讯次数。坏处是发送数据可能要更多。
- 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或没有提交,可以查选举代码或是提交部分。