目的
根据《In Search of an Understandable Consensus Algorithm》论文的描述来看,Raft和Paxos这些共识算法是为了解决分布式系统里状态机同步的问题。最典型的应用就是分布式里的容错,所有的server都应该要有备份,如果是带状态的server(典型的如kv server),那么master和secondary之间的数据如何保持一致不会出错就是一个难题,而这些共识算法就是为了解决这类问题的。
前言
一般来说状态机同步有以下两种方案。
1.State Transfer。将完整的状态同步。
2.Replicated State Machine。假设没有外因的情况下,对一样的状态机输入一样的数据,那么他输出的数据也是一样的,即只要保持输入一致那么状态机的最终态也是一致的。而Raft采取的是这个方案。
概念
Raft中的所有节点在某个时刻必定处于以下三种状态中的一种:
Leader:接受客户端的请求,将数据同步给其他Server。
Follower:接受Leader或者Candidate的请求进行处理。
Candidate:选举中的可能成为Leader的一个状态。
Term: 任期。这是一个随时间单调递增的变量,每个Term都是由选举开始的,Raft保证一个Term最多选出一个Leader。任何一个Server在观察到自己的Term过期后会更新Term并且转换为Follower状态。
这个算法可以保证以下结论:
1.在给定的Term中最多只会选出一个Leader。
2.对于Leader来说,它永远只会做添加日志操作,不会做删除或者覆盖日志操作。
3.如果两条Log拥有相同的Index和Term那么认为这条Log以及这条Log之前的所有Log都是相同的。
4.如果一条Log已经提交,那么这条Log会在出现在所有Term大于该Term的leader的Log中。
5.如果一个Server将某索引为Index的一条记录提交到状态机,那么其他Server不能将同一个Index的不同记录提交到状态机。
实现
Raft主要通过两个RPC方法来实现。
RequestVote:Candidate在选举过程中使用的,用于请求投票。
AppendEntries:Leader进行Log复制和Heartbeat时候用的。
Raft的一致性问题可以拆分成三个子问题:Leader选举,日志复制,安全性。
Leader选举
Leader选举是通过Heartbeat机制触发的。具体体现在AppendEntries RPC方法。启动的时候所有Server都是Follower状态,当Follower的计时器超时都没收到Leader的Heartbeat时,它就会假设集群里没有Leader,转换成Candidate状态发起选举。
选举开始时,Follower会首先自增Term,然后切换到Candidate状态。同时它会给自己投票,并向其他服务器发起RequestVote RPC请求以获得投票支持。它会保持Candidate状态直到发生以下三种情况之一:
1.胜出选举。此时,它会切换到Leader状态,并立即调用AppendEntries RPC进行Hearbeat操作,阻止发起下一轮选举。
2.某个Server宣告自己已成为Leader。此时需区分两种情况:如果该Server的Term>=自己的Term,则认为其声明合法的,Candidate切换到Follower状态;若不满足此条件,则认为其声明不合法,继续保持Candidate状态。
3.选举超时,没有产生胜者。在这种情况下,将重新发起一轮选举。
在此过程中,我们容易注意到一个问题:如果没有经过特殊处理,所有Server同时发起选举的可能性很高。这将导致每个Server都给自己投票,而无法产生胜出的Leader。为解决这一问题,Raft算法引入了随机性操作,对选举超时时间进行一定范围的随机化处理。这样便能在很大程度上保证选举过程中,有Server能够胜出成为Leader。
日志复制
前文提到,Raft算法采用了Replicated Machine的方式,通过复制Log来确保输入一致性。同步操作由Leader发起的,每当Leader处理Client的请求(即状态机的一条命令Command)时,Leader会将Command追加到自己的Log中,然后发起AppendEntries RPC请求,直到Log被安全复制。Log应包含Term和Command信息,其中Term用于进行一致性检查。一旦Leader提交了一条Log,我们称为这条Log是Committed状态。当这条Log被复制到大多数Server时,那么称这条Log是提交的。Raft算法会确保已提交的日志应用到其他状态机上。
Leader维护了一个可提交日志的索引Index,并在AppendEntries RPC中携带此信息,告知Follower应提交此索引之前的所有记录。Leader在发送AppendEntries的时候会包含当前复制记录的前驱记录的Index和Term,如果Follower发现没有相同的前驱记录,它会拒绝该请求。如果Follower拒绝请求,证明Leader和Follower的Log不一致,此时,Leader会强制要求Follower同步成Leader的数据,Leader通过不断执行Index-1操作发起请求直到找到最后一条一致的记录。然后,Leader会将该记录之后的所有记录通过AppendEntries RPC同步给Follower。
需要注意的是,这里的每次Index-1操作可能存在效率问题。在实际工程实现中,可以进行优化。然而,作者本人对优化的必要性表示怀疑,因为这种事件发生的概率极低。
安全性
Leader宕机:
如上图,存在这么一种情况,在图a时候,Leader是S1,此时刚开始复制Log2,但在还未复制到大多数节点之前就宕机了。在图b时,S5选举成为新的Leader,将Log3添加到本地后宕机了。到图c时,S1重新成为Leader,将Log2复制到了大多数节点并提交。此时,如果S1再次宕机,且c成为了Leader,会出现图d的情况,把Log3覆盖原来Log2的位置,但是S1已经把Log2提交到了自己的状态机,此时便会发生状态机不一致的问题。为了避免这一问题,Raft算法增加了选举约束:如果Follower的日志比Candidate的日志更新,那么Follower应拒绝这次投票。Log比较的优先级顺序是先比较Term,再比较Index。此外,新Leader在重新选举出来后,不能直接提交非本任期以内的日志,因为无法确定这些日志是否复制到了大多数节点,需要等到下一个本任期内的新日志提交来保护前面的日志。
Follower和Candidate宕机:Leader持续重试RPC操作,直至它们恢复,Raft的RPC操作是可重入无危害的。
快照
为解决数据严重滞后的Server的问题,采用大量的RPC请求相较于直接发送整个状态机的状态来说并不高效。因此,通常每个Server可以在固定的n条日志提交后进行一次快照操作。具体参数的选择取决于机器性能以及应用场景。
集群成员变更
集群成员变更的难点在于变更过程中不能出现两个Leader,作者首先提出来Joint Consensus的实现,后来又提出更简单的Single-server Changes的实现,即通过单节点变更来解决这个问题。由于Joint Consensus过于复杂,且在工程中大多数都使用Single-server Changes,因此这里仅讨论Single-server Changes的原理。
上图分别展示了4种情况。
a:偶数节点增加一个节点。
b:奇数节点增加一个节点。
c:奇数节点减少一个节点。
d:偶数节点减少一个节点。
可以观察到,在单节点变更下,无论是哪一种情况,新旧配置中的大多数节点总会存在一个交集节点。由于Follower只能给一个Server投票,因此不会有多个节点满足大多数节点的投票要求,最多只会产生一个Leader。
预投票
Prevote预投票是作者在另一篇论文《CONSENSUS: BRIDGING THEORY AND PRACTICE》中提及的概念,主要用于解决以下问题。
假设存在一种情况,我们有5个Server分别为S1至S5,其中S1是Leader。连线表示两个Server之间网络是联通的。S5无法收到Leader S1的心跳,此时会发起一次新的选举,获得了S2,S3,S4的投票,成为了新的Leader。S1意识到了有新的Term产生,会转换为Follower,但又收不到S5的心跳,因此会陷入反复发起选举的现象。
还有一种情况,假设图上S5是个孤立的节点,它会反复发起新的选举导致Term不断增加变得非常大。当它重新与其他节点联通时,集群内的所有节点都会受到这个旧节点的干扰。
预投票机制就是为了解决这些问题而设计的。在预投票算法中,Candidate首先要确认自己能赢得集群中大多数节点的投票,这样才会把自己的Term增加,然后发起真正的投票。其他投票节点同意发起选举的条件如下:
1.没有收到有效领导的心跳,至少有一次选举超时。
2.Candidate的日志足够新(Term更大,或者Term相同Log Index更大)。