在分布式系统中,有很多复杂的理论,从CAP理论到BASE理论,我们不断的在可用性以及一致性之间做出抉择,每一部分都相当复杂,就分布式一致性而言,又有许多协议,从2PC到3PC再到paxos算法,到ZAB协议,再到Raft算法。本篇文章主要介绍Raft算法的实现过程,最近有空看了下国外的论文,这里做个简单的总结,本人水平有限,如有问题,欢迎批评指正。
在介绍Raft算法之前,你肯定听说过Paxos算法,它是Leslie Lamport
大师提出的,解决了很大一部分分布式领域一致性问题,但是由于其算法理解复杂,细节描述不够详细,现有的所有解决方案都是基于该算法自己实现具体的细节。这也被Raft算法的作者一直所诟病,所以它一直希望能有一种算法能够更加便于理解,所以在2014年,由斯坦福大学提出的一种全新的一致性协议。在原算法论文中,作者还有意的吐糟了Paxos算法:
leader
,将所有server分为两种主要的角色Proposer
以及Accepter
,每一个Proposer
可以提出一个propose,然后进行投票选举,提交propose,造成整个过程比较复杂。Paxos
算法实现方案比如Google的Chubby
也是自己实现了一些具体的过程,而且也没有公开。基于上面的的一系列问题,作者本着简单实用的原则,实现了Raft,它相比Paxos
算法有以下几大优点与不同:
timeout
,每一个server有一个id,然后 通过投票选举。joint consensus
的方式支持在线上更改配置,并且在这个阶段保证整个集群的可用性。lastest commitId
进行恢复。基于以上对于Raft算法的介绍,我们开始介绍它的每一个实现细节。首先,我们了解一下什么是复制状态机,如下图所示:
提到分布式一致性,我们不得不提到复制状态机,它结合一致性协议解决了分布式系统中的高可用以及容错性,比如Zookeeper,HDFS等。所谓复制状态机,就是说每一台服务器上维持着一份持久化Log,然后 通过一致性协议算法,保证每一个实例中的Log保持一致,并且顺序存放,这样客户端就可以在每一个实例中读取到相同的数据。如上图所示,有一个Consensus Module
就是一致性协议模块,它可以是Paxos算法的实现或者Raft算法,下面,我们重点介绍Raft算法的实现。
在这之前,需要理解Raft算法中的几个状态以及它的RPC调用,它主要有两个RPC通信,分别是RequestVote RPC
以及AppendEntries RPC
,分别负责选举以及数据的交互。
这里需要着重理解Term
,对于每一个leader,都会有一个Term
,它会将这个term带到log日志中的entry中,代表当前entry是在哪一个term时期写入。具体过程下文会详细解释。
如上图,当前节点的term为4,state为leader,commitIndex为0,总共有五个实例。
选主阶段
Raft算法的选举算法比较简单理解,对于每一个实例都有一个server id
以及timeout
,每一个实例会随机sleep一个timeout,然后开始投票,都去选举那个最大的serverid
,如果有超过半数以上的实例同意,那么将其选举为leader。之所以随机sleep一个time,是因为这样可以加快选举的流程,在一般情况下,只要网络状态良好,先timeout的实例先进行选举,这样只需要一轮选举就选举出leader,避免了由于相互选举而再次进入选举的情况。
主节点和其他子节点之间通过心跳机制保持联系,如果在一段时间内收不到心跳报告,则将该节点判为故障。
如果从节点故障,那么待故障恢复之后,就会去主节点同步日志进行故障恢复。
如果主节点故障,那么会重新进入选举阶段,每一个实例会将当前的term加1然后进行投票,如果接收到的投票小于自己的term,那么投反对票,即选举自己。最后统计大于半数的节点选举为leader,如果选举失败,则会将当前term再加1进行选举,直到选举成功。
数据的同步
在选举完成之后,客户端与leader交互写入数据,这时leader会在自己的log的第一个空位index中写入数,接着,通过AppendEntries RPC
把当前index的entry以及lastest commiteid
发送给每一个follower节点,如果follower接收到的lastest commiteid
等于当前实例的,代表<=该ID的所有entry已经被commit
,此时follower返回true,否则返回false。如果超过半数的follower向leader返回true,那么代表当前entry写入成功。对于返回false的实例,可能是因为网络原因或者故障恢复的原因,数据没有正确同步,此时leader会从最后一个entry开始向前遍历,知道找到故障实例对应的entry,然后开始恢复数据。以下图为例:
leader的当前index=8,term为3,x=4,开始同步数据,会收到第一个(包括自己),第三个和第五个实例的同意,大于半数的实例返回true,当前entry commit,第二个和第四个实例将会返回false,此时leader会从最后一个entry开始从后向前遍历,将当前index发送给返回false的实例,直到它返回true开始同步数据。比如刚开始leader发送index=8,term=3,x=4
的数据给第二个实例,它将会返回false,接着发送前一个entry,也返回false,直到发送index=6,term=3,y=7
的数据时返回true,开始正确同步数据,第四个实例也同理。
那么,有人会提出,如果在数据同步的时候节点发生宕机该怎么办?它会出现哪些情况呢?
对于a~f的实例,可能是因为某种故障而展现出的不同情况,它们都有可能成为当前leader的follower节点,以其中的(f)实例为例解释一下故障发生的原因。(f)实例之前可能是term=2是的leader节点,在写入数据的时候,它只写入了自己的log就发生宕机,在一个短暂的时间之后,它故障恢复,并且有再次被选举为leader,并且当前term=3,此时它写入数据,但是又在写入自己的log日志但未发送给其他节点的时候发生故障宕机。所以就造成了上述的现象,对于其他实例的故障也类似。
从上面数据同步的过程可以看出,每一个实例都维护这一个最小committed index
,代表着小于等于当前index的数据已经全部被commit,那么如果在收到leader的提交的index的时候会进行比较,此时会出现一个有意思的问题呢,之前提交的log日志可能被修改,或者更准确的说被覆盖。具体原因我们下面分情况来讨论一下上面提到的数据同步时宕机的情况。
case1
当前节点发起写数据的请求,刚写入自己的log,还没有append到其他实例上,发生宕机,如下图所示:
接着很短的时间内故障恢复,并且之前的leader又被重新选举为leader,在写数据的时候,之前的数据已经存在,所以这条数据被认定为已经commit,所以不需要其他节点选举,直接将数据同步,如下图所示,S1恢复故障后继承被选举为leader,它直接将之前的数据同步到其他节点。
比较有意思,在故障恢复后,它不需要其他节点去投票就直接同步数据,但这并不会造成什么影响,只不过是数据可能会被重复消费,如果需要实现exactly-once
语义需要用户自己去实现,比如在客户端缓存一个ID,保存当前消费的offset来达到目的。
case 2
当前主节点宕机,但是故障恢复后其他节点被选举为主节点。如下图所示,S1之前为leader,发生故障宕机,此时重新选举:
新选举的节点为S2,它写入新数据,当前term=6,那么它会不会覆盖之前主节点写入的数据呢?
答案是肯定的,它会覆盖之前的数据,最终结果如下图所示:
对于整个故障以及恢复的流程通过下图可以清楚的了解,该图描述的情况上面已经介绍过了,不过这里还是要说一下e状态,上面的case 2提到,当前leader宕机之前数据只写入了自己的log,那么数据会被重新选举后的leader覆盖,如d状态,但是e状态,就是说当前leader将数据同步到其他实例后宕机,那么重新选举并不会被S5覆盖,这是因为S5会选举失败。
Cluster membership changes
一个集群在运行过程中,有时候我们需要对当前集群进行配置修改或者硬件升级,通常的办法是将集群下线,修改完毕之后,然后上线,但是这会导致这段时间内服务不可用,那么可不可以集群不下线,修改配置后自动更新呢?
Raft采用了joint consensus
机制将整个更新阶段划分为两个阶段,地一个阶段是之前的旧配置阶段,这个阶段不能处理客户端的请求,当该阶段commit之后,将会过渡到下一个阶段,即支持新配置的阶段。如下图,第一个阶段为Cold配置阶段,代表使用之前的配置,Cold,Cnew阶段是中间状态,它会被保存为一个Log Entry,接着就是Cnew阶段,那表示更新配置后的状态。
虚线表当前阶段被成功创建 ,但是未commit,实线代表当前阶段commit,Cold阶段刚开始被提交,急着创建了Cold,new阶段,但是没有被提交,直到Cold阶段完成Cold,new阶段才commit,在Cold,new 阶段完成之后,Cnew阶段才会被提交,如果在Cnew阶段leader宕机,那么就会退回到Cold或者Cold,new阶段进行处理。
Snapshot
最后,简单介绍一下快照机制,对于集群中的每一个实例,都会维护着一个快照,这个快照中保存了最后提交的index,当前term,以及当前数据的最后更新值,如图中保存x,y的最近状态的值x=0,y=9,在发生故障的时候,就会通过读取快照从而快速恢复到之前的状态。
至此,我们介绍完了Raft算法的选主,写入数据、故障恢复以及如何线上修改集群配置,最后介绍了快照机制。从这一系列过程足以看出Raft算法是一个优秀的分布式一致性协议算法,值得我们深入研究。
参考资料:
https://raft.github.io/
https://raft.github.io/raft.pdf