Mit6.824-lab2d-2022

Mit6.824-lab2d-2022

写在前面

如果你听从了我在 2a里的建议,计算下标时已经考虑到了snapshot并且有好好做过2a和2b实验的设计与debug,那2d实验和2c差不多一样简单,只完成snapshot和installSnapshot这两个功能,并在适时的位置调用即可。如果你是和我一样什么也没有准备,就一个实验一个实验接着往下做,那想要2d做到完善,就需要大量的填补漏洞与漫长的debug,甚至需要把前面的代码重构一遍。我这里在完成功能代码之后被迫把2a和2b的代码进行了一些重构,重新设计了逻辑和顺序,最后才通过测试。

实验目标

按照任务书,2d实验主要有两部分

  1. 完成snapshot方法
  2. 完成发送snapshot功能

这两个要求较之前设计的要复杂一点,所以这里单独介绍一下

1.snapshot方法
func (rf *Raft) Snapshot(index int, snapshot []byte) 

这个方法不需要你在Raft中调用,是由测试代码接收到足够的apply的log后,进行调用,将snapshot的二进制文件和对应的index传入,要求你丢弃相应的log文件并修改LastIncludedIndex等参数。

2.发送snapshot功能
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的锁,所以很可能出现提交顺序错误的情况,包括以下两种:

  1. snapshot提交后,apply了snapshot里包含的log
  2. snapshot未及时提交,但lastApplied已经增加,apply了snapshot后面的log

所以applyCh的使用必须加锁,这个后面也会提到

除了提交顺序的问题,这里有一个很容易被忽略的错误,由于提交时会放开锁,所以LastApplied很可能在这里因为提交了snapshot被修改,因而导致LastApplied数值出现错误,所以重新加锁后一定要再加一步判断。

实验内容

由于我写2d的之前完全没有注意到LastIncludedIndedx的问题,所以整个代码进行了完全的重构,这篇博文也按照我自己代码思路来组织。

1.工具方法

在完成功能代码之前,建议先完成工具方法。我看也有一些博客建议把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的计算方法,分类讨论。

2.snapshot
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)
}

这里有几个需要注意的点

  1. 提交snapshot后要更新 commitIndex和lastApplied,同时进行持久化
  2. 提交snapshot后log为空,要考虑log的组织方式是否需要初始化。按照paper的意思,log的0号位置保持为空,从1号开始使用。我这里在Raft初始化时,在log的0号位置放了个空的log,而在提交snapshot后,log从0号位置直接继续使用。这样的好处是计算下标时更简单,坏处是必须考虑提交snapshot后log长度为0的特殊情况。
  3. 持久化的数据要加上LastIncludedInde和LastIncludedTerm,并且这两个值不为0时,恢复Raft时需要赋给LastApplied
3.installSnapshot

这部分逻辑和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

4.其他重要重构逻辑
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 :TestSnapshotInstallUnreliable2DTestSnapshotInstallCrash2D的叠加

总结

一定要好好debug,查好自己所有的逻辑,一定要严格遵守paper的要求,并且自己所有的逻辑一定要一致,只能说bug可能出现在每一行功能代码。debug做了好几天,2d测试做一遍要好几分钟,然后bug也是两三百次测试才出一次,只能在每处可能出错的地方打上log,再从几万条log里一条一条找哪里可能出错。笔者改好可以让2d稳定通过之后测试2c发现又高频出错,只能把代码又重构。最后测试结果,lab2测试大概2-4千次会有一次错误,原因是RPC不稳定情况下长时间选不出Leader,这种罕见情况也是情理之内,只能说低概率事件没法避免了。
Mit6.824-lab2d-2022_第1张图片
Mit6.824-lab2d-2022_第2张图片

你可能感兴趣的:(Mit6.824,分布式)