实现要求

PartA 和 PartB 基本实现了不宕机情况下的领导者选举、日志同步和日志应用逻辑。但是在当某个 Peer 异常重启后,是不能正常重新加入集群的。为此,需要将 Raft 的关键信息定时持久化,重启后加载,以保证正常加入集群。

至于哪些状态需要持久化,论文图 2 都有标记。这些需要持久化的属性集中任何一个属性发生变化,都需要进行持久化,因为 Peer 任意时刻都有可能宕机。在本课程中,我们不会真将状态持久化到硬盘上,而是使用测试框架中的 persistor.go 中的 Persister 类对数据进行“持久化”,主要涉及两个接口:

1
2
func (ps *Persister) ReadRaftState() []byte {}
func (ps *Persister) Save(raftstate []byte, snapshot []byte) {}

ReadRaftState 是反序列化接口,可以认为是从磁盘上读出一个字节数组;Save 是序列化接口,负责将字节数组形式的快照(PartD 才用)和 raft 状态写入磁盘。

写完后在 src/raft 文件夹,使用 go test -run PartC -race 来测试代码逻辑是否正确、是否有数据竞态。

实现要点

从上面的接口可以看出,你还需要一个序列化工具,将 Raft 状态序列化为字节数组。可以使用 tester 中提供的 labgob 包,这是一个很简易的序列化和反序列化类。注释中有给一些简单的例子来说明 labgob 如何用。

序列化:

1
2
3
4
5
6
w := new(bytes.Buffer)
e := labgob.NewEncoder(w)
e.Encode(rf.xxx)
e.Encode(rf.yyy)
raftstate := w.Bytes()
rf.persister.Save(raftstate, nil)

反序列化:

1
2
3
4
5
6
7
8
9
10
11
r := bytes.NewBuffer(data)
d := labgob.NewDecoder(r)
var xxx
var yyy
if d.Decode(&xxx) != nil ||
d.Decode(&yyy) != nil {
// error...
} else {
rf.xxx = xxx
rf.yyy = yyy
}

也就是说,核心在于利用 labgob 的接口,来实现 :

1
2
func (rf *Raft) persist() {}
func (rf *Raft) readPersist(data []byte) {}

两个函数。并且在合适的地方调用 persist 持久化 raft 状态。

以下是一些实现注意点:

  1. labgob 序列化的 struct 中的所有字段必须都要首字母大写。
  2. Persister.Save 的第二个 Snapshot 参数在 PartC 中传 nil 就可以。
  3. 每次需要持久化的字段发生改变时,都记得调用 rf.persist()
  4. PartC 可能会让你 PartA 和 PartB 中有些实现的不好的地方暴露出来,毕竟前两个测试也不能面面俱到。
  5. PartC 会加大日志的写入量。而在日志量大且有的 Peer 宕机很久时,在日志同步时如果只是一个 entry 一个 entry 的后退,可能会导致在规定时间内完不成测试。一个 term 一个 term 的退可能也不够,下面依据论文第七页底到第八页头的叙述,给出了一种优化方法。

日志同步时“试探阶段”的优化。主要思路是将 Follower 的一些日志信息在 AppendEntriesReply 时返回给 Leader。Leader 根据这些信息来快速决定下次的 nextIndex。假设当 Follower 因 prevLog 冲突而拒绝 Leader 时,有如下信息:

1
2
3
XTerm:  空,或者 Follower 与 Leader PrevLog 冲突 entry 所存 term
XIndex: 空,或者 XTerm 的第一个 entry 的 index
XLen: Follower 日志长度

则 Leader 的逻辑可以为:

1
2
3
4
5
6
Case 1: Follower 的 Log 太短了:(迅速退到和 Follower 同长度)
nextIndex = XLen
Case 2: Leader 没有 XTerm:(以 Follower 为准迅速回退跳过该 term 所有日志)
nextIndex = XIndex
Case 3: Leader 存在 XTerm:(以 Leader 为准,迅速回退跳过该 term 所有日志)
nextIndex = Leader 在 XTerm 第一个 entry 的 index

因此,这一节的主要考察点除了正确的持久化,最重要的就是对日志同步性能的优化。