Raft 协议实战系列(四)—— 安全性

3,346 阅读5分钟

本文介绍 Raft 对选主和日志复制的算法限制,以保证其安全性(论文原文是 “Safety”,其实此处翻译为 “正确性” 更合适一些,国内“安全性”一般对应 “Security”,但出于对论文作者的敬意,本文继续使用“安全性”)。

注:本文原创,转载请标明出处。

笔者期望帮助读者深入理解 Raft 协议,并能付诸于工程实践中,同时解读不易理解或易误解的关键点,看完不懂你来拍我😜 。该系列从原理、源码、实践三个部分讲解 Raft 原理及算法:

  • 原理部分结合 Raft 论文讲解 Raft 算法原理,分篇遵循 Raft 的模块化思想,分别讲解 Leader election、Log replication、Safety、Cluster membership change、Log compaction 等。
  • 源码部分分析 hashicorp/raft 来学习一个工业界的 Raft 实现(hashicorp/raft 是 Consul 的底层依赖)。
  • 实践部分基于 hashicorp/raft 实现一个简单的分布式 kv 存储,作为收尾。

系列历史文章链接:


前面的章节我们讲述了 Raft 算法是如何选主和复制日志的,然而到目前为止我们描述的这套机制还不能保证每个节点的状态机会严格按照相同的顺序 apply 日志。想象以下场景:

  1. Leader 将一些日志复制到了大多数节点上,进行 commit 后发生了宕机。
  2. 某个 follower 并没有被复制到这些日志,但它参与选举并当选了下一任 leader。
  3. 新的 leader 又同步并 commit 了一些日志,这些日志覆盖掉了其它节点上的上一任 committed 日志。
  4. 各个节点的状态机可能 apply 了不同的日志序列,出现了不一致的情况。

因此我们需要对“选主+日志复制”这套机制加上一些额外的限制,来保证状态机的安全性,也就是是 Raft 算法的正确性。

1. 对选举的限制

我们再来分析下前文所述的 committed 日志被覆盖的场景,根本问题其实发生在第2步。Candidate 必须有足够的资格才能当选集群 leader,否则它就会给集群带来不可预料的错误。Candidate 是否具备这个资格可以在选举时添加一个小小的条件来判断,即:

每个 candidate 必须在 RequestVote RPC 中携带自己本地日志的最新 (term, index),如果 follower 发现这个 candidate 的日志还没有自己的新,则拒绝投票给该 candidate。

Candidate 想要赢得选举成为 leader,必须得到集群大多数节点的投票,那么它的日志就一定至少不落后于大多数节点。又因为一条日志只有复制到了大多数节点才能被 commit,因此能赢得选举的 candidate 一定拥有所有 committed 日志

因此前一篇文章我们才会断定地说:Follower 不可能比 leader 多出一些 committed 日志。

比较两个 (term, index) 的逻辑非常简单:如果 term 不同 term 更大的日志更新,否则 index 大的日志更新。

2. 对提交的限制

除了对选举增加一点限制外,我们还需对 commit 行为增加一点限制,来完成我们 Raft 算法核心部分的最后一块拼图。

回忆下什么是 commit:

当 leader 得知某条日志被集群过半的节点复制成功时,就可以进行 commit,committed 日志一定最终会被状态机 apply。

所谓 commit 其实就是对日志简单进行一个标记,表明其可以被 apply 到状态机,并针对相应的客户端请求进行响应。

然而 leader 并不能在任何时候都随意 commit 旧任期留下的日志,即使它已经被复制到了大多数节点。Raft 论文给出了一个经典场景:

图1 *图1

图1从左到右按时间顺序模拟了问题场景。

阶段a:S1 是 leader,收到请求后将 (term2, index2) 只复制给了 S2,尚未复制给 S3 ~ S5。

阶段b:S1 宕机,S5 当选 term3 的 leader(S3、S4、S5 三票),收到请求后保存了 (term3, index2),尚未复制给任何节点。

阶段c:S5 宕机,S1 恢复,S1 重新当选 term4 的 leader,继续将 (term2, index2) 复制给了 S3,已经满足大多数节点,我们将其 commit。

阶段d:S1 又宕机,S5 恢复,S5 重新当选 leader(S2、S3、S4 三票),将 (term3, inde2) 复制给了所有节点并 commit。注意,此时发生了致命错误,已经 committed 的 (term2, index2) 被 (term3, index2) 覆盖了。

为了避免这种错误,我们需要添加一个额外的限制:

Leader 只允许 commit 包含当前 term 的日志。

针对上述场景,问题发生在阶段c,即使作为 term4 leader 的 S1 将 (term2, index2) 复制给了大多数节点,它也不能直接将其 commit,而是必须等待 term4 的日志到来并成功复制后,一并进行 commit。

阶段e:在添加了这个限制后,要么 (term2, index2) 始终没有被 commit,这样 S5 在阶段d将其覆盖就是安全的;要么 (term2, index2) 同 (term4, index3) 一起被 commit,这样 S5 根本就无法当选 leader,因为大多数节点的日志都比它新,也就不存在前边的问题了。

以上便是对算法增加的两个小限制,它们对确保状态机的安全性起到了至关重要的作用。

至此我们对 Raft 算法的核心部分,已经介绍完毕,如果您坚持阅读到本文,相信已经能很好掌握 Raft 协议的核心算法机制。

下一篇我们会介绍两个同样描述于论文内的辅助技术:集群成员变更和日志压缩,它们都是在 Raft 工程实践中必不可少的部分,协助读者在源码分析前充分掌握核心原理。

欢迎转发、关注笔者微信公众号:Q的博客。 不定期发送干货,实践经验、系统总结、源码解读、技术原理。