写在前面,part D快照这部分没有完全通过,看debug 日志十分痛苦,所以就用了别人的raft直接快进到 lab3。如果以后有时间了再重新来看吧,目前这篇只是想记录下,part D最基本的测试点怎么过,以及理解part D中多出来的几个函数调用之间的关系。
我自己的partD实现在这里,不过有bug。别人的代码就暂时不贴了,未经别人同意。
函数之间的关系
课程给出了两个基本的函数,分别是Snapshot()
,CondInstallSnapshot()
这俩,当然了自己还需要根据论文快照那一节实现InstallSnapshot
。一开始的时候不能很好的理解这几个函数,所以下面说一下我自己的理解。
论文中提到使用快照来避免日志的无限制增长,最开始我误认为生成快照的工作是由Raft来做的,而实际上生成快照的工作是由基于Raft的应用来做的。比如说一个KV 数据库,它记录下 日志: put(1,13)。然后由Raft将这条日志同步给集群中的其他节点,当日志增长到一定大小的时候,就要对日志截断,释放一些空间出来。关键点就是要考虑到Raft只是一个一致性协议,它就是保证上层应用的数据能够很好的同步给集群中的其他节点。
认识到这一点之后,就很好理解这几个函数的作用了
Snapshot(index int, snapshot []byte):
应用程序将index(包括)之前的所有日志都打包为了快照,即参数snapshot [] byte。那么对于Raft要做的就是,将打包为快照的日志直接删除,并且要将快照保存起来,因为将来可能会发现某些节点大幅度落后于leader的日志,那么leader就直接发送快照给它,让他的日志“跟上来”。 不过,并不是所有的快照接收到就必定应用到上层程序当中,比如说接收到快照里面包含着:put(1,14) ,put(2,14) ,我们要判断快照里面哪些日志是可用的,有效的才将他们应用到程序当中。这就是CondInstallSnapshot()
的工作。
CondInstallSnapshot(lastIncludedTerm int, lastIncludedIndex int, snapshot []byte) bool:
如果一个节点接收到一个快照后,提交给上层应用(由InstallSnapshot来完成这个工作),上层应用必须要判断,目前接收到的快照是不是有效的,如果快照的lastIncludedIndex
比自己的最后一个日志的log entry index要大,那也就是说自己的日志太落后,要更新下。如果收到老的快照,那么就直接丢弃。不过我目前在这里的实现还有些问题,后续在修改这里的描述吧。
InstallSnapshot
这是一个RPC处理函数,每一个节点在这个处理函数中接收来自leader的快照,然后要判断一下是不是过期的快照。不然的话就直接传给上层应用,让它去判断是否要应用快照里面的内容。 在lab中,往applyCh写入一个snapshot就是将快照传给应用的过程。
代码的实现
总的来说,实现快照以后,就是要调整一些对于日志的log index的调整。这个稍后再说,先说一下basic测试点。
basic要做的东西比较简单,就是丢弃index之前的所有日志,并且将snapshot保存下来。不过这里对于日志的删除并不是简单的slice操作,因为slice操作不利于GC对于数组的回收,所以我们在删除日志的时候,要copy一下。提示中也说到了这一点:
Raft must discard old log entries in a way that allows the Go garbage collector to free and re-use the memory; this requires that there be no reachable references (pointers) to the discarded log entries.
代码如下:
func (rf *Raft) Snapshot(index int, snapshot []byte) {
// Your code here (2D).
DPrintf("{Node %v} wants to snapshot at index %d",rf.me,index)
firstIndex := rf.getFirstEntry().Index
if index <= firstIndex {
DPrintf("{Node %v} cannot make snapshot at %d index",rf.me,index)
return
}
rf.deleteObsoleteLog(index)
rf.persister.SaveStateAndSnapshot(rf.encodeState(),snapshot)
}
// 如果此时的firstEntry的index = 10,lastEntry = 14
// 新的数组长度就应该是logLen := rf.getLastEntry().Index - index + 1,
// 后面要+1的目的是,注意raft的log[0]是一个nil的日志
// 所以在上面的例子中,新的log长度就是 14 - 10 + 1 = 5
func (rf *Raft) deleteObsoleteLog(index int) {
logLen := rf.getLastEntry().Index - index + 1
newLog := make([]Entry,logLen)
copy(newLog,rf.log[index - rf.getFirstEntry().Index:])
rf.log = newLog
rf.log[0].Command = nil
}
这样一来,还有别的地方要修改下标,比如说在同步日志的时候,rf.log[prevLogIndex].Term
已经不适用了,比如说prevLogIndex = 10,但是实际上len(rf.log) = 3
。
就会发生数组越界,所以这里应该是rf.log[args.PrevLogIndex - rf.getFirstEntry().Index].Term
还有一些地方也要做类似的修改。具体就看代码中的实现吧。
待续
目前就做完了basic部分,后面的debug比较困难,有时候日志打的很长才能看到问题。有时间再补。
在snapshot这一个函数里,碰到了一个错误,不知道作者您碰到过没,就是您上面第19行的rf.log[index – rf.getFirstEntry().Index:],偶尔会报错,slice[10:8],一直没查出来原因,而且只是偶尔会有这个错误。
我没有运行过几百上千次,6.824比较难做,手动运行多几次都OK我就没管了。如果单单这个问题,slice[10:8]就是数组越界问题,切片的起始index要小于end index。我当时没有遇到,当然极有可能是我的代码存在问题。你可以在修改数组下标的位置打印出来看看。
你如果需要的话,我可以把我的代码实现给你看下,但是我好久没做了,看日志太痛苦了,后面秋招也比较忙就搁置了
没搞懂Snapshot函数的第二个参数snapshot的作用是什么,不需要进行赋值吗?还是说是上层应用负责将index之前的日志保存到snapshot当中
hi,时间太过久远,细节我也忘了好多了,我印象里就是你说的这个意思,因为raft只是保证数据的一致性,上层应用(基于raft的数据库之类的)来确定啥时候要保存快照。