春节在家闲着没事看了几篇论文,把一致性协议的几篇论文都过了一遍。在看这些论文以前,我一直有一些疑惑,好比一样是有Leader和两阶段提交,Zookeeper的ZAB协议和Raft有什么不一样,Paxos协议到底要怎样才能用在实际工程中,这些问题我都在这些论文中找到了答案。接下来,我将尝试以本身的语言给你们讲讲这些协议,使你们可以理解这些算法。同时,我本身也有些疑问,我会在个人阐述中提出,也欢迎你们一块儿讨论。水平有限,文中不免会有一些纰漏门也欢迎你们指出。html
逻辑时钟其实算不上是一个一致性协议,它是Lamport大神在1987年就提出来的一个想法,用来解决分布式系统中,不一样的机器时钟不一致可能带来的问题。在单机系统中,咱们用机器的时间来标识事件,就能够很是清晰地知道两个不一样事件的发生次序。可是在分布式系统中,因为每台机器的时间可能存在偏差,没法经过物理时钟来准确分辨两个事件发生的前后顺序。但实际上,在分布式系统中,只有两个发生关联的事件,咱们才会去关心二者的先来后到关系。好比说两个事务,一个修改了rowa,一个修改了rowb,他们两个谁先发生,谁后发生,其实咱们并不关心。那所谓逻辑时钟,就是用来定义两个关联事件的发生次序,即‘happens before’。而对于不关联的事件,逻辑时钟并不能决定其前后,因此说这种‘happens before’的关系,是一种偏序关系。算法
图和例子来自于这篇博客数据库
此图中,箭头表示进程间通信,ABC分别表明分布式系统中的三个进程。服务器
逻辑时钟的算法其实很简单:每一个事件对应一个Lamport时间戳,初始值为0网络
若是事件在节点内发生,时间戳加1并发
若是事件属于发送事件,时间戳加1并在消息中带上该时间戳app
若是事件属于接收事件,时间戳 = Max(本地时间戳,消息中的时间戳) + 1分布式
这样,全部关联的发送接收事件,咱们都能保证发送事件的时间戳小于接收事件。若是两个事件之间没有关联,好比说A3和B5,他们的逻辑时间同样。正是因为他们没有关系,咱们能够随意约定他们之间的发生顺序。好比说咱们规定,当Lamport时间戳同样时,A进程的事件发生早于B进程早于C进程,这样咱们能够得出A3 ‘happens before’ B5。而实际在物理世界中,明显B5是要早于A3发生的,但这都没有关系。性能
逻辑时钟貌似目前并无被普遍的应用,除了DynamoDB使用了vector clock来解决多版本的前后问题(若是有其余实际应用的话请指出,多是我孤陋寡闻了),Google的Spanner 也是采用物理的原子时钟来解决时钟问题。可是从Larmport大师的逻辑时钟算法上,已经能够看到一些一致性协议的影子。学习
说到一致性协议,咱们一般就会讲到复制状态机。由于一般咱们会用复制状态机加上一致性协议算法来解决分布式系统中的高可用和容错。许多分布式系统,都是采用复制状态机来进行副本之间的数据同步,好比HDFS,Chubby和Zookeeper。
所谓复制状态机,就是在分布式系统的每个实例副本中,都维持一个持久化的日志,而后用必定的一致性协议算法,保证每一个实例的这个log都彻底保持一致,这样,实例内部的状态机按照日志的顺序回放日志中的每一条命令,这样客户端来读时,在每一个副本上都能读到同样的数据。复制状态机的核心就是图中 的Consensus模块,即今天咱们要讨论的Paxos,ZAB,Raft等一致性协议算法。
Paxos是Lamport大神在90年代提出的一致性协议算法,你们一直都以为难懂,因此Lamport在2001又发表了一篇新的论文《Paxos made simple》,在文中他本身说Paxos是世界上最简单的一致性算法,很是容易懂……可是业界仍是一致认为Paxos比较难以理解。在我看过Lamport大神的论文后,我以为,除去复杂的正确性论证过程,Paxos协议自己仍是比较好理解的。可是,Paxos协议仍是过于理论,离具体的工程实践还有太远的距离。我一开始看Paxos协议的时候也是一头雾水,看来看去发现Paxos协议只是为了单次事件答成一致,并且答成一致后的值没法再被修改,怎么用Paxos去实现复制状态机呢?另外,Paxos协议答成一致的值只有Propose和部分follower知道,这协议到底怎么用……可是,若是你只是把Paxos协议当作一个理论去看,而不是考虑实际工程上会遇到什么问题的话,会容易理解的多。Lamport的论文中对StateMachine的应用只有一个大概的想法,并无具体的实现逻辑,想要直接把Paxos放到复制状态机里使用是不可能的,得在Paxos上补充不少的东西。这些是为何Paxos有这么多的变种。
Basic-Paxos即Lamport最初提出的Paxos算法,其实很简单,用三言两语就能够讲完,下面我尝试着用我本身的语言描述下Paxos协议,而后会举出一个例子。要理解Paxos,只要记住一点就行了,Paxos只能为一个值造成共识,一旦Propose被肯定,以后值永远不会变,也就是说整个Paxos Group只会接受一个提案(或者说接受多个提案,但这些提案的值都同样)。至于怎么才能接受多个值来造成复制状态机,你们能够看下一节Multi-Paxos.
Paxos协议中是没有Leader这个概念的,除去Learner(只是学习Propose的结果,咱们能够不去讨论这个角色),只有Proposer和Acceptor。Paxos而且容许多个Proposer同时提案。Proposer要提出一个值让全部Acceptor答成一个共识。首先是Prepare阶段,Proposer会给出一个ProposeID n(注意,此阶段Proposer不会把值传给Acceptor)给每一个Acceptor,若是某个Acceptor发现本身历来没有接收过大于等于n的Proposer,则会回复Proposer,同时承诺再也不接收ProposeID小于等于n的提议的Prepare。若是这个Acceptor已经承诺过比n更大的propose,则不会回复Proposer。若是Acceptor以前已经Accept了(完成了第二个阶段)一个小于n的Propose,则会把这个Propose的值返回给Propose,不然会返回一个null值。当Proposer收到大于半数的Acceptor的回复后,就能够开始第二阶段accept阶段。可是这个阶段Propose可以提出的值是受限的,只有它收到的回复中不含有以前Propose的值,他才能自由提出一个新的value,不然只能是用回复中Propose最大的值作为提议的值。Proposer用这个值和ProposeID n对每一个Acceptor发起Accept请求。也就是说就算Proposer以前已经获得过acceptor的承诺,可是在accept发起以前,Acceptor可能给了proposeID更高的Propose承诺,致使accept失败。也就是说因为有多个Proposer的存在,虽然第一阶段成功,第二阶段仍然可能会被拒绝掉。
下面我举一个例子,这个例子来源于这篇博客
假设有Server1,Server2, Server3三个服务器,他们都想经过Paxos协议,让全部人答成一致他们是leader,这些Server都是Proposer角色,他们的提案的值就是他们本身server的名字。他们要获取Acceptor1~3这三个成员赞成。首先Server2发起一个提案【1】,也就是说ProposeID为1,接下来Server1发起来一个提案【2】,Server3发起一个提案【3】.
首先是Prepare阶段:
假设这时Server1发送的消息先到达acceptor1和acceptor2,它们都没有接收过请求,因此接收该请求并返回【2,null】给Server1,同时承诺再也不接受编号小于2的请求;
紧接着,Server2的消息到达acceptor2和acceptor3,acceptor3没有接受过请求,因此返回proposer2 【1,null】,并承诺再也不接受编号小于1的消息。而acceptor2已经接受Server1的请求并承诺再也不接收编号小于2的请求,因此acceptor2拒绝Server2的请求;
最后,Server3的消息到达acceptor2和acceptor3,它们都接受过提议,但编号3的消息大于acceptor2已接受的2和acceptor3已接受的1,因此他们都接受该提议,并返回Server3 【3,null】;
此时,Server2没有收到过半的回复,因此从新取得编号4,并发送给acceptor2和acceptor3,此时编号4大于它们已接受的提案编号3,因此接受该提案,并返回Server2 【4,null】。
接下来进入Accept阶段,
Server3收到半数以上(2个)的回复,而且返回的value为null,因此,Server3提交了【3,server3】的提案。
Server1在Prepare阶段也收到过半回复,返回的value为null,因此Server1提交了【2,server1】的提案。
Server2也收到过半回复,返回的value为null,因此Server2提交了【4,server2】的提案。
Acceptor1和acceptor2接收到Server1的提案【2,server1】,acceptor1经过该请求,acceptor2承诺再也不接受编号小于4的提案,因此拒绝;
Acceptor2和acceptor3接收到Server2的提案【4,server2】,都经过该提案;
Acceptor2和acceptor3接收到Server3的提案【3,server3】,它们都承诺再也不接受编号小于4的提案,因此都拒绝。
此时,过半的acceptor(acceptor2和acceptor3)都接受了提案【4,server2】,learner感知到提案的经过,learner开始学习提案,因此server2成为最终的leader。
刚才我讲了,Paxos还过于理论,没法直接用到复制状态机中,总的来讲,有如下几个缘由
那么其实Multi-Paxos,其实就是为了解决上述三个问题,使Paxos协议可以实际使用在状态机中。解决第一个问题其实很简单。为Log Entry每一个index的值都是用一个独立的Paxos instance。解决第二个问题也很简答,让一个Paxos group中不要有多个Proposer,在写入时先用Paxos协议选出一个leader(如我上面的例子),而后以后只由这个leader作写入,就能够避免活锁问题。而且,有了单一的leader以后,咱们还能够省略掉大部分的prepare过程。只须要在leader当选后作一次prepare,全部Acceptor都没有接受过其余Leader的prepare请求,那每次写入,均可以直接进行Accept,除非有Acceptor拒绝,这说明有新的leader在写入。为了解决第三个问题,Multi-Paxos给每一个Server引入了一个firstUnchosenIndex,让leader可以向向每一个Acceptor同步被选中的值。解决这些问题以后Paxos就能够用于实际工程了。
Paxos到目前已经有了不少的补充和变种,实际上,以后我要讨论的ZAB也好,Raft也好,均可以看作是对Paxos的修改和变种,另外还有一句流传甚广的话,“世上只有一种一致性算法,那就是Paxos”。
ZAB即Zookeeper Atomic BoardCast,是Zookeeper中使用的一致性协议。ZAB是Zookeeper的专用协议,与Zookeeper强绑定,并无抽离成独立的库,所以它的应用也不是很普遍,仅限于Zookeeper。但ZAB协议的论文中对ZAB协议进行了详细的证实,证实ZAB协议是可以严格知足一致性要求的。
ZAB随着Zookeeper诞生于2007年,此时Raft协议尚未发明,根据ZAB的论文,之因此Zookeeper没有直接使用Paxos而是本身造轮子,是由于他们认为Paxos并不能知足他们的要求。好比Paxos容许多个proposer,可能会形成客户端提交的多个命令无法按照FIFO次序执行。同时在恢复过程当中,有一些follower的数据不全。这些断论都是基于最原始的Paxos协议的,实际上后来一些Paxos的变种,好比Multi-Paxos已经解决了这些问题。固然咱们只能站在历史的角度去看待这个问题,因为当时的Paxos并不能很好的解决这些问题,所以Zookeeper的开发者创造了一个新的一致性协议ZAB。
ZAB其实和后来的Raft很是像,有选主过程,有恢复过程,写入也是两阶段提交,先从leader发起一轮投票,得到超过半数赞成后,再发起一次commit。ZAB中每一个主的epoch number其实就至关于我接下来要讲的Raft中的term。只不过ZAB中把这个epoch number和transition number组成了一个zxid存在了每一个entry中。
ZAB在作log复制时,两阶段提交时,一个阶段是投票阶段,只要收到过半数的赞成票就能够,这个阶段并不会真正把数据传输给follower,实际做用是保证当时有超过半数的机器是没有挂掉,或者在同一个网络分区里的。第二个阶段commit,才会把数据传输给每一个follower,每一个follower(包括leader)再把数据追加到log里,此次写操做就算完成。若是第一个阶段投票成功,第二个阶段有follower挂掉,也没有关系,重启后leader也会保证follower数据和leader对其。若是commit阶段leader挂掉,若是此次写操做已经在至少一个follower上commit了,那这个follower必定会被选为leader,由于他的zxid是最大的,那么他选为leader后,会让全部follower都commit这条消息。若是leader挂时没有follower commit这条消息,那么这个写入就当作没写完。
因为只有在commit的时候才须要追加写日志,所以ZAB的log,只须要append-only的能力,就能够了。
另外,ZAB支持在从replica里作stale read,若是要作强一致的读,能够用sync read,原理也是先发起一次虚拟的写操做,到不作任何写入,等这个操做完成后,本地也commit了此次sync操做,再在本地replica上读,可以保证读到sync这个时间点前全部的正确数据,而Raft全部的读和写都是通过主节点的
Raft是斯坦福大学在2014年提出的一种新的一致性协议。做者表示之因此要设计一种全新的一致性协议,是由于Paxos实在太难理解,并且Paxos只是一个理论,离实际的工程实现还有很远的路。所以做者狠狠地吐槽了Paxos一把:
所以,Raft的做者在设计Raft的时候,有一个很是明确的目标,就是让这个协议可以更好的理解,在设计Raft的过程当中,若是遇到有多种方案能够选择的,就选择更加容易理解的那个。做者举了一个例子。在Raft的选主阶段,原本能够给每一个server附上一个id,你们都去投id最大的那个server作leader,会更快地达成一致(相似ZAB协议),但这个方案又增长了一个serverid的概念,同时在高id的server挂掉时,低id的server要想成为主必须有一个等待时间,影响可用性。所以Raft的选主使用了一个很是简单的方案:每一个server都随机sleep一段时间,最先醒过来的server来发起一次投票,获取了大多数投票便可为主。在一般的网络环境下,最先发起投票的server也会最先收到其余server的同意票,所以基本上只须要一轮投票就能够决出leader。整个选主过程很是简单明了。
除了选主,整个Raft协议的设计都很是简单。leader和follower之间的交互(若是不考虑snapshot和改变成员数量)一共只有2个RPC call。其中一个仍是选主时才须要的RequestVote。也就是说全部的数据交互,都只由AppendEntries 这一个RPC完成。
理解Raft算法,首先要理解Term这个概念。每一个leader都有本身的Term,并且这个term会带到log的每一个entry中去,来表明这个entry是哪一个leader term时期写入的。另外Term至关于一个lease。若是在规定的时间内leader没有发送心跳(心跳也是AppendEntries这个RPC call),Follower就会认为leader已经挂掉,会把本身收到过的最高的Term加上1作为新的term去发起一轮选举。若是参选人的term还没本身的高的话,follower会投反对票,保证选出来的新leader的term是最高的。若是在time out周期内没人得到足够的选票(这是有可能的),则follower会在term上再加上1去作新的投票请求,直到选出leader为止。最初的raft是用c语言实现的,这个timeout时间能够设置的很是短,一般在几十ms,所以在raft协议中,leader挂掉以后基本在几十ms就可以被检测发现,故障恢复时间能够作到很是短。而像用Java实现的Raft库,如Ratis,考虑到GC时间,我估计这个超时时间无法设置这么短。
在Leader作写入时也是一个两阶段提交的过程。首先leader会把在本身的log中找到第一个空位index写入,并经过AppendEntries这个RPC把这个entry的值发给每一个follower,若是收到半数以上的follower(包括本身)回复true,则再下一个AppendEntries中,leader会把committedIndex加1,表明写入的这个entry已经被提交。如在下图中,leader将x=4写入index=8的这个entry中,并把他发送给了全部follower,在收到第一台(本身),第三台,第五台(图中没有画index=8的entry,但由于这台服务器以前全部的entry都和leader保持了一致,所以它必定会投赞成),那么leader就得到了多数票,再下一个rpc中,会将Committed index往前挪一位,表明index<=8的全部entry都已经提交。至于第二台和第四台服务器,log内容已经明显落后,这要么是由于前几回rpc没有成功。leader会无限重试直到这些follower和leader的日志追平。另一个多是这两台服务器重启过,处于恢复状态。那么这两台服务器在收到写入index=8的RPC时,follower也会把上一个entry的term和index发给他们。也就是说prevLogIndex=7,prevLogTerm=3这个信息会发给第二台服务器,那么对于第二台服务器,index=7的entry是空的,也就是log和leader不一致,他会返回一个false给leader,leader会不停地从后往前遍历,直到找到一个entry与第二台服务器一致的,从这个点开始从新把leader的log内容发送给该follower,便可完成恢复。raft协议保证了全部成员的replicated log中每一个index位置,若是他们的term一致,内容也必定一致。若是不一致,leader必定会把这个index的内容改写成和leader一致。
其实通过刚才个人一些描述,基本上就已经把Raft的选主,写入流程和恢复基本上都讲完了。从这里,咱们能够看出Raft一些很是有意思的地方。
第一个有意思的地方是Raft的log的entry是可能被修改的,好比一个follower接收了一个leader的prepare请求,把值写入了一个index,而这个leader挂掉,新选出的leader可能会从新使用这个index,那么这个follower的相应index的内容,会被改写成新的内容。这样就形成了两个问题,首先第一个,raft的log没法在append-only的文件或者文件系统上去实现,而像ZAB,Paxos协议,log只会追加,只要求文件系统有append的能力便可,不须要随机访问修改能力。
第二个有意思的地方是,为了简单,Raft中只维护了一个Committed index,也就是任何小于等于这个committedIndex的entry,都是被认为是commit过的。这样就会形成在写入过程当中,在leader得到大多数选票以前挂掉(或者leader在写完本身的log以后还没来得及通知到任何follower就挂掉),重启后若是这个server继续被选为leader,这个值仍然会被commit永久生效。由于leader的log中有这个值,leader必定会保证全部的follower的log都和本身保持一致。然后续的写入在增加committedIndex后,这个值也默认被commit了。
举例来讲,如今有5台服务器,其中S2为leader,可是当他在为index=1的entry执行写入时,先写到了本身的log中,还没来得及通知其余server append entry就宕机了。
当S2重启后,任然有可能被从新当选leader,当S2从新当选leader后,仍然会把index=1的这个entry复制给每台服务器(可是不会往前移动Committed index)
此时S2又发生一次写入,此次写入完成后,会把Committed index移动到2的位置,所以index=1的entry也被认为已经commit了。
这个行为有点奇怪,由于这样等于raft会让一个没有得到大多数人赞成的值最终commit。这个行为取决于leader,若是上面的例子中S2重启后没有被选为leader,index=1的entry内容会被新leader的内容覆盖,从而不会提交未通过表决的内容。
虽说这个行为是有点奇怪,可是不会形成任何问题,由于leader和follower仍是会保持一致,并且在写入过程当中leader挂掉,对客户端来讲是原本就是一个未决语义,raft论文中也指出,若是用户想要exactly once的语义,能够在写入的时候加入一个相似uuid的东西,在写入以前leader查下这个uuid是否已经写入。那么在必定程度上,能够保证exactly once的语义。
Raft的论文中也比较了ZAB的算法,文中说ZAB协议的一个缺点是在恢复阶段须要leader和follower来回交换数据,这里我没太明白,据我理解,ZAB在从新选主的过程当中,会选择Zxid最大的那个从成为主,而其余follower会从leader这里补全数据,并不会出现leader从follower节点补数据这一说。
目前,通过改进的Paxos协议已经用在了许多分布式产品中,好比说Chubby,PaxosStore,阿里云的X-DB,以及蚂蚁的OceanBase,都选用了Paxos协议,但他们都多多少少作了一些补充和改进。而像Raft协议,广泛认为Raft协议只能顺序commit entry,性能没有Paxos好,可是TiKV中使用了Raft,其公开的文章宣传对Raft作了很是多的优化,使Raft的性能变的很是可观。阿里的另一个数据库PolarDB,也是使用了改进版的Parallel-Raft,使Raft实现了并行提交的能力。相信将来会有更多的基于Paxos/Raft的产品会面世,同时也对Raft/Paxos也会有更多的改进。
本文为云栖社区原创内容,未经容许不得转载。