【MIT 6.5840/6.824】Lab3 Raft
Raft
- 3A
- 3B
- 思路
- 注意点
- 3C & 3D
3A
3A的目的就是实现Leader的选举,总体来说不复杂。可分为几个部分进行实现:
首先,需要实现RequestVoteRPC,用于Candidate向其他server发起选举请求,具体实现方案论文中的Figure 2已写得很清楚了,便不再赘述。
其次,需要实现AppendEntriesRPC,用于Leader向Follower发送心跳信号(在3A中,AppendEntriesRPC只需用于发送信号),也是较为简单,不再赘述。
最后,可以利用代码给出的ticker函数,模拟Leader的心跳,以及判断Follower和Candidate是否超时,从而进行Follower变为Candidate的状态转换。当Follower转为Candidiate时,我们需要广播RequestVoteRPC发起选举,建议抽象出一个负责广播的函数,并在其中并发发送RPC,在收集到所需票数后,可在其中进行Candidate变为Leader的状态转换。
3B
思路
3B对比3A,其实在思路上来看也没有很复杂,只是实现起来,需要注意的细节多了一些。
首先,进一步实现AppendEntriesRPC,支持日志同步功能。
其次,将日志同步结合进行Leader心跳之上。起初,我将心跳与日志同步分开了,搞得很复杂。但其实,它们俩都同属于AppendEntriesRPC,理应合为一体。于是,我们只需在广播心跳时,顺带把最新的日志同步至leader即可。也就是说,当用户调用Start函数时,我们不需要立刻同步日志,而是跟随心跳,每一次同步所有新添加的日志(这样做恰好可以减少所需的RPC次数)。
最后,Start函数的实现、commitIndex的更新、日志的apply都比较简单,就不赘述了。
注意点
- 在测试的时候,我发现TestBackUp3B一直过不去,总会出现one函数fail掉的情况。经过调试发现,是同步日志的过程太慢了。因为这个测试用例中,会有很多没用的幽灵日志。当
AppendEntries失败时,如果nextIndex是一步一步往后移动的话,需要很多次RPC才能够成功同步日志,所以需要采用论文中所提到的优化,每次按照一个Term去跳。(见论文第7页末到第八页首) AppendEntries中,如果PrevLogIndex所指向的日志冲突了,那么需要删除自此之后的冲突条目,以防止follower提交了未同步的日志- Raft State中
log[]的起始索引是1!!!(论文里面说的,但是写的时候容易从0开始计算索引) - Leader或Candidate收到RPC Reply(无论是哪种RPC)的时候,都得判断一下Reply中的Term是否比自身的更新(因为它可能断连之后重连,系统中已经有新的leader了)
- 在apply日志时,需先把待apply的日志处理出来,然后解开锁,再发送至管道applyCh中。如果持有锁去写管道,会阻塞较久,从而影响系统中其他部分的运行,可能导致整体超时。
3C & 3D
3C就不多说了,将Figure 2中提到的需要持久化的状态进行持久化即可。(为什么持久化?这个问题很简单,因为需要支持崩溃恢复,服务器crash后,内存中的状态会丢失,而不是所有的内存状态就需要持久化,故只持久化必要状态。)
3D需要我们实现Snapshot去压缩日志。
- 在
persist的时候,如果snapshot为空,不能直接Save(raftstate, nil),而需要从persist中读取出快照,再Save - 在
readPersist时,需要恢复commitIndex与lastApplied的状态,不然它们俩都会被初始化为0,从而导致一些错误
