Raft 算法
背景
在分布式系统中,一致性至关重要,其中以 Paxos 最为出名。但是 Paxos 难以理解,实现复杂。于是有了 Raft。
Raft 角色
一个集群通常包含若干个节点,Raft 把这些节点分为三种角色:Leader,Follower,Candidate,各个角色分工不同,正常情况下,只会存在Leader,Follower。
Leader
负责客户端的请求和日志的同步,对Follower 发送心跳。Leader 是通过随即计时器和投票选举出来的。
Follower
此角色只负责响应来自 Leader 或 Candidate 的请求,并且不会主动发起任何请求。Follower 有一个随机的选举超时时间,如果在这个时间内没有收到 Leader 的心跳包或者 Candidate 的投票请求, 它就会变成 Candidate, 并开始发起新一轮的选举。Follower 会接受 Leader的日志并同步到自己的状态机中并回复Leader。在同步的时候,Follower 会检查是否存在日志丢失,如果存在丢失,则会开始回溯。删除不一致的日志,并从 Leader 重新同步。
Candidate
负责选举投票,集群刚启动或者 Leader 宕机的时候,状态为 Follower 的节点将转为 Candidate 并发起选举,如果超过半数统一,转变为 Leader 角色。
问题分割
Raft 是基于操作转移的算法,它将一致性分解为多个子问题:Leader选举(Leader election),日志复制(Log replication),安全性(Safety)。
Leader 选举
关于选举,其实无非就是多数服从少数的问题,当获得的投票数量超过一半时,自己就可以升为Leader。无非需要注意几个问题。
何时选举
- [ ] 如何避免同时成为 Candiate,而全部投票给自己?
通过心跳机制,可以知道 Leader 宕机或者不存在。每一个 Follower 在初始化的时候便会启动一个随机的选举超时时间。随机是为了不让所有Follower 同时成为 Candidate,而全部投票给自己。
选举中的状态
- 选举成功:一个Candidate 赢得了大多数选票,成为新的Leader。
- 选举失败:未赢得大多数选票,选举时间超时,进入下一个任期。
- 取消选举:其他Candidate 赢得了大多数选票,收到心跳后转为Follower。
如何保证选举出的Leader拥有最新的日志
当一个节点成为候选人时,它会向其他节点发送RequestVote RPC请求,请求其他节点投票支持它成为新的leader。这个请求中包含了当前的任期号、候选人的id以及候选人的日志信息等内容。其他节点在收到这个请求后,会根据候选人的信息来判断是否支持候选人成为新的leader。
在判断过程中,每个节点都会比较候选人的日志和自己的日志。如果候选人的日志比自己的日志要新,那么就会支持候选人成为新的leader,否则就会拒绝候选人的请求。这就能说明Candidate 比大多数的Follower的日志都要新。
- [ ] 通过何种方式进行比较?
Raft算法采用了"最后日志条目的任期号"作为比较的依据。具体而言,每个节点在投票时会比较自己的"最后日志条目的任期号"和候选者的"最后日志条目的任期号",如果候选者的任期号比自己的任期号要大,那么就认为候选者的日志比自己的日志要新,否则就认为自己的日志比候选者的日志要新。
- [ ] 即使能比较最新,但也仅仅是和大多数节点进行了比较,如果保证是所有节点最新的呢?
如果一个 Leader 在和大多数的 Follower 进行比较的时候,发现自己的日志比它们都要新,这并不能保证所有的 Follower 都已经提交了比 Leader 更加新的日志,因为有一些 Follower 可能还没有和 Leader 进行比较。
不过,这种情况不会影响 Raft 算法的正确性,因为 Raft 算法中,只要某个日志条目被大多数的节点复制,那么这个日志条目就会被认为是已经提交了,且一定会被后续的 Leader 复制。因此,如果 Leader 确信自己的日志是最新的,并且这些日志已经被大多数的节点复制了,那么这些日志就会被认为是已经提交的,并且后续的 Leader 会一定会复制它们,以保证系统的一致性。
Leader的必要条件
具体而言,在Raft算法中,如果一个节点想要成为Leader,它需要满足以下两个条件:
- 它的日志必须包含了所有已经提交的日志。
- 它的日志必须比其他节点的日志新。
为了满足第一个条件,Raft算法中的日志复制机制可以保证Leader的日志包含了所有已经提交的日志。
日志复制
在出现各种情况,比如宕机,网络分区等,就有可能出现日志不一致的情况。新选举出的Leader 一定是日志最新的,也不一定是日志最全的。而由于各种分区,导致多个Leader出现,所以也有可能导致Follower 的日志不是最新的情况。这些问题正是日志复制解决的问题。
Leader 的日志领先
心跳的时候会检查日志。AppendEntries RPC 中会包含一个 prevLogIndex
和 prevLogTerm
字段,它们用于让 Follower 检查自己的日志是否和 Leader 的日志一致。具体来说,Leader 会在 prevLogIndex
和 prevLogTerm
指定的位置之前的日志条目都已经匹配的情况下,才会发送新的日志条目给 Follower。
这就保证了,如果 prevLogIndex
和 prevLogTerm
一致,那么 prevLogIndex
之前的所有日志都能保持绝对的一致。所以只需要检查 prevLogIndex
和 prevLogTerm
,如果出现不匹配的情况。这时,Leader 就会回退 prevLogIndex
,继续发送更早的日志条目,直到 Follower 接受了 Leader 发送的日志条目为止。然后覆盖Followers在该位置之后的条目。
Leader 的日志落后
Leader 在对比的过程中,当 Leader 发送 AppendEntries RPC
时,它会包含 prevLogIndex
和 prevLogTerm
两个参数,这两个参数用于告诉 Follower 在哪里匹配 Leader 的日志。如果 Follower 发现 prevLogIndex
和 prevLogTerm
与自己的日志不匹配,它会返回 false
,并且将自己日志中匹配 prevLogIndex
的最后一条日志的 term
值返回给 Leader,从而帮助 Leader 找到自己缺失的日志。
通过这种方式,Leader 可以得知自己缺失的日志是哪些,然后重新发送这些缺失的日志,在这个请求中,Leader 会携带上一条匹配的日志条目的索引以及之后的所有未提交日志条目。通过这个请求,Follower 可以将缺失的日志条目重新发送给 Leader。如果 Follower 的日志比 Leader 更长,那么这些额外的日志条目也会在这个请求中被包含,Leader 可以通过这些日志条目将自己的日志完善。
安全性
总的来说,安全性贯穿于整个流程。不管是 Leader 选举的时候,还是日志复制的时候。
- [ ] 如果 Leader 在同步的时候故障,也就是这个写入请求并没有被大多数节点所接受。这个问题有什么解决方案吗?还是无可避免的造成了丢失更新。
如果这个故障一致没有办法解决,那这条日志缺失永久性缺失了。但是如果故障恢复了,继续同步日志,就很有可能造成日志错乱。举一个网上的普遍例子。
- (a) S1是Leader,并且部分地复制了index-2;
- (b) S1宕机,S5得到S3、S4、S5的投票当选为新的Leader(S2不会选择S5,因为S2的日志较S5新),并且在index-2写入到一个新的条目,此时是term=3(注:之所以是term=3,是因为在term-2的选举中,S3、S4、S5至少有一个参与投票,也就是至少有一个知道term-2,虽然他们没有term-2的日志);
- (c) S5宕机,S1恢复并被选举为Leader,并且开始继续复制日志(也就是将来自term-2的index-2复制给了S3),此时term-2,index-2已经复制给了多数的服务器,但是还没有提交;
- (d) S1再次宕机,并且S5恢复又被选举为Leader(通过S2、S3、S4投票,因为S2、S3、S4的term=4<5,且日志条目(为term=2,index=2)并没有S5的日志条目新,所以可以选举成功),然后覆盖Follower中的index-2为来自term-3的index-2;(注:此时出现了,term-2中的index-2已经复制到三台服务器,还是被覆盖掉);
- (e) 然而,如果S1在宕机之前已经将其当前任期(term-4)的条目都复制出去,然后该条目被提交(那么S5将不能赢得选举,因为S1、S2、S3的日志term=4比S5都新)。此时所有在前的条目都会被很好地提交。
如果上述情况(c)中,term=2,index=2的日志条目被复制到大多数后,如果此时当选的S1提交了该日志条目,则后续产生的term=3,index=2会覆盖它,此时就可能会在同一个index位置先后提交一个不同的日志,这就违反了状态机安全性,产生不一致。也就是说当一个新Leader当选时,由于所有成员的日志进度不同,很可能需要继续复制前面term的日志条目,就算复制到多数服务器并且提交,还是可能被覆盖,因为前面term对应的日志条目较旧,容易使的没有这些条目的其他服务器当选Leader,此时就会覆盖这些日志条目。
为了消除上述场景就规定Leader可以复制前面任期的日志,但是不会主动提交前面任期的日志。而是通过提交当前任期的日志,而间接地提交前面任期的日志。