14. Raft PartC 状态持久化
实现要求
PartA 和 PartB 基本实现了不宕机情况下的领导者选举、日志同步和日志应用逻辑。但是在当某个 Peer 异常重启后,是不能正常重新加入集群的。为此,需要将 Raft 的关键信息定时持久化,重启后加载,以保证正常加入集群。
至于哪些状态需要持久化,论文图 2 都有标记。这些需要持久化的属性集中任何一个属性发生变化,都需要进行持久化,因为 Peer 任意时刻都有可能宕机。在本课程中,我们不会真将状态持久化到硬盘上,而是使用测试框架中的 persistor.go 中的 Persister 类对数据进行“持久化”,主要涉及两个接口:
1 | func (ps *Persister) ReadRaftState() []byte {} |
ReadRaftState 是反序列化接口,可以认为是从磁盘上读出一个字节数组;Save 是序列化接口,负责将字节数组形式的快照(PartD 才用)和 raft 状态写入磁盘。
写完后在 src/raft 文件夹,使用 go test -run PartC -race 来测试代码逻辑是否正确、是否有数据竞态。
实现要点
从上面的接口可以看出,你还需要一个序列化工具,将 Raft 状态序列化为字节数组。可以使用 tester 中提供的 labgob 包,这是一个很简易的序列化和反序列化类。注释中有给一些简单的例子来说明 labgob 如何用。
序列化:
1 | w := new(bytes.Buffer) |
反序列化:
1 | r := bytes.NewBuffer(data) |
也就是说,核心在于利用 labgob 的接口,来实现 :
1 | func (rf *Raft) persist() {} |
两个函数。并且在合适的地方调用 persist 持久化 raft 状态。
以下是一些实现注意点:
labgob序列化的 struct 中的所有字段必须都要首字母大写。Persister.Save的第二个Snapshot参数在 PartC 中传 nil 就可以。- 每次需要持久化的字段发生改变时,都记得调用
rf.persist()。 - PartC 可能会让你 PartA 和 PartB 中有些实现的不好的地方暴露出来,毕竟前两个测试也不能面面俱到。
- PartC 会加大日志的写入量。而在日志量大且有的 Peer 宕机很久时,在日志同步时如果只是一个 entry 一个 entry 的后退,可能会导致在规定时间内完不成测试。一个 term 一个 term 的退可能也不够,下面依据论文第七页底到第八页头的叙述,给出了一种优化方法。
日志同步时“试探阶段”的优化。主要思路是将 Follower 的一些日志信息在 AppendEntriesReply 时返回给 Leader。Leader 根据这些信息来快速决定下次的 nextIndex。假设当 Follower 因 prevLog 冲突而拒绝 Leader 时,有如下信息:
1 | XTerm: 空,或者 Follower 与 Leader PrevLog 冲突 entry 所存 term |
则 Leader 的逻辑可以为:
1 | Case 1: Follower 的 Log 太短了:(迅速退到和 Follower 同长度) |
因此,这一节的主要考察点除了正确的持久化,最重要的就是对日志同步性能的优化。
