本文试着讲解和梳理 Raft 算法的基本原理和工程优化,谈一谈个人对于 Raft 算法的理解,主要参考 Raft 作者的博士论文 web.stanford.edu/~ouster/cgi... 以及分布式数据库 etcd 中的 raft 模块 etcd-io/raft: Raft library for maintaining a replicated state machine (github.com)。
全文分为一下几个章节,也是参考 Raft 作者博士论文的结构:
- Raft 基础 (Raft basics)
- 领导者选举 (Leader election)
- 日志复制 (Log replication)
- 安全性保障 (safety)
- 成员变更 (Cluster membership changes)
- 日志压缩 (Log compaction)
- 客户端交互 (Client interaction)
本文中出现的图片部分来自 Raft 作者的博士论文。
Raft 基础 (Raft basics)
在一个 Raft 集群(Cluster)中,可以包含一个或多个 server。Server 的数量通常是较小的奇数,比如五个或七个。Raft 保证在这样一个集群中,少数派的 server 不可用不会影响到这个 cluster 的可用性。比如一个 cluster 有五个 server,那么即使其中两个 server 处于不可用的状态,cluster 仍然是可用的。
这里的 server,从 Raft 的角度看,是运行了 Raft 算法的程序,Raft 算法本身并不关心这些 server 是不是在一个物理机器上。
Server 不可用,指的是 server 本身停止工作 (fail by stop),或者 server 由于网络分区 (network partition) 而无法被 client 或者其他 server 访问到:在一个异步分布式系统中,无法区分两者。不可用的 server 可能会在之后恢复并重新加入到 cluster 中。
每一个 server 在任一时刻都处于以下三种状态 (state) 中的一种
-
领导者 (Leader): 这个 cluster 的 leader,负责和 client 交互等逻辑。
-
跟随者 (Follower): Leader 的 follower,接受来自 leader 的数据拷贝。
-
候选者 (Candidate): Follower 成为 leader 的中间状态。
三种状态之间的转换关系如下图所示,在 Leader Election 章节中会对其关系有更详细的讨论。
除了 state,每个 server 还维护着一个任期 (term)。Term 是一个在每个 server 上单调递增的数字,在 Raft 中扮演者逻辑时钟的角色:物理时钟在分布式系统中是不可靠的。Raft 保证在任一一个 term 中,最多只有一个 leader 。通过 term,Raft 将时间切分成了选举时间段和可用时间段,如下图所示。
在 term 1 中,首先经过蓝色的选举时间段,选出了一个 leader,因此整个 cluster 进入绿色的可用时间段。在 Raft 中存在一种可能性:在一个 term 中并没有选举出一个 leader,比如 term 3,在这种情况下 cluster 会继续进行选举,直到选举出一个 leader。
各个 server 之间传递消息时,会附带 server 当前的 term,从而让接受消息的 server 能够判断出这个消息是否已经过时。
领导者选举 (Leader election)
每一个 server 在启动时都初始化为 follower 状态。处于 follower 状态的 server 会维护一个选举超时计时器。如果在计时器到期之前,follower 没有收到来自其他 leader 或者 candidate 的比自己当前 term 更高的消息,那么这个 follower 就会转化为 candidate 状态并让自己的 term 加一。Candidate 会向 cluster 中的其他 server 发送 RequestVote 消息,要求其他 server 为自己投票。当收到了超过半数的 server 的同意回复之后,candidate 将会转变为 leader。
收到 RequestVote 请求的 server,如果请求中的 term 大于等于自己的 term,并且在当前自己的 term 中还没有投过票,那么就同意 RequestVote 请求,反之则拒绝。
通过这样一套投票机制,Raft 保证在一个 term 中最多有一个 leader 被选举出来。为什么说是最多呢?因为存在一种可能,在一轮选举中有多个 candidate,并且这些 candidate 各自获得了相同的票数。考虑一个五个 server 的 cluster,当 leader 5 不可用之后,follower 1 和 follower 2 可能同时开始转变为 candidate并开始选举。Candidate 1 获得了 follower 3 的投票,candidate 2 获得了 follower 4 的投票,没有一个 candidate 获得了多数票,因此在这样一个 term 中,没有 leader 被选举出来。
Raft 通过不断重试选举来解决这个问题:选举会不断进行,直到有一个 leader 在一个 term 中被选举出来。同时, Raft 引入了随机值,通过随机各个 server 上的选举超时时间,避免两个或多个 server 同时转变为 candidate 开始选举投票,降低出现选举活锁的概率。
在任一时间,如果一个 server,不论它处于什么状态,收到了来自其他 server 更高 term 的消息,都会更新自己的 term 为这个更新的 term,并转化为 follower 状态。这时因为收到 term 更高的消息代表当前 server 所掌握的消息已经过时了,server 中可能已经出现了新的 leader,为了保证任一 term 中最多只有一个 leader,过时的 server 必须变成 follower。
日志复制 (Log replicaton)
所有 server 都维护着一个状态机。Leader 接受来自 client 的请求, 将请求转换为日志项 (log entry),持久化在本地磁盘上,并 随后将 log entry 发送给所有的 follower。当 leader 能够确认这个 log entry 已经被保存在大部分 follower 上时,leader 认为这个 log entry 处于一个已提交 (committed) 状态,因此会将这个 log entry 中的指令应用到状态机上,计算出结果,并返回给客户端。
Log 是由连续的 log entry 组成的数组,在 raft 中,每一个 log entry 都有三个属性:
- Log entry 在 log 数组中的位置 index。
- 接受 log entry 的 leader 的 term。
- 客户端请求的内容。
上图所示就是一个 Raft cluster 中 leader 和 follower 所维护的 log。
在 Raft 中,Leader 上已有的 log 永远不会被覆盖或修改。这也就以为着已经被 committed 的 log entry 永远不会被修改或覆盖。关于如何保证这一点,将在下面的 Safety 章节中讨论。
通过保证 leader 上的 log 不变,Raft 保证了一旦一个 entry 被 committed,那么即使发生了 leader 变换,在新的 leader 上这个 entry 也是 committed 的。因此状态机上已经执行的命令不会因为 leader 更换而不同或者"后悔"。
但是对于 follower 来说,log 是会被改变的。Follower 会收到来自 leader 的 AppendEntries 请求,其中包含了 leader 的 log entry。Follower 会使用 leader 的 log entry 覆盖掉自己的 log entry:一切以 leader 的 log 为准。
安全性保障 (Safety)
选举限制 (Election restriction)
仅仅根据上面讨论的方法,已经 committed 的 log entry 是有可能会被覆盖或修改的。
考虑如上图所示的种情况,s1 是 term 3 的 leader,在接受了来自 client 的请求后,将 y <- 7
这个 entry 复制到了 s2 和 s3 上,因为已经复制到了大多数 server 上,所以 s1 认为这个 entry 时 committed 并交给状态机执行并将结果返回给 client。但是在 s1 将 entry 复制给 s4 和 s5 之前,s1 下线了。随后 s5 经过选举成为了新的 server,但是因为 s5 上并没有 index = 6, term = 3
这条 log,所以当其接受到来自 client 的新请求之后,会在 index = 6
这个位置添加新的 entry = 4
的 entry,并将其复制到其他 server 上,从而覆盖掉了之前已经 committed 的 entry。
Raft 解决这一问题的方法是:让 log 最新的那个 candidate 成为 leader。
这也是 Raft 和其他分布式算法,比如 Multi-Paxos 和 Zab 之间不同的地方,Raft 通过限制谁能成为 leader 避免了 leader 上的 log 被覆盖,其他的算法通过将所有 server 上最新的 log 汇聚到 leader 上避免 leader 上的 log 被覆盖。
Candidate 在发送 RequestVote 请求时,必须附带上它的 log 中最后一个 entry 的 index 和 term。Server 在收到 RequestVote 之后,如果发现自己的 log 中最后一个 entry 的 index 和 term 比 RequestVote 中的 entry 的 index 和 term 要新,那么就拒绝掉这个请求。这个 server 在之后也会触发选举超时计时器,并作为 candidate 开始选举。
所谓一个 log entry 的 index 和 term 更新,指的是比较两个 entry 的 index 和 term,term 较大者更新,当 term 相同时,index 较大者更新。
回到上图所示的这种情况,当 s1 下线后,只有 s2 或者 s3 可能赢得选举成为 term 4 的 leader。而 s2 和 s3 上已经有了之前复制到了多数 server 上的 committed 的 entry,从而避免了 committed 的 log entry 被覆盖。
提交之前 term 的 entry (Committing entries from previous terms)
即使进行了选举限制,如果一个 leader commit 了之前 term 的 entry,这个被 commit 的 entry 也是有可能会被覆盖掉的。
考虑下图所示的情况
流程是这样的
-
S1 是 leader,将
index = 2, term = 2
的 entry 复制到 s2 后下线,如图 a。 -
S5 获得 s3 和 s4 的投票成为 term 3 的 leader,接受 client 请求生成
index = 2, term = 3
的 entry,但是还没来得及把这个 entry 复制到其他 server 上,s5 就下线了,如图 b。 -
S1 经过 s2 和 s3 的投票,成为 term 4 的 leader。S1 认为它可以直接 commite 之前 term 3 的 entry,所以讲
index = 2, term = 2
的 entry 继续复制到了 s3 上,并将其 commit,随后下线。注意此时 s1 还没有在当前 term 4 上 commit 任何一个 entry,如图 c。 -
S5 此时重新上线,获得 s3 和 s4 的投票,成为 term 5 的 leader。和 s4 一样,s5 认为它可以 commit 之前 term 的 entry,并将
index = 2, term = 3
的 entry commit,覆盖了已经 committed 的index = 2, term = 2
的 entry,如图 d1.
所以这里问题的本质是,一个 leader (s5)即使在自己的 log 中看到了之前 term 的 entry (index = 2, term = 3
),也不能确定这个 entry 是不是已经 committed 了的。
Raft 解决这个问题的方法是要求一个 leader 绝对不要 commit 之前 term 的 entry:leader 只 commite 当前 term 的 entry。比如对于图 c 中的 s1 来说,在它成为 leader 之后,直到 index = 3, term = 4
的 entry 被 commit 后,index = 2, term = 2
的 entry 才被 committed,这样即使 s1 在 commit index = 3, term = 4
的 entry 之前下线,并且 s5 再度成为 leader,因为 index = 2, term = 2
的 entry 并没有被 committed,也没有被交给状态机执行,也不会有任何 committed entry 被覆盖的情况发生了。
在实际实现中,每个 server 在成为 leader 后,会 commit 一个当前 term 的空的消息。
至此,我们已经讨论完了所有 Raft 算法的基础情况,可以看到 Raft 通过把共识问题分解为领导者选举,日志复制,和安全性保障三方面,很大程度简化了共识算法在理解上的复杂性。后面我们会探讨讲在实际应用中使用 Raft 算法还需要做哪些事情。
成员变更 (Cluster membership changes)
成员变更是所有共识算法都要考虑的问题,因为在一个实际的分布式系统中,不可避免要更换系统中的成员,比如给某些机器上面安装操作系统安全补丁,或者单纯地更换硬件,这些都会造成一个 cluster 内成员的增加或减少。
如上图所示,s1, s2, s3 是现有的集群中的三个节点,当我们通知集群去增加另外两个节点 s4 和 s5 的时候,如果让所有 server 都在新的集群成员配置的时刻立刻应用,在一个 term 中,就有可能出现 s1 和 s2 仍然认为集群中只有三个 server,并选举出 s1 作为 leader (由 s1 和 s2 投票产生),而 s3,s4 和 s5 认为集群中有五个 server,并选举出 s5 作为 leader (由 s3,s4 和 s5 投票产生):集群中同一个 term 出现了两个 leader。
为了解决这个问题,Raft 在成员变更上有两种方式,单个成员变更和联合共识成员变更。Raft 作者在最开始的论文 In Search of an Understandable Consensus Algorithm 中首先提出了联合共识成员变更,即一次可以增加或益处集群中的多个成员。随后在他的博士论文中,提出并推荐了单个成员变更,因为单个成员变更更加简单并易于实现,这很符合 Raft 算法的设计思想:易于理解。
有趣的是,在 etcd 的实现中,是首先实现了单个成员变更,后面又实现了联合共识成员变更。
单个成员变更 (Single membership change)
单个成员变更的思想非常简单,那就是一次只增加或移除一个 server。
如图所示,这次我们只向集群中新增一个 server s4,就可以保证在任一一个时间节点,集群中都不可能出现两个 leader:
- T1: 此时 s4 还未加入集群,s1,s2 和 s3 都认为集群中只有 s1,s2 和 s3,会从中选举出唯一一个 leader。
- T2: 此时 s4 启动,但 s1,s2 和 s3 仍然没有感知到 s4 的存在,因此只会从 s1,s2 和 s3 中选举出唯一一个 leader。
- T3: 此时 s3,和 s4 都认为集群中有四个 server。我们分开考虑每个 server 成为 leader 的可能性:
- S1 获得来自 s2 的投票成为 leader,此时 s3 和 s4 都不能成为 leader,因为 s1 和 s2 已经参与了投票,而 s3 或 s4 想要成为 leader 需要至少三票。
- S2 同理 s1。
- S3 获得来自 s2,s4 的投票成为 leader,那么 s1 就不能成为 leader,因为 s2 和 s3 都已经完成了投票。
- S4 同理 s3。
T4 和 t5 就不再赘述。删除节点的场景也不再赘述。
在单个成员变更中,old 集群中的大多数 server,和 new 集群中的大多数 server 必然存在相互重叠,即必然存在一个 server 即在 old 集群中,又在 new 集群中,从而保证了集群不会分裂出两个多数派。
回到算法层面,当使用单个成员变更时,server 把这个新的配置当作一个 log entry 进行处理,处理流程和普通的 entry 一致,包括由 leader 将其复制到其他节点上,存储并 commit。值得注意的是,Raft 论文中提到 server 可以在接受到新的集群成员配置 entry 时,立刻开始使用新的配置,无需等待这个 entry 被 committed。根据我们前面的讨论,这么做是没有安全性问题的。
在 etcd 的实现中,单个成员变更(以及联合共识成员变更)是在其对应的 entry 被 committed 之后才生效的。
联合共识成员变更 (Joint consensus membership changes)
联合共识成员变更相比单个成员变更,允许在一次变更中增加或移除(也可以混合)多个 server。
联合共识成员变更中,除了现有的成员配置 C_old 和新的成员配置 C_new 之外,还存在一个中间状态 C_old_new,它同时包含了 C_old 和 C_new,并且感知到了 C_old_new 的 server,在成为 candidate 进行 leader 选举时,需要在一个 term 中同时分别获得 C_old 和 C_new 中大多数 server 的投票才能成为 leader。
因此如上图所示,通过这种方法,在成员变更的过程中,存在一个中间时刻,C_old 和 C_new 是同时生效的。而这个同时生效的时间段的存在,就避免了在一个时间点集群内部出现两个自认为是多数派的小团体。
学习者 (Leaner)
集群在刚刚完成新增成员的配置变更时,集群的可用性会更容易受到影响。
考虑上图中的两种情况。
在图 a 中,集群刚刚新增了一个成员 s4,此时 s4 中的 log 远远落后于其他 server。此时如果一个 server,比如 s3 变得不可用,那么整个集群将无法继续服务,commit 新的 entry。这是因为想要 commit 新的 entry,需要至少三个 server 复制了这个新的 entry,显然 s4 在一段时间内做不到,因为 s4 必须要从 leader 获取新的 entry 之前所有的 entry 之后,才能 commit 这个新的 entry。所以直到 s4 的 log 追赶上其他 server 之前,整个集群都无法 commit 新的 entry。
在图 b 中,如果在一个拥有三个 server 的集群中快速地增加了额外三个 server,那么直到这三个新的 server 的 log 追赶上原有的 server 为止,集群都无法 commit 新的 entry。
为了解决这个方法,Raft 论文中提到可以在完成成员变更之前,增加一个新的阶段,允许新加入集群的 server 像 follower 一样接受来自 leader 的 log entry,但是不具有投票权,也不能成为 candidate,也不影响 leader 对于 entry commit 与否的判断(不作为集群中的一部份影响多数派判断),直到这些新 server 的 log 追赶上其他的 server,才转变为 follower。
这一提议在 etcd 中被实现为了 Learner: etcd learner design | etcd
预投票 (Pre-Vote)
集群在刚刚完成移除成员的配置变更时,集群的可用性也会更容易收到影响。
考虑在一个拥有四个 server 的集群中移除掉 s4。当 s4 被移除后,在将它和其他 server 从网络上隔离开来,或者将其下线之前,它将不再收到来自 leader 的 heartbeat,并触发选举超时,成为 candidate 并像其他的 server 发送消息。显然 s4 不会赢得选举,因为新的 leader 已经 commit 了新的 entry,这个 entry 在 s4 上时不存在的。但是 s4 会契而不舍地反复重试选举,自增 term,并要求其他 server 为其投票。这会造成本来正常运行的拥有三个 server 的集群的 leader 频繁切换回 follower (因为 s4 的 term 会不断增大),造成整个集群不可用。
Raft 里解决这个问题的方法,是让 server 在成为 candidate 发起选举之前,先发起一轮预选举,即询问其他 server,自己的 log 是不是最新的。只有在获得了大多数 server 的肯定之后,才会真的转变为 candidate 发起选举。而收到预选举请求的 server,并不会因为消息中的 term 比自己的 term 大而改变自己的状态(比如从 leader 变为 follower)。
因此 server 的状态图可以演变为
预投票的思路在于,在真的发起一轮会影响集群可用性的投票选举之前,先演习一遍,只有在演习结果符合预期(如果真的发起投票选举,自己能够成为 leader)的情况下,才真正地发起选举,避免影响集群中其他的 server。
因此预投票除了能够(几乎)解决被移除成员对集群可用性造成的负面影响意外,还能够避免当一个 server 在被网络分区隔离之后重新加入集群时,造成的无意义的 leader 选举。
一个被隔离的 server 重新加入集群时,如果不进行预投票,而直接发起投票的话,因为其自身的 term 会比较大,会造成正在运行的 leader 变为 follower,cluster 会开始一轮新的选举,而这样的选举是没有意义的,只会影响集群的可用性。
领导者租约 (Leader lease)
前面我们说到,预选举能够"几乎"解决被移除成员对集群可用性的负面影响。是的,预选举不能完全解决这个问题。因为预选举能够解决问题的前提是被移除的 server 不能赢得选举,这个前提不是一定能够满足的。
考虑上图的情况,s1 被移除,s4 是新的 leader。那么存在这样一种可能性,在 s4 还未将属于它的 term 的新 entry 复制到 s2 和 s3 之前,s1 发起了预选举,获得了 s2 和 s3 的预投票,从而发起选举投票,迫使 s4 变成 follower。
在这种情况下,Raft 的作者认为不存在单纯依靠比较 log 就能就觉问题的方法。所以换一个思路,不从 candidate 的角度,而是从 follower 的角度去解决问题。
因此 Raft 引入了领导者租约这一机制。如果一个 follower,在选举过期超时之前,曾经收到过 leader 发来的消息,那么即使有一个 candidate 要求 follower 为它更高的 term 进行投票(或预投票),follower 也会拒绝投票,并且无视掉这个更高的 term:因为已经有一个可用的 leader,这个 candidate 的投票请求很可能是无效的。这相当于是在每一个 follower 上为 leader 维护了一个租约,这个租约通过接受来自 leader 的 heartbeat 续期,在选举超时之前,对于 follower 来说,有唯一一个 leader 持有这个租约。
回到上图中的情况,当被移除的 s1 进行预投票时,只要在 s2 和 s3 上 leader s4 的租约还在有效期内,s2 和 s3 都会拒绝为其投票。
在 etcd 中,leader lease 和 CheckQuorum 是一起使用的。CheckQuorum 机制要求 leader 在无法收到来自大多数 follower 的回复时,自动转化为 follower。
这是因为领导者租约的存在,要求 leader 在发现自身已经不再具备成为 leader 的必要条件时,应当主动放弃领导权,否则会造成集群中的一部份 follower 拒绝响应新的 leader。
领导者转移 (Leadership Transfer)
分布式系统中,有时需要允许集群的管理者制定哪个 server 成为 leader。有可能管理者希望运行在一台高性能机器上的 server 成为 leader,来提高系统的性能。
Raft 并没有设计一套独有的机制在做领导者转换,而是在现有的 leader election 框架里面做文章实现变更领导者。
简单来说,当 leader A 接受到了要将领导权转一个另一个 server B 的请求后,
- 停止接受来自 client 的消息。
- 将 log 完全复制到 server B 上。
- 发送一个
TimeoutNow
请求到 server B 上。 - Server B 在收到请求后,立刻转变为 candidate 状态并开始选举 (election timeout)。
- 其他 server (包括 leader A) 在收到 server B 包含更高 term 的选举请求之后,会投票从而使 server B 成为 leader。
因为 server B 是第一个开始发送选举请求的 candidate,它有很大概率能够赢得选举成为新的 leader。
然而,需要注意的是,这一套领导权转移的操作是有可能失败的,比如出现其他 server 比 server B 更早地触发了选举超时。 因此如果 server B 没有成为 leader,需要重复执行这套流程直到领导权转移成功。
值得注意的是,leader A 在开始领导权转移的时候就停止了接受 client 的 request,这时为了保证在领导权转移阶段减少对 server B 的影响,尽量确保 server B 上的 log 是最新的。因此为了不对集群的可用性教程较大的影响,同时也为了避免让其他 server 的选举超时触发,一次领导权转移一般发生在一个选举超时的最小限时内,比如选举超时的最小限时是 T,所有 follower 的随机选举超时时间一般在 T 到 2T 之间。如果领导权转移在 T 内无法完成,那么 leader A 会恢复 leader 状态,继续接受 client 的请求。
从这里和之前的讨论中我们也能看到,Raft 算法是一个 CP 算法,在 P,也就是网络分区发生的情况下,Raft 保证数据的一致性,牺牲一定的系统可用性。
领导者转移时发送的 TimeoutNow,会和上一节提到的领导者租约产生冲突,因此 server B,或者说所有 server 在收到 TimeoutNow 时,应当忽视领导者租约的限时,直接开始发起选举投票。
日志压缩 (Log compaction)
日志压缩的问题,其实不是一个 Raft 独有的问题,而是任何一个有状态的状态机都会遇到的问题:无限增长的 log entry 会耗尽本地磁盘,同时在重启时延长重放 (Replay) 日志的时间。
通常的解决方法是将状态机的当前状态存储下来,并移除之前已经被应用 (apply) 到状态机上面的 log entry。
比如上图所示,原本的 7 个 log entry,如果前五个 log entry 已经 committed,并且被硬到了状态机上面,那么就可以生成一份 snapshot,记录状态机当前的状态。
至于如何生成 snapshot,其实已经超过了 Raft 应该考虑的范围,更多地是一个应用层的选择,这里就不再赘述了。
客户端交互 (Client interaction)
在 Raft 中,集群和外界的交互都是由 leader 同一负责的,client 会把收到的来自客户端的请求 forward 给 leader,或者拒绝 client 的请求并告知 client leader 的地址。
线性一致性 (Linearizable consistency)
Client 和 有状态的系统交互时,都面临着一个命令可能会被执行多于一次的问题。比如说 client 发送一个 x=x+1
的命令给系统,系统在执行完成之后,返回结果给 client 之前下线了。当它重新上线(在 Raft 里是另一个 leader 被选举出来)时,client 会重试这个请求,因此这个命令会被执行两次,甚至更多次。
Raft 的目标是提供线性一致性服务,应当避免这样的情况发生:即所有的请求只能被执行一次。为了解决这个问题,每一个 client 都应当被赋予一个唯一的 client ID,client 也会给它发送的每一个请求一个唯一的,连续的 request ID。
Stateful 的状态机给每个 client 维护一个 session,session 中记录着已经处理过的来自这个 client 的最后一个 request ID,以及相应请求结果。当一个 server 接受到了一个已经执行过的 request ID,就直接返回 client 告诉它这个命令已经执行过了。
虽然这种机制对 client 有了额外的要求,但是这可能是维护线性一致性最简单高效的做法了。
提高读请求的效率 (Effecient read-only queries)
读请求只查询当前状态机的状态,而不改变状态机的状态,因此我们像避免将读请求也转变为 log entry 走一遍 Raft 的流程来提升读请求的效率,毕竟很多时候应用接受的请求中,读请求的占比都要更大。
但是如果允许一个 leader 直接返还 client 的读请求,会破坏 Raft 的线性一致性,返回过期的数据给 client。比如一个 leader 如果被网络分区隔离,当其余的 server 已经选举出一个新的 leader 并且接受了来自 client 的新的写请求之后,这个被隔离的 leader 如果还继续回复 client 的读请求,那么它返回的其实是过期的数据。
Raft 解决这个问题的办法,就是让 leader 在响应读请求之前,先发送一轮 heartbeat 给其他 server,当它肯定自己仍然是 leader 后,再响应这个读请求。具体的流程是
- Leader 在收到读请求时,记录当前的 commit index 为 readIndex。
- Leader 向其他 server 发送一轮 heartbeat,如果收到了大多数 server 的回复,这名它仍是 leader,并且在 readIndex 这个时间点它也是 leader。
- Leader 等待状态机不断地应用 log entry,直到 readIndex 的 log entry 被状态机应用,就能够响应读请求了。
这个模式在 etcd 中叫做
ReadOnlySafe
,也是默认开启的模式。
显然在想要维持线性一致性的前提下,一个读请求需要至少一轮的 server 间通信。如果实际的应用愿意继续提高读请求的效率,那么 Raft 的 leader 也可以直接响应读请求。因为领导者租约的存在,在没有发生时钟漂移(不同 server 上时钟运行的速度有较大差异)的情况下,leader 其实能够肯定自己就是 leader,从而无需发起一轮 server 间通信。但注意,这里的前提是不存在时钟漂移,虽然这个假设在大多数情况下是能够满足的,但严格来说,还是存在读取到过期数据的可能。
这个模式在 etcd 中叫做
ReadOnlyLeaseBased
。
总结 (Summary)
我们整个 Raft 流程看下来,可以看到 Raft 算法的基础部份,包括 leader election, log replication, safety,原理和实现都是十分简单的,但是随着成员变更的引入,一系列的细节问题随之而来,将算法的复杂度大大提升。
考虑到工程实现,即使 Raft 已经在论文中涵盖了几乎实现一个分布式算法的方方面面,想要保证算法的正确性仍然需要大量的测试和 debug。这也是为什么像 Raft 这样描述清晰的算法相较于 Paxos 在今天更受欢迎的原因了,毕竟如果一个算法的定义都不清晰易于理解的话,工程师怎么去实现呢。