paper: In Search of an Understandable Consensus Algorithm (Extended Version)
为了保证服务的稳定性(解决单点故障问题),人们提出了副本技术(replication)。但是副本之间需要一个中心服务器进行协调(比如GFS和MapReduce的master server),那么单点故障只是转移到了中心服务器,并没有得到彻底解决。
于是人们又提出了使用多个中心服务器用来容错,本质上又是副本技术,那么如何保证一致性呢?这里没有使用更高一层的中心服务器,而是提出了RSM (replicated state machine),也就是把单个服务器当做一个状态机,所有的输入会引起状态的转移,同时需要保证服务器的所有操作都是确定的(比如获取当前时间、产生随机数等就是不确定行为,产生的结果因机器等因素而异),否则不同服务器即使都接受相同的输入,得到的状态也会不一致。
一般来说,人们通过维护一个日志,记录所有的输入,对于不确定指令,只在一台服务器上执行,然后将其结果当做输入记录到日志中,其他服务器按顺序执行日志中的命令,就可以保证所有的服务器保持相同的状态。
那么核心问题就是如何保证所有机器都维护相同的日志,这就是共识算法需要做的事情了。
Paxos提出的比较早,在长达十多年的时间里一直是主流。但是非常难以理解,而且实现方面可能存在问题。
因此Diego Ongaro 和 John Ousterhout提出了Raft。
在Raft中,节点有三种状态,Leader,Follower,Candidate,三种状态之间的转换关系如下如所示。
在正常情况下,只有一个Leader,其他所有节点都是Follower,Candidate状态是为了选举新的Leader的临时状态。Follower只回应Leader和Candidate的消息,Leader处理所有来自client的消息。
在Raft中,时间被分为了term
,有连续的整数标识,每个term
开始时都进行选举,Raft保证每个term
最多只有一个Leader。
election timeout
:选举时间,在Raft中,Leader周期性的向Follower发送心跳包,如果Follower在选举时间内没有收到来自Leader或者Candidate的心跳包,那么自己就变成Candidate去竞选Leader,同时term
加1。
Candidate状态会一直持续,直到以下三种情况发生:
term
不小于自己的term
,那么该Candidate变成Follower,否则忽略该心跳包。term
重新开始新一轮的选举。为了避免反复进入第三种情况,Raft使用了随机的election timeout
(150-300ms),因此各个节点进入Follower的时间不完全相同(发起投票的时间不同),大大降低了进入第三种情况的可能。
当Leader收到client的请求后,先生成一个日志条目(log entry),该条目都包含一个当前term
和一个index
(日志中的位置),然后将该条目发给Follower。当日志条目复制到大多数节点后,日志被Leader标记为committed
,此时返回结果给client。
在日志中,Raft保证两个属性:
term
和index
的日志条目包含相同的请求命令。term
和index
的日志条目之前的日志条目完全相同。由于日志条目只由Leader生成,Leader保证每个term
中对于一个index
只生成一个日志条目,因此属性一显然满足。
对于属性二,当Leader向Follower发送日志条目时,会附带前一个日志条目的term
和index
,如果Follower的日志条目与之不一致,就拒绝添加新的日志条目,并向Leader索要前一个日志条目,直到满足term
和index
,然后将之后的日志从Leader复制到Follower中。
以上还不足以保证所有的节点执行相同的操作,比如当一个Follower掉线重新上线后被选为了Leader,由于该Leader丢失了很多日志,后续可能会把前一个Leader标记为Committed
的日志条目覆盖掉,导致不同的节点执行了不同的操作。
Raft对Leader选举添加了一个限制来解决这一问题:保证任何term
的Leader包含前一term中被标记为Committed
的日志条目。
在投票过程中,Candidate需要附带上自己的日志中最后一个条目的term
和index
,如果投票者的日志比Candidate的日志新,那么就拒绝投票。因此如果某Candidate得到了大多数节点的投票,就可以保证该Candidate的日志包含了前一个term
中被标记为Committed
的所有日志条目。
这里需要说明一下,由于只有被大多数节点备份的日志条目才会被标记为
Committed
,而Candidate又需要得到大多数节点的投票,这两个大多数节点中至少有一个节点是重复的,因此可以保证满足该限制:任何term
的Leader包含前一term中被标记为Committed
的日志条目
Raft如何处理前一个term
中的日志条目? 如下图所示,S1到S5是五台服务器,每个方格中的数字是term
,每个方格代表clients发起的一个请求命令。
在a中,S1是Leader,正在将日志复制到Follower,此时S1出现了故障;到了b,S5被选为了Leader,并接受了clients发起的一个命令,还没开始复制就挂掉了;到了c,S1被重新选为Leader,继续复制未完成的term
2中的日志,此时该日志已经被大多数节点接受,不应该再被撤销了,然而,在S1准备commit该日志的时候,S1挂了;到了d,S5被选为新的Leader,复制日志过程中将term2的日志覆盖了;在e中,如果S1没有去管term
2,直接去复制并commit term
4的日志,那么当term
4被commit之后,term
2的日志也会被间接commit,S5就不会被选为Leader了,不会导致覆盖发生。
有点难以理解。。。我的理解是:在c中,
term
2的命令已经被大多数节点记录到日志中,但是此时Leader是否执行、是否返回结果给client是未知的,因此我们只能假设已经执行并且返回了结果(否则会导致不一致),所以term
2的日志绝对不能再被覆盖。
Raft对于这种问题的解决方案是:Leader不去直接commit前一个term
的日志,而是去commit当前term
的日志,然后通过前面提到的日志副本的属性二保证前面term
的日志会被间接commit。
TODO
TODO
TODO
深入阅读: