如果你听从了我在 2a里的建议,计算下标时已经考虑到了snapshot并且有好好做过2a和2b实验的设计与debug,那2d实验和2c差不多一样简单,只完成snapshot和installSnapshot这两个功能,并在适时的位置调用即可。如果你是和我一样什么也没有准备,就一个实验一个实验接着往下做,那想要2d做到完善,就需要大量的填补漏洞与漫长的debug,甚至需要把前面的代码重构一遍。我这里在完成功能代码之后被迫把2a和2b的代码进行了一些重构,重新设计了逻辑和顺序,最后才通过测试。
按照任务书,2d实验主要有两部分
这两个要求较之前设计的要复杂一点,所以这里单独介绍一下
func (rf *Raft) Snapshot(index int, snapshot []byte)
这个方法不需要你在Raft中调用,是由测试代码接收到足够的apply的log后,进行调用,将snapshot的二进制文件和对应的index传入,要求你丢弃相应的log文件并修改LastIncludedIndex等参数。
func (rf *Raft) InstallSnapShot(args *InstallSnapShotArgs,reply *InstallSnapShotReply)
func (rf *Raft) SnapShotSender(server int)
可以理解为appendEntry一样的功能,当某一个Follower要求的log已经被Leader丢弃时,Leader负责发送snapshot给Follower,然后后者负责接收,并且将收到的snapshot直接进行apply。
在这里有一个事情需要特别注意:提交snapshot和提交entry的效果是一样的,而且snapshot对entry是覆盖的效果。但是snapshot和log的提交都是通过同一个通道applyCh,并且由于通道接收阻塞的缘故,在进行提交的时候必须放开Raft的锁。具体介绍可以参考 这篇博文。这里补充一个原文章没有提到的事情,由于提交snapshot和log都需要放开Raft的锁,所以很可能出现提交顺序错误的情况,包括以下两种:
所以applyCh的使用必须加锁,这个后面也会提到
除了提交顺序的问题,这里有一个很容易被忽略的错误,由于提交时会放开锁,所以LastApplied很可能在这里因为提交了snapshot被修改,因而导致LastApplied数值出现错误,所以重新加锁后一定要再加一步判断。
由于我写2d的之前完全没有注意到LastIncludedIndedx的问题,所以整个代码进行了完全的重构,这篇博文也按照我自己代码思路来组织。
在完成功能代码之前,建议先完成工具方法。我看也有一些博客建议把log单独封装成一个类,并且写一些相关的方法。个人感觉没有必要,直接把工具方法写道Raft上即可。这里列出我的工具方法,不过不是所有的都用到了,按需添加。
// 全部长度
func (rf *Raft) LastLength()int{
if rf.LastIncludedIndex == 0{
return len(rf.log)
}else{
return len(rf.log) + rf.LastIncludedIndex + 1
}
}
// 返回全部log的最后一个的index,包括snapshot
func(rf *Raft) LastIndex() int{
if rf.LastIncludedIndex == 0{
return len(rf.log) - 1
}else if len(rf.log) == 0{
return rf.LastIncludedIndex
}else{
return rf.LastIncludedIndex + len(rf.log)
}
}
// 返回全部log的最后一个的Term,包括snapshot
func (rf *Raft)LastTerm() int{
lastTerm := 0
if len(rf.log) > 0{
lastTerm = rf.log[len(rf.log)-1].Term
}else if len(rf.log) == 0{
lastTerm = rf.LastIncludedTerm
}else{
panic("term wrong,len(log) smaller than 0")
}
return lastTerm
}
// 入大出小 Index
func (rf *Raft)ScalerIndexToReal(index int) int{
if rf.LastIncludedIndex == 0{
return index
}else{
var result = index - rf.LastIncludedIndex - 1
if result >= 0{
return result
}else{
log.Printf("index %d lastIncludedIndex %d",index,rf.LastIncludedIndex)
panic(" ")
}
}
}
// 入大出小 Term
func (rf *Raft)ScalerTermToReal(index int) int{
if index == rf.LastIncludedIndex{
return rf.LastIncludedTerm
}
return rf.log[rf.ScalerIndexToReal(index)].Term
}
// 入小出大 indedx
func (rf *Raft)ScalerIndexToLast(index int) int{
var result = index + rf.LastIncludedIndex
if result >= 0{
return result
}else{
panic("wrong index smaller than LastIncludeindex")
}
}
// 入小出大 term
func (rf *Raft)ScalerTermToLast(index int) int{
if index > 0 {
if rf.RealLength() == 0{
return rf.LastIncludedTerm
}else{
return rf.log[index].Term
}
}else if index == 0{
return rf.LastIncludedTerm
}else {
panic("wrong index smaller than 0")
}
}
// 返回目前log长度
func (rf *Raft)RealIndex() int{
return len(rf.log) - 1
}
func (rf *Raft)RealLength() int{
return len(rf.log)
}
在这里,我的lastApplied,nextIndex等都是按照log总长度(log+snapshot)来计数,这样的好处是不必每一次进行snapshot的时候修改全部参数,而且逻辑更自然。编写代码的时候一定要注意好自己的设计,让自己的下标逻辑成完整体系。并且考虑到长度和Index的计算方法,分类讨论。
func (rf *Raft) Snapshot(index int, snapshot []byte) {
// Your code here (2D).
log.Printf("peer :%d going to snapshot : index %d , applied %d",rf.me,index,rf.lastApplied)
if rf.killed(){
return
}
rf.mu.Lock()
defer rf.mu.Unlock()
if index > rf.commitIndex || rf.LastIncludedIndex >= index{
return
}
snapLogs := make([]Entry,0)
//snapLogs = append(snapLogs,Entry{0,-1,true})
for i := rf.ScalerIndexToReal(index + 1); i < rf.RealLength(); i++ {
snapLogs = append(snapLogs, rf.log[i])
}
rf.LastIncludedTerm = rf.ScalerTermToReal(index)
rf.LastIncludedIndex = index
log.Println(rf)
rf.log = snapLogs
if index > rf.commitIndex{
rf.commitIndex = index
}
if index >= rf.lastApplied{
rf.lastApplied = index + 1
}
rf.persister.SaveStateAndSnapshot(rf.raftState(), snapshot)
}
这里有几个需要注意的点
这部分逻辑和AppendEntry一样,并且放在心跳部分
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] <= rf.LastIncludedIndex{
rf.mu.Unlock()
go rf.SnapShotSender(peer)
log.Printf("leader: %d send snapshot to peer %d with len(log) %d, snapshot %d , nextindex %d ", rf.me,peer,len(rf.log),rf.LastIncludedIndex,rf.nextIndex[peer])
time.Sleep(50 * time.Millisecond)
break
}
……
……
……
}
}
}
当Leader发现需要发送的log已经被丢弃的时候,就发送snapshot
func (rf *Raft) SnapShotSender(server int){
log.Printf("leader :%d send snapshot to peer %d", rf.me,server)
if rf.killed(){
return
}
if rf.state != Leader{
return
}
rf.mu.Lock()
args := &InstallSnapShotArgs{
Term:rf.currentTerm,
LeaderId:rf.me,
LastIncludedIndex:rf.LastIncludedIndex,
LastIncludedTerm:rf.LastIncludedTerm,
Data:rf.persister.ReadSnapshot(),
}
reply := &InstallSnapShotReply{
Term: -1,
}
rf.mu.Unlock()
ok := false
done := make(chan bool)
time_out := make(chan bool)
go func(){
ok = rf.sendSnapShot(server, args, reply)
done <- true
}()
go func(){
time.Sleep(1000 * time.Millisecond)
time_out<- true
}()
select{
case <- done :
case <- time_out :
}
rf.mu.Lock()
if ok{
if args.Term != rf.currentTerm || rf.state != Leader{
rf.mu.Unlock()
return
}
if reply.Term > rf.currentTerm{
rf.followerOut = 0
rf.mu.Unlock()
rf.BeFollower(reply.Term, NILL)
return
}
log.Printf("leader %d finish snapshot with rf.matchIndex%d args.LastIncludedIndex %d rf.nextIndex %d",rf.me, rf.matchIndex[server],args.LastIncludedIndex,rf.nextIndex[server])
log.Println(rf.currentTerm,args.Term,ok)
rf.matchIndex[server] = args.LastIncludedIndex
rf.nextIndex[server] = args.LastIncludedIndex + 1
}
rf.mu.Unlock()
log.Printf("leader :%d send snapshot to peer %d end", rf.me,server)
return
}
基本上的逻辑和发送log是一样的,不同的地方是,这里不需要考虑是否发送成功,只要RPC通讯成功了,并且自己的状态没变(term和state),并且返回的term不比自己高,就默认发送成功,并更新自己的matchIndex和nextIndex
func (rf *Raft) InstallSnapShot(args *InstallSnapShotArgs,reply *InstallSnapShotReply){
if rf.killed(){
return
}
log.Printf("peer %d receive snapshot", rf.me)
rf.mu.Lock()
switch{
case args.Term < rf.currentTerm:
reply.Term = rf.currentTerm
rf.mu.Unlock()
return
case args.Term > rf.currentTerm:
rf.mu.Unlock()
rf.BeFollower(args.Term, NILL)
case args.Term == rf.currentTerm:
rf.mu.Unlock()
rf.BeFollower(args.Term, args.LeaderId)
}
rf.mu.Lock()
rf.followerOut = 0
reply.Term = rf.currentTerm
if rf.LastIncludedIndex >= args.LastIncludedIndex{
rf.mu.Unlock()
return
}
index := args.LastIncludedIndex
tempLog := make([]Entry,0)
for i := rf.ScalerIndexToReal(index + 1);i<=rf.RealIndex();i++{
tempLog = append(tempLog,rf.log[i])
}
rf.LastIncludedIndex = args.LastIncludedIndex
rf.LastIncludedTerm = args.LastIncludedTerm
rf.log = tempLog
if index > rf.commitIndex{
rf.commitIndex = index
}
if index >= rf.lastApplied{
rf.lastApplied = index + 1
}
rf.persist()
rf.persister.SaveStateAndSnapshot(rf.raftState(),args.Data)
rf.IsSnapShot = true
rf.mu.Unlock()
msg := ApplyMsg{
SnapshotValid: true,
Snapshot: args.Data,
SnapshotTerm: rf.LastIncludedTerm,
SnapshotIndex: rf.LastIncludedIndex,
}
if rf.isCommit{
time.Sleep(10 * time.Millisecond)
}
rf.applyMessage <- msg
rf.mu.Lock()
rf.IsSnapShot = false
rf.mu.Unlock()
log.Printf("peer %d handle snapshot and end", rf.me)
//log.Println(rf)
//log.Println(args)
//log.Println(reply)
}
上文提到默认snapshot安装成功,是因为根据paper的逻辑,只要收到的term不小于自己的term,并且自己没安装过,peer就会接收这个snapshot。peer接收到snapshot后,应该立刻更新commitIndex和lastApplied,并且把snapshot提交给ApplyCH,这里提交snapshot时也需要放开锁。
func (rf *Raft) InstallSnapShot(args *InstallSnapShotArgs,reply *InstallSnapShotReply){
……
rf.IsSnapShot = true
rf.mu.Unlock()
msg := ApplyMsg{
SnapshotValid: true,
Snapshot: args.Data,
SnapshotTerm: rf.LastIncludedTerm,
SnapshotIndex: rf.LastIncludedIndex,
}
if rf.isCommit{
time.Sleep(10 * time.Millisecond)
}
rf.applyMessage <- msg
rf.mu.Lock()
rf.IsSnapShot = false
……
}
func (rf *Raft)ApplyEntries(){
……
if rf.log[rf.ScalerIndexToReal(rf.lastApplied)].IsEmpty == true{
rf.lastApplied++
continue
}else{
if rf.IsSnapShot {
break
}
tempapply := rf.lastApplied
message := ApplyMsg{true,rf.log[rf.ScalerIndexToReal(rf.lastApplied)].Command,rf.lastApplied,false,[]byte{'a'},0,0}
rf.isCommit = true
rf.mu.Unlock()
rf.applyMessage <- message
log.Println("4")
rf.mu.Lock()
rf.isCommit = false
if tempapply != rf.lastApplied{
break
}
}
……
}
由于前文提到的的ApplyCh的共用问题,提交log和提交snapshot时必须加锁,在这里用isCommit和isSnapShot写了一个简单的锁结构,因为我不太熟悉go的锁机制,所以自己来写了这个。
同时这里用tempapply记录了放锁前的lastApplied,重新加锁后查看是否有改变,如果有则说明已经提交过snapshot,此时不应再继续进行apply
func (rf *Raft) BeCandidate() {
for {
if rf.killed() {
return
}
if rf.state != Candidate{
return
}
go func(){
……
……
……
}()
randTime := rand.Intn(600)
time.Sleep(time.Duration(600+randTime) * time.Millisecond)
}
}
选举代码不再使用阻塞循环,而是使用了一个goroutine来进行一轮的选举。这样的好处是使得每一轮选举之间的时间可控。
TestSnapshotBasic2D :基础snapshot提交测试,每提交10个log,调用一次snapshot,查看能否正常提交,达成一致等。
TestSnapshotInstall2D :断联peer并给Leader追加log,直到部分log丢弃后重连peer,查看是否能在一定时间内完成snapshot的传递并达成一致。
TestSnapshotInstallUnreliable2D :TestSnapshotInstall2D 的基础上有一定几率RPC丢失或高延迟
TestSnapshotInstallCrash2D :TestSnapshotInstall2D基础上使peer直接carsh,重新启动后能否正常读取持久化信息,并达成一致
TestSnapshotInstallUnCrash2D :TestSnapshotInstallUnreliable2D和TestSnapshotInstallCrash2D的叠加
一定要好好debug,查好自己所有的逻辑,一定要严格遵守paper的要求,并且自己所有的逻辑一定要一致,只能说bug可能出现在每一行功能代码。debug做了好几天,2d测试做一遍要好几分钟,然后bug也是两三百次测试才出一次,只能在每处可能出错的地方打上log,再从几万条log里一条一条找哪里可能出错。笔者改好可以让2d稳定通过之后测试2c发现又高频出错,只能把代码又重构。最后测试结果,lab2测试大概2-4千次会有一次错误,原因是RPC不稳定情况下长时间选不出Leader,这种罕见情况也是情理之内,只能说低概率事件没法避免了。