浅谈分布式共识算法Raft
1
01
Raft算法
Raft 算法是分布式系统开发首选的共识算法。比如现在流行 Etcd、Consul。
1.Raft的状态:
Raft算法是从多副本状态机的角度提出的,用于管理多副本状态机的日志复制,确保日志的一致性,是强一致性分布式共识算法。Raft系统的角色分为领导者(Leader)、跟从者(Follower)和候选人(Candidate):
Leader:接受处理所有客户端的请求,并向Follower同步请求日志,当日志同步到大多数节点上后告诉Follower提交日志;
Follower:接受并持久化Leader同步的日志,在Leader告之日志可以提交之后,提交日志,但领导者leader心跳超时的时候,会推荐自己当候选人;
Candidate:Leader选取过程中的临时角色,Candidate会向其他节点发送请求RequestVote RPC的消息,通知其他节点投票,如果赢得大多数选票,普通领导者,失败,则会变回Follower。
2. Term:
Raft算法将时间划分为任意不同长度的任期,任期号是连续的数字。每一个任期的开始都是一次选举(election)。某种情况下选票会被平均瓜分(raft集群一般保持奇数个数量),导致没有领导者产生,那么将会开始下一次任期的选举。
3.RPC消息类型:
RequestVote RPC:候选人在选举期间发起;
AppendEntries RPC:领导人发起的一种心跳机制,复制日志也在该命令中完成;
InstallSnapshot RPC: 领导者使用该RPC来发送快照给太落后的追随者。
1
02
Leader选举
2.1 选举过程:
当Raft服务集群启动时,所有的节点初始化时都为Follower角色,然后设置任期(term为0),启动计时器发起选举(election),每个参与者都会有一个150ms 到 300ms之间的随机超时时间(Election Timeout),在此期间如果没有收到Leader的heartbeat(官方称之为:Append Entries,Append Entries是一种RPC协议),就会等待随机时间结束后,发起另一起选举。Raft 算法使用随机选举超时时间的方法来确保很少会发生选票瓜分的情况,就算发生也能很快的解决。
先结束上一次选择的Follwer发起选择,将当前的term加一转为Candidate,给自身投票,并且向集群中的其他节点发送RequestVote RPC,只要其他follower收到来自Candidates的数据,那么其会保持follower的状态。一旦选举成为Leader,其会向其他节点发送 AppendEntries RPC, 确认其leader的地位,阻止选举。
Candidates状态会持续,直到下面的3种情况发生:
赢得了多数的选票,成功选举为Leader;
收到了Leader的消息,表示有其它服务器已经抢先当选了Leader;
没有服务器赢得多数的选票,Leader选举失败,等待选举时间超时后发起下一次选举。
2.2 选举规则和限制:
一个任期内,Leader一直不变,直到出现宕机,或者网络延迟,其他节点发起新一轮的选择;
一个选举中,每个节点对一个任期编号只有一票投票权;
大多数原则,节点必须获取到大多数选票才能当选Leader;
在Raft协议中,日志条目只会从Leader向follower写入,Leader节点上的日志只会增加,而不会删除或者覆盖,因此,Leader节点中一定包含了所有已经提交的日志条目。
1
03
日志复制
3.1日志复制过程:
Leader当选后,就会接受来自客户端的请求。每一个客户端发送的请求都包含一个将要被节点状态机执行的command。Leader会讲这个command包装为一个entry放入其log中,并通过AppendEntries RPC 发送给其他节点,要求其添加此entry到log中。
当entry被大部分节点接受并复制后,这个entry的状态变为了committrd,Leader将这条日志应用到它的状态机并向客户端返回执行结果。
3.2 日志entry的组成:
如上图所示,在entry中包含了common命令、entry所在的term,以及每一个entry的有序编号index。
3.3 日志的一致性:
Raft的一致性保证如下两点属性:
如果在不同节点中log中的entry有相同的index 和term,那么它们所存储的命令是相同的;
如果在不同节点中log中的entry有相同的index 和term,那么此entry之前的日志条目都是完全相同的。
特殊情况:
上图节点f在term2当选leader,添加entry到log中,但是还没commit时就奔溃了,快速恢复后又当选了term3的leader,把entry加入到log中,但是没有commit又继续奔溃了,并且后面几个任期内都一直处于宕机状态。在一般情况下,Leader和Followers的日志保持一致,因此 AppendEntries 一致性检查通常不会失败。然而,Leader人的崩溃会导致日志不一致(旧的领导人可能没有完全复制完日志中的所有条目)。这些不一致会导致一系列Leader和Followers崩溃。此时的集群似乎打破了上面的两点属性。(下面安全性中解答疑问)
在raft中,为了处理这样的不一致性,强制要求follower的log与leader的log要一致。
1
04
安全性
Raft增加了如下两条限制以保证安全性:
拥有最新的已提交(commit)的log entry的Follower才有资格成为Leader。
Leader只能推进commit index来提交当前term的已经复制到大多数服务器上的日志,旧term日志的提交要等到提交当前term的日志来间接提交(log index 小于 commit index的日志被间接提交)。
根据上图分析,(a)中的 S1 是Leader并且部分节点复制了索引 2 上的日志条目;(b)中 S1 崩溃了,S5 通过 S3,S4 和自己的选票赢得了选举,并且在索引 2 上接收了另一条日志条目;(c)中 S5 崩溃了,S1 重启了,通过 S2,S3 和自己的选票,S1赢得了选举,并且继续索引 2 处的复制,这时任期 2 的日志条目已经在大部分节点上完成了复制,但是还并没有commit(假设没commit,其实已经commit了)。(d)时刻 S1 崩溃了,S5 会通过 S2,S3,S4 的选票成为Leader,然后用它自己在任期 3 的日志条目覆盖掉其他服务器的日志条目。然而,如果在崩溃之前,S1 在它的当前任期在大多数服务器上复制了一条日志条目,就像在(e)中那样,那么这条条目就会被提交(S5 在(d)就不会赢得选举)。
1
05
日志压缩
客户端的每个操作命令会被包装成日志交给 Raft 处理。然而,在实际系统中,日志不能无限增长,否则系统重启时需要花很长的时间进行回放,从而影响可用性。因此,Raft提供了了一种机制去清除日志里积累的陈旧信息,叫做日志压缩(snapshot)。
注意,在 Raft 中我们只能为 committed 日志做 snapshot,因为只有 committed 日志才是确保最终会应用到状态机的。
snapshot中包含以下内容:
日志元数据。最后一条已提交的 log entry的 log index和term。这两个值在snapshot之后的第一条log entry的AppendEntries RPC的完整性检查的时候会被用上。
系统当前状态。
当Leader要给某个日志落后很大的Follower同步时,改Follower的日志会被丢弃,然后Leader会将snapshot发给Follower。或者当新加进一台机器时,也会发送snapshot给它。发送snapshot使用InstalledSnapshot RPC。
1
06
集群成员变更
在实际中,集群中的节点数是可能发生变化的(增加或减少),那么就需要更改配置了。通过关闭整个集群,升级配置再启动,那么在此过程中会导致整个集群不可用。另外存在手工操作,那么就会有操作失误的风险。为了避免这些问题,决定采用自动改变配置并且把这部分加入Raft算法中。
将成员变更当作一般的一致性问题,直接向Leader发送成员变更请求,Leader向Followers传达成员变更日志,达成多数派之后commit,各Follower提交成员变更日志后从旧成员配置(Cold)切换到新成员配置(Cnew)。
因为各个节点提交成员变更日志的时刻可能不同,造成各个节点从旧成员配置(Cold)切换到新成员配置(Cnew)的时刻不同。这就有可能导致选出两个Leader(Cold和Cnew节点数不同,导致认为的多数节点这个条件不同),破坏安全性。
简单来说,就是成员变更的数量太多了,导致旧成员和新成员组没有交集,因此导致双Leader。解决这个问题的方法就是每次只增加或减少一个节点(如果要变更多个成员,连续变更多次)。