这篇文章主要介绍分布式协调一致性算法Raft。 Raft算法是由斯坦福大学Diego Ongaro 和John Ousterhout提出的,尽管它跟Paxos功能和性能上差别不大,但它比Paxos更易懂。这也是作者设计这个算法的目的。
一致性算法一般要具有以下特点:
1. 要确保数据在网络延时、丢包、重复等多种情况下,数据是正确的。
2. 保证系统是可用的,即使有些节点已经宕机。
3. 不依赖与时间去确保日志的一致性。 这里是指
4. 一个命令的执行只需要在多数系统中执行完,就算真正完成。小部分节点未完成不影响其执行。
Diego认为Paxos主要的不足在于让人难以理解,并且其架构不适合与构建真实系统。所以Raft的设计原则就是让人更可理解,在后续的很多选择中都可以体会到这点。类似与Paxos, 也是采用中心化的方式,所有的写操作都只能通过Leader完成,其他节点只能跟随。
1.Raft把Server分为三种状态:
1.) Leader
只能有唯一的头头。
2.) Follower
追随者,接受Leader发过来的日志。
3.) Candidate
候选者,Follower在超时时间内还没接收到Leader的心跳,则自动变为候选者。
上图是它的状态转换图, 一开始所有server状态都为Follower,当在超时时间未收到Leader的心跳或命令,则变为Candidate; Candidate通过
选举得到了多数的票数后,变为Leader。Leader会一直做下去,直到其宕机重启或发现有比他更新的Term(后面讲)
2. Raft的时间段
Raft把时间分为一个一个的Term, 在一个Term里最多只能有一个Leader。用Term来表示可以使得集群不依赖与时间,只需要用Term就能判断当前Leader的作用期限。
当一个Leader宕机后,其他follower在timeout时间内收不到心跳,则自动把Term自增1并变为Candidate发起投票。
3. Raft的通讯
Raft Server采用RPC(remote procedure call)进行通讯,只有2种RPC以及其回应
1.) Request Vote RPCs
这个RPC是Candidate在发起投票请求时向其他server发送的, 其他server会根据日志以及Term信息给RPC返回一个响应,具体看Leader选举。
2.) AppendEntries RPCs
这个RPC是Leader向Follower发送心跳或者复制日志时用的, Follower也需要给Leader回应。
4. Leader选举
集群内的Raft Server一启动是以Follower的状态存在,每个Follower都会设置一个随机的选举超时时间,因为此时无Leader存在,所以必定有第一个Follower因为超时而变为Candidate状态而发起选举并投自己一票。 其他server收到Request Vote请求后,根据当前状态进行判断是否要投赞成票:
1.) 如果Candidate的Term比其本身的Term少,则投反对票。 如果发现Candidate的Term更大,并设置本身的Term为接受到的新Term,然后看条件2
2.) 如果未投票并且Candidate的日志比其要更新,则投赞成票。
当一个Candidate收到超过半数的投票,则认为其当选成为Leader。每轮投票至多只有一个Candidate当选,因为每人只能投一票而且要超过半数才能当选。但也有可能一轮投票无人当选。此时只能等待下一次选举超时重新起一轮投票。如果同时很多Follower因选举超时而变为Candidate,会导致当前Term无法产生Leader,又要等下一轮。这种情况称为Spilt Vote。
为了避免Spilt Vote,Raft对每个Server采用随机的选举超时时间,一般是150~300ms之间。这样可以减少同一时间内出现很多Candidate的情况。 一般最新超时的Follower能很快就当选,然后发送心跳给其他server,防止他们超时。
5.日志复制
只有Leader才能通过AppendEntries RPC向其他Server发送日志,要求其根据日志执行命令,此过程称为日志复制。我们先来看看日志长什么样。
如上图,一个框框就是一个日志项,里面有Term信息和命令。同时日志项上还有一个index,标记其在日志的位置。 当一个日志在多数server都保存后,就可以认为日志是提交了,当然后续还有一些针对提交日志的限制,这个后面讲。
日志有如下性质:
1.) 如果两个日志的Term和Index相等,则其命令也是相等。
2.) 如果两个日志的Term和Index相等, 则其之前的日志也是相等的。
第一个性质因为每个Term只有一个Leader, 并且Leader的日志从来不会被覆盖,只会不断增加,所以显然是满足的。 第二个性质是因为Follower在接受到AppendEntries RPC时, 会检查RPC中新日志项的前一个日志项是否包含在本身中,否则会拒绝增加新日志项。
当Follower因为检测前一个项不存在而拒绝RPC时, Leader收到回应后会尝试发更前一个日志项,一直到Follower接受为止。这种情况特别适合于Leader重启,因为Leader重启后不知道Follower到底保存了哪些日志,它会先尝试从最后的日志开始复制,直到找到Follower真正能接受的日志项,然后才恢复正常的日志复制。
6. 安全性
为了保证所有的Server能够在任何条件下都能执行相同的命令,还需要加一些约束条件。
1.) 选举约束
选举必须保证本次选出来的Leader必须是含有上一个Term中已经commit的日志。Raft为了满足这个条件,规定日志只可以从Leader发往Follower,Leader日志只能增加不能被覆盖。同时在投票时,候选者必须得到多数的票数,而每一个已提交的日志必定包含在这些多数的server中,那么得到多数的投票,必定这个候选者是在多数server中日志是最新的,所有其必定含有所有已经提交的日志。
注意当server少于原来的一半时,此时不能选出Leader,即集群不能工作,所以不会存在已commit的日志在现有server中不存在的情况下还能选出Leader的情况。
2.) 不能提交前非当前Term的日志
Leader标记日志为提交时,只能标记那些已经复制到多数Follower,并且Term是当前Term的日志,不能标记以前Term的日志为 commit。这样的约束是为了避免以下这种情况的:
如上图描述, (a)图S1已经复制index 2到S2后, S1 宕机。 (b) S5 在这种情况下,可能比S2先超时,导致先变为Candidate,它可以得到S3,S4的票数和自己的票,所以能成为Leader, 并开始一个新的Term(注意2并不是新term,否则S5不可能当选) (c) S5刚当选就宕机, 此时S1重启,并当选为Leader。 开了一个新Term,但继续复制Log 2 变成多数,但未提交。 (d) 此时S1又挂了, S5有可能再当选(注意S5重启后保持原来的新Term,
此时跟其他server的Term大小是一样的,但经过一次选举超时后其term又变大,所以能当选), 这样就会继续复制之前的3号日志,从而把其他server的2号日志覆盖。 (e)只有像S1把当前term 4复制到多数,此时才算提交,然后以前的日志就被默认提交。
7. Leader 完备性
定义: 如果一个日志在之前的Term被提交,则其日志会被包含在后续的更高Term的Leader日志中。
要证明完备性,关键点在于多数server中投赞成票的voter中。 假设term T已经提交了一个日志, term U是最小的其LeaderU不包括commit日志, U > T。那么在选举LeaderU的时候,必定有个多数派中的voter含有term T提交的日志,但又投票给LeaderU。 但是投票的原则是LeaderU要比voter的日志更新,即要包含term T提交的日志。这样就造成矛盾,得证。