Raft是一种分布式一致性算法,相比于Paxos,它更容易理解和实现,效率也相当。Raft 将一致性的关键元素分成几部分,如leader选举、日志复制和安全性,实现了这几部分也就实现了一致性。
基础概念
-
角色: 一个Raft集群由若干个节点组成,每个节点有三种角色:leader、follower、candidate
- leader:表示主节点,由选举产生,在选举中获得超过集群节点数量一半选票的cadidate会成为leader。leader会定期向集群的其他节点发送心跳,以维持自己的leader地位。在处理客户端请求时,必须由leader进行处理(即使follower收到请求也要转发由leader处理),它会将日志数据同步给其他follower节点,并提交给状态机执行。
- follower:普通的从节点,follower不会主动向leader发请求,只会被动地接收来自leader的请求。在选举时会响应candidate的投票请求。
- candidate:只有在选举阶段存在,当follower一段时间收不到来自leader的心跳,就会发起选举,成为candidate。candidate会投票给自己,并请求其他节点投票给自己。
-
任期
任期是raft中的一个重要概念,它是时间轴上不重叠的一段段时间,每次发生选举时,就会进入新的任期。每个任期都有一个任期号,它是单调递增的。在选举、日志复制、安全性等问题的解决中会非常依赖这个任期号。
选举
- 正常情况下,一个集群中有一个leader,其他节点都是follower,leader定期向follower发送心跳,节点的角色也一直能维持,任期一直保持。
- 但特殊情况下,一个follower超时没有收到leader心跳,它会认为leader挂了,就会增加自己的任期号,然后向其他节点发起选举,表示竞选下一个任期的leader。此时它会遇到几种情况:
2.1 如果它获得超过集群节点数量一半选票,它就会成为新任期的leader,并开始向其他节点发送心跳(为什么要超过一半,是为了保证在任何一个任期里都不可能产生超过一个leader。此外结合日志的提交,还能保证安全性,这个后面再说)
2.2 如果选举超时它还没有获得超过一半的选票,它会结束这个任期的选举,成为下一个任期的candidate
2.3 如果它收到其他candidate的选举请求:其他candidate选举的任期号比它所竞选的任期号大,它就会放弃选举,回退到follower角色;其他candidate选举的任期号小于等于它所竞选的任期号,则拒绝请求
2.4 如果它收到leader的心跳:如果这个leader的任期号大于等于它所竞选的任期号,它就会放弃选举,回退到follower角色;如果这个leader的任期号小于它所竞选的任期号,则拒绝请求 - leader只有在收到比自己任期号更大的leader发来的心跳时,才退位让贤,回退到follower角色
以上2.1是期望的情况,这样可以快速选出leader。2.4发生在其他节点赢得选举的情况。而2.2和2.3发生在超过一个节点成为candidate从而导致选票被瓜分的情况,这种情况我们希望它尽量不发生,因此有一个简单的解决方法,就是每个节点的超时时间不是常数而是一定范围的随机数。这样,follower在接收心跳时的超时有先后,就会有的节点率先成为candidate;即使发生了选票瓜分,也有candidate节点会率先选举超时从而进入新任期选举,把其他candidate挤掉,这样就可以尽量避免长时间选不出leader的情况。
角色的转变关系如下图所示
日志复制
Raft算法是为了保证节点内部状态的最终一致性,每个节点内部都有一个状态机,而外部客户端的请求可以被看作是一系列的日志,每个节点的状态机只要执行相同的日志序列,最终就能达到一致的状态。所以状态一致性问题就转化为了日志的一致性问题。Raft算法是依赖leader来实现日志一致性同步的。
可以看到日志包含的数据类似下图:
日志包含一个单调递增的序列号(log index)以及当前任期的任期号(term index)
每一条日志都是由某个任期的leader产生的,然后同步给follower。如果leader中有日志与follower不一致,leader会覆盖follower的日志,确保一致性。
因此它们必然有以下性质:
- 如果两个节点中的两条日志,序列号和任期号一致,那么它们必然属于同一条日志,内容必然一致
- 如果两个节点中的两条日志,序列号和任期号一致,那么它们之前的日志的顺序和内容也必然一致
性质1没有问题,性质2是因为leader在同步日志给follower的时候,必须找到相同的一条日志,然后再按顺序把之后的日志一一复制过去,由此可以保证日志都是完全一致的
例子:
正常情况下,leader不断产生日志,不断同步给follower,不会出现不一致的情况。
但是特殊情况下,比如网络问题,集群会反复进行选举,短期内产生多个任期,如下图所示
此时的leader获得了任期8,需要再log index=11的地方写入一条数据,然后将它同步给followers。对于任意一个follower,leader会从log index=11的地方开始向前尝试,直到找到一条与自己log index和term index都一致的日志,然后强制将follower这个位置之后的日志都删除,把自己这个位置之后的日志复制过来,从而实现覆盖。
安全性
以上的逻辑保证了日志的最终一致性,但似乎不能保证状态机执行的一致性。比如说:一个follower在一段不可用状态期间,leader提交了若干的日志条目,然后这个follower被选举为leader并且用新的日志条目覆盖这些日志条目;结果,不同的状态机可能会执行不同的指令序列。
所以问题的关键就在于日志在什么时候被提交给状态机执行,且被执行过的日志是不是一定能确保在leader的日志序列中。这两个问题的答案是
- 当leader成功将日志同步给超过集群节点数量一半的节点(包括自身)后,即可将该日志提交状态机执行
- 通过在选举中增加限定条件:如果一个节点A收到节点B的投票请求,而A中的最新日志比B的最新日志还新,那A就拒绝投票给B。从而确保leader的日志序列中必须包含所有已经被提交的日志,这样即使将该leader的日志覆盖其他节点,也不会造成某条日志已经被提交执行了,却又被其他leader的日志覆盖了的情况
此处“新”的定义:如果任期号不一致,则任期号大的新;如果任期号一致,则日志序列号大的新。
下面用一个例子来解释,为什么这样就能保证安全性:
- 一开始A节点为leader,其他节点都是follower,任期号为1。客户端向A发请求写入数据index=3时,A节点同步的时候与E、D的网络中断,只同步给了B、C,此时同步数为3超过了集群半数,因此index=3的数据可以被提交状态机执行了。
2.此时A节点发生了重启,假设E节点首先超时,变成candidate来竞选任期号2的任期。E、D会投票给E,但是A、B、C因为他们的最新日志比E新,所以不会投票给E,所以E一定无法当选leader。
由此可见,一条日志如果被提交,那么它必然已经被同步给了超过一半的节点,而那些没有被同步该日志的节点,永远无法获得超过一半的选票,也就无法成为leader。换句话说,能成为leader的节点必然包含了所有被提交了的日志记录。
所以成为leader的节点用自己的日志序列去覆盖其他节点永远是安全的。
综上,Raft算法巧妙地使用选举、日志复制和安全性保证,实现了分布式系统的最终一致性。
参考:
- 分布式一致性算法:Raft 算法(论文翻译)地址:https://www.cnblogs.com/linbingdong/p/6442673.html
- Raft 算法 地址:https://raft.github.io/raft.pdf