6.824 Lab2B Raft

2022.12.18 完成了 Lab2B内容是实现 Raft 的共识算法经过 200 轮测试的检验没有发现问题我就当作实验完成了这里记录一下过程中发现的比较有意思的 bug它们不一定很难修改很多也都明确地写在 Raft paper 的 Figure 2 中但是去思考它们为什么会导致错误什么情况会触发这些错误是很有意思的一件事并且能帮助我们更好地理解 Raft 这个系统想到读 paper 时没有想到的问题

Livelock

有时会发生这种情况系统虽然在线但不再做有效的工作连心跳都不发送打印许多日志后发现集群由3台机器组成一直在选举而且永远是同一个机器竞选而另外两台机器拒绝投票因为 Candidate 没有通过 up-to-date 检查Raft paper 5.4.1这个 bug 错在 RequestVote RPC 的接收方不加判断地更新重新选举定时器正确地做法是仅在投票时更新定时器拒绝投票时不更新定时器来保证自己可以正确地超时启动竞选在这里Candidate 没有通过任何 Follower 的 up-to-date 检查说明它无法竞选成功要使系统继续工作需要另外两台机器之一超时启动竞选

类似地AppendEntry RPC 应当在 RPC term 没有过期时更新定时器注意这不要求 RPC 成功因为 RPC 可能因为 log 不一致而失败这时 Leader 会自减 nextIndex[] 并重试这仍然构成一个 Heartbeat应当更新定时器

错误地认为 log 冲突导致不必要的删除

在 Raft Paper Figure 2 中AppendEntry Step 3是

If an existing entry conflicts with a new one (same index but different terms), delete the existing entry and all that follow it.

我曾经在比较 term 时出错用 existing entry 的 term 与 AppendEntry 的 term 比较了正确的做法与 AppendEntry 中新的 Log Entry 的 term 进行比较这可能会导致不必要的删除日志因为传输的新 Log Entry 不一定是当前 term 的内容也可能是之前 term 的内容

这会导致问题的场景是如果 AppendEntry 在网络中经历了较高的延迟RPC 可能已经过期正确做法下两条 log 不会冲突所以不会删除而这个错误会导致删除这条以及其后所有的 log而我们很可能之前已经告诉了 Leader 我们拥有那条 log如果集群中拥有这条 log 的机器数刚好过半Leader commit 并应用到状态机上这时我们删除了这条 log拥有 log 的机器数就不再过半如果此时 Leader 再离开集群重新选举的 Leader 就不一定包含这条 log 了正常情况下新Leader 包含这条 log 这件事由 5.4.1 的 up-to-date check 保证也就是 committed log 被回滚了

助教的这篇博文也提到了这一问题

这个 bug 是我阅读代码发现的这可能并不好测试根据我的理解6.824 的测试用例并没有模拟乱序的网络通信

CommitIndex 的激进更新导致某些机器做出错误 commit

问题在于TestBackup2B 会有很小的概率失败原因是某台机器作出的 commit 和其他机器已经作出的不一致这个问题困扰了我很久直到我离开电脑准备睡觉时才想到这个问题可能的原因在 Raft Paper Figure 2 中AppendEntry Step 5 是

If leaderCommit > commitIndex, set commitIndex = min(leaderCommit, index of last new entry).

这个 new 很重要我的错误在于直接用 log 长度更新 commitIndex正确的做法是用 AppendEntry RPC 中携带的最新 Entry 的下标来更新 commitIndex这会导致 commitIndex 的更新过于激进后面的尚未确认与 Leader 同步的 log 也会被 commit这看起来应该经常出错为什么在 TestBackup2B 中很少出现呢因为带有错误 log 的机器在连接到正确集群时Leader 会不断地自减 nextIndex[] 并重试 AppendEntry RPC直到在某处 Log 吻合这时该 Leader 会第一次更新该 Follower 的 commitIndexcommitIndex 首次被更新时 AppendEntry RPC 通常携带新的 Entry而这个 Entry 和 RPC 接收方的 log 是冲突的所以 log 都在步骤 3 中被清空了于是步骤 5 中 index of last new entry 也就是 log 的长度那什么时候才会触发这个错误呢答案是 commitIndex 首次被更新时接收到的是心跳不带 Entry 的 AppendEntry RPC它不会清空后面的错误的 log而且还会更新 commitIndex这就要求心跳刚好在 nextIndex[] 减少到正确值的时候发送所以出现的概率很低

要定位这个 bug 还是很困难的因为这个问题出现的概率真的很低跑50次出1次问题左右每跑一次都需要等很久而且每次 commit 都打印一条信息导致输出很长推荐一篇文章Debugging by Pretty Printing是 6.824 的一名助教写的帮助我们更好地用日志调试分布式系统

Committing entries from previous terms

我在 Leader 上的 CommitIndex 更新采用的方法是matchIndex[] 中第 n/2+1 大的数这时要注意这条 log 还需要属于当前 term否则无法宣布 commit也就是不能 commit 之前 term 的日志原因在 Raft paper 的 5.4.2 节和 Figure 8 中有详细说明