数据一致性与Paxos算法

1. CAP与数据一致性

1-1. CAP概要

分布式系统的假设是,工作在网络环境下的系统拥有多个节点,而这些节点本身会由于各种原因而变得不稳定。这其中就有一个非常重要的概念——CAP原理。这个原理指导着大多数分布式系统的设计过程,CAP原理大致是说分布式系统中一定存在三个特性:一致性(Consistency)、分区容忍性(Partition)和可用性(Availability),且这三个特性在分布式系统的设计中不可能全部同时满足。

                                                       

举个例子,一个分布式系统中有N个节点通过网络链接在一起协同工作。首先你不能将完整的数据X只存放在一个节点上,这是因为一旦这个节点由于各种原因停止工作了,数据X就不能被访问那肯定就不满足系统可用性了,并且一旦这个节点不能再被恢复,数据X就永远丢失了。所以数据X至少也应该在不同的节点上存储多份,存储的副本量越多越能保证数据X的安全,也更能保证即使在多个节点同时不可用的情况下,数据X也同样能够被访问。这就是分区性的要求,按照普遍经验,数据X的副本数至少应该有三份。在这种情况下,当数据X发生变化时如何对这些副本进行更新呢?最理想的效果是,当如果客户端要求发出数据X的更新请求后,从任何一个节点访问数据X都可以拿到它最新的状态,这就是一致性要求。当然这个最理想的效果太理论化了,要知道基于网络工作的分布式系统受很多外在因素影响:要是同步过程中发现某个副本节点无法连接了怎么办?要是同时又有一个客户要求再次更新数据X怎么办?如果真要达到这么理论的一致性要求,那就只能让所有需要读/写数据X的客户端等待,直到完成数据X的所有副本同步后,再进行响应,但显然这从可用性的角度出发又是不满足要求的。

                                                

再例如,关系型数据库的设计通常基于ACID原理,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),而且关系型数据库普遍采用事务技术实现ACID原理,其中每一个事务就是最小化的原子操作,还可以设置不同的事务级别,包括可读未提交、可重复读这样的事务级别;关系型数据库还规定了一旦事务正确提交就不能进行数据回滚,如果要继续修改数据就只能启动一个新的事务。而这一切都是为了保证数据库系统是一个强一致性系统。就连架设在各个关系型数据库实例之上的分布式事务机制,也是为了保证这个目标:只要有任何一个参与分布式事务过程的数据库实例出现异常,整个分布式事务就无法正常提交,当然也就无法完成数据写操作。当然老数据是可以进行读操作的,即使某些节点出现了问题其他数据库节点也可以承担这个读操作,因为这样的操作不存在一致性改变的风险。

任何分布式系统都不可能以CAP原理中三个特性同时作为主要设计目标,要到达非常高的强一致性和较高的分区容忍性,就必须以牺牲可用性作为代价(注意是牺牲而不是完全放弃)。而分区容忍性又是分布式系统成立的基础,没有任何分区容忍性的分布式系统甚至都不能称为分布式系统;高压力的环境下又不能过度牺牲分布式系统的可用性,要知道99.99%的可用性和99.999%的可用性完全就是两个档次的分布式系统。

1-2. 数据一致性

所以类似分布式事务机制那样过度强调数据一致性的设计思路就不太受主流分布式系统设计思想欢迎,至少从目前各种分布式系统公布的设计原理来看是这样的。例如HDFS这样的分布式文件系统,首先强调的是高可用性和一定的分区容忍性其次才是数据一致性,数据一致性通过副本保证。但并不是所有副本都完成写入动作后HDFS才认为数据一致,而是只要一部分数据副本完成了写入动作,HDFS就认为数据成功写入且客户端可以调用新数据,而没有完成同步的副本将会接着进行数据同步,达到数据的最终一致性。DNS也是一种需要首先考虑高可用性和分区容忍性的分布式系统,由于DNS服务跨不同网络,所以当一个DNS配置更改后新的域名解析请求要等全球所有DNS节点都更改生效了才继续工作,但DNS的组织结构能够保证多个DNS服务节点解析www.XXXX.com这个域名的结果最终是一致的。这种牺牲系统一致性保证系统可靠性和分区容忍性的设计思路,在分布式系统领域有一个特定的称呼:BASE。基本可用(Basically Availble)、软状态(Soft-state)和最终一致(Eventual Consistency)。

在上一小节简单说明CAP原理和示例的内容中,提到两个关于一致性的概念:强一致性和最终一致性。强一致性可以概括为任何时刻客户在分布式系统中获取数据X,无论它在分布式系统的哪一个节点进行这个操作,其获取到的数据X都是一致的。从这个定义来看,分布式事务机制就是一种强一致性的实现。有强一致性就有对应的弱一致性定义,弱一致性不是说不保持数据的一致性,而是说不保证数据每时每刻都一致,也不承诺什么时候才能保证分布式系统的任何节点都能读取到一致的数据。而最终一致性是弱一致性的一种特定结果,既是承诺基于弱一致性,在经过一个数据不一致的时间窗口后,最终能保证数据一致。这个数据不一致的时间窗口在客户端看来非常短,而且分布式系统还可以通过多种方式向客户端屏蔽不一致的数据,例如主从副本方式。

2. 最终一致性的经典实现:Paxos算法

Paxos算法又被称为两阶段算法,请注意不是实现分布式事务的两阶段提交协议。Paxos算法的工作场景是基于CAP原理构建的分布式系统,是在这样的环境中如何高效率的达到数据的最终一致性;而两阶段/三阶段提交协议的工作场景是基于AICD原则的数据强一致性系统。关于这两种场景的设计思路已经在上文中详细介绍过,这里就不再赘述了。

Paxos算法中有四个角色Client(议题产生者)、Proposer(正式提议者)、Acceptor(投票决策者)和Learner(最终结果学习者),这四个角色中Client和Learner角色是两个广义的概念,而真正完成算法的是Proposer角色和Acceptor角色,所以很多技术文章中都是重点讲解后两个角色的工作过程。Proposer角色是正真的提议发起者,负责发起提议投票、总结提议投票结果以及变更后发起新的投票;Acceptor角色是决策者,对于某个提案发表自己的投票结果,并交由Proposer角色进行处理,各个Acceptor角色都有自己的都票结果,不受其它Acceptor角色的影响。

总的来说Paxos算法在处理这样一个场景:当分布式系统中的多个Proposer对于某件事的结果不能达成一致时,就发起一个提案,由多个Acceptor负责进行投票并让多个Proposer对于这件事的结果达成最终一致。“这件事”可以指代很多事情,例如“选主”,再例如K所代表的变量被同时赋予V1和V2两个值甚至更多的值,等等。这就是上文提到的最终一致性的一个具体实现算法——每个Proposer对某件事情的判断过程可能在某段时间内是不一致的,但最终在客户端Client上呈现的结果将是一致的。

需要说明的是,Paxos算法最早由莱斯利·兰伯特(Leslie Lamport)提出,在他的论文中《The Part-Time Parliament》本来就介绍了Paxos算法的多种变体,加上在实际应用中各技术人员又加入了自己的理解,所以才出现了Paxos算法许多不一样的实现版本,但实际上这些算法版本中各角色的工作都有其处理原则,只要掌握了这些处理原则,理解Paxos就不难了。

2-1. Basic-Paxos算法

虽然说Paxos算法和两阶段提交协议的设计思路完全不一样,但前者借鉴了后者的一些思想——即分为准备阶段(Prapare)和赋值阶段(Accept) 。为了顺利的完成数据最终一致性这个工作目标,在整个工作过程中Proposer角色和Acceptor角色要分别保持以下的的工作原则:

  • Proposer和Acceptor一个负责发起提议,一个负责响应提议。后者要尽量回复前者的每一个请求,无论这次请求根据Acceptor的处理原则是成功了还是失败了。如果Proposer在规定的时间内没有获得Acceptor的响应,则往最坏的情况来进行考虑。

  • 针对一个提议X的多轮投票,各个Proposer需要保证自己发起的后一轮投票轮次编号(vote)一定大于前一轮投票轮次编号。而各个Proposer不必保证自己的投票轮次是全局最大的,当然为了减少在第一阶段的授权冲突,也可以保证新的投票轮次全局最大(这个原因分析在后文中会详细讨论)。

  • 什么情况下针对一个议题X,Proposer不再发起新一轮投票并认为得到了最终一致的数据呢?当Proposer收集了各个Acceptor的最终投票值,并发现其中至少N/2 + 1个Acceptor的最终值都是V,则认为议题X的最终结果为V。实际上这个工作规则属于Proposer角色,但很多时候Proposer、Proposer、Acceptor都是一个应用程序。

  • 在Prapare阶段,Acceptor需要保证不接受投票轮次编号(vote)小于等于当前PrepareVote的投票轮次的授权(发起)申请。

  • 在Accept阶段,Acceptor需要保证不接受投票轮次编号(vote)小于当前PrepareVote的投票轮次的赋值申请。以上两条工作原则是最重要,为什么Pasox算法会有若干种变体呢?其原因就是在算法应用实现阶段,具体的算法实现在保证这两个基本工作原则下,为了提高工作效率而对实现过程进行微调。

2-1-1. Prapare准备阶段

              

首先需要介绍几个在Acceptor角色上需要被持久化保存的数据属性:

  • PrepareVote保存了当前Acceptor接收到的已完成投票授权的最大投票轮次
  • AcceptedVote保存了当前Acceptor在赋值阶段完成投票赋值的投票轮次
  • AcceptedValue保存了当前Acceptor在赋值阶段被赋予的值

1、第一个阶段Proposer和Acceptor至少要完成一次网络通讯,其主要目的是确定针对提议X的当前投票轮次是否能被授权。换句话说,根据Acceptor在准备阶段的工作原则,即使确定当前投票轮次的编号值是大于Acceptor中记录的PrepareVote的值。处理过程很简单,即Proposer向所有Acceptor发出关于发起提议X的新一轮投票的申请,并等待各个Acceptor进行响应。当然会有一个超时时间,如果超过这个时间还没有得到Acceptor的响应,则认为已经被拒绝。如果有超过N/2 + 1个节点在规定的时间内没有回复响应,那就说明整个选举系统发现了问题,则终止操作抛出错误,向客户端反馈异常信息

2、收到这个发起新的一轮投票操作的授权请求后,各个Acceptor开始判断是否可以进行授权。判断的原则只与PrepareVote有关,既是如果当前申请的投票轮次小于等于PrepareVote的赋值,则拒绝授权;其它情况都要接受授权,并更改PrepareVote属性为当前新的投票伦次编号。这里有一个隐藏含义,即这个Acceptor新授权的投票轮次编号,一定大于之前PrepareVote的值。

3、Proposer将负责汇总所有Acceptor的响应情况,并根据汇总的情况判断下一步的操作。无论Acceptor是授权这次投票还是拒绝这次投票,给出的响应信息中都最好包括当前Acceptor所记录的PrepareVote、AcceptedVote和AcceptedValue信息,这样有利于Proposer分析为什么会被Acceptor拒绝。下图展示了Proposer在汇总所有Acceptor的响应时可能出现的各种情况:

                 

当然还有一种情况三,就是超过(包括)N/2 + 1个Acceptor节点在规定的时间内没有反馈结果,这种情况直接判定Paxos系统崩溃,所以就不做进一步讨论了。请注意,无论是上图的哪种情况,只要至少N/2 + 1个Acceptor节点的AcceptedValue为同一个值,就认为提议X的结果达到了最终一致,整个Paxos算法过程也结束。

3.1、在Proposer得到的响应情况一中,至少N/2 + 1个Acceptor允许这轮投票。这些Acceptor就形成了一个集合Q,这个集合Q将继续下一步骤的操作。这时集合Q中的Acceptor是否已有AcceptedValue就很重要了:如果集合Q中没有任何一个Acceptor的AcceptedValue属性有值,则当前Proposer会在下一步提议自己的值为集合Q中每一个Acceptor的赋值目标;如果集合Q中至少存在一个Acceptor的AcceptedValue属性有值,则Proposer会选择一个AcceptedVote最大的AcceptedValue属性值,作为当前Proposer会在下一步进行Acceptor赋值的目标。

3.2、在Proposer得到的响应情况二中,始终未达到N/2 + 1个Acceptor允许这轮投票——无论是不满足Acceptor的授权原则还是Acceptor超时未响应。只要至少N/2 +1个Acceptor所回复的AcceptedValue属性值相同,则表示针对提议X已经形成了最终一致的结果,无需再进行投票了。否则,Proposer会将自己的投票轮次编号进行增加后,再发起投票——这个增加规则后续再讨论,读者目前可以认为是+1。

2-1-2. Accept赋值阶段

一旦有N/2 + 1个Acceptor节点授权了本轮投票,Proposer就可以进入第二阶段——赋值阶段,第二阶段将以上一阶段形成的多数派集合Q作为操作目标。如下图所示:

          

1、Proposer将会以上一阶段3.1步骤中所确定的value和自己的vote一起发送给集合Q中的每一个Acceptor,并等待回复。

2、Acceptor收到赋值请求后,将会按照判断原则确认是否进行赋值。这个判断原则上文已经说过了,这里再说一次。如果当前收到的vote小于当前Acceptor的PrepareVote属性值,则不会进行赋值。为什么Acceptor上的PrepareVote会发生变化呢?这是因为在这个Proposer从第一阶段到第二阶段的操作间隙,另一个或者多个Proposer使用编号更大的vote发起了更新一轮的投票,并得到当前Acceptor的授权。如果当前收到的vote等于当前Acceptor的PrepareVote属性值则接受这次赋值,这时Acceptor将更改其AcceptedVote属性为vote,更改其AcceptedValue属性为value。

注意一种情况,Acceptor会不会在第二阶段操作时收到一个vote大于当前PrepareVote的赋值请求呢?这是不会的,因为任何Acceptor要更换PrepareVote,只可能更换比当前PrepareVote更大的值,所以之前被Acceptor同意授权的vote一定会小于或者等于当前Acceptor的PrepareVote属性值。

3、赋值操作完成后,Acceptor将向Proposer返回赋值操作后的AcceptedValue属性和AcceptedVote属性。换句话说就是,即使Acceptor拒绝了第二阶段的赋值操作,也要向Proposer返回AcceptedValue属性值。以下为Proposer端汇总统计时,可能出现的情况:

                      

3.1、Acceptor收到集合Q中所有Acceptor的赋值结果后,就会进行汇总判断。如果发现所有赋值结果都是一样的value,则认为针对议题X形成了最终一致的结果。整个投票过程结束,value就是达成的最终值。

3.2、如果收到集合Q中所有Acceptor的赋值结果,并进行比较的过程中,发现任何一个赋值结果不一致,则认为赋值操作失败。这时Proposer会将自己的投票轮次编号进行增加后,再回到第一阶段重新发起投票。

2-1-3. 分析一种极端情况

Paxos算法的一个核心思路在于形成多数派决议,要形成核心思路Acceptor就必须按照自己的两个工作原则进行授权操作和赋值操作。这就是为什么我们在介绍Paxos算法时经常提到N/2 + 1的边界节点数量的原因。因为一旦形成多数派,决定最终一致性的关键表决就可以落在唯一一个节点上,也就是说如果第X1轮投票和第X2轮投票如果都同时拿到了多数派Acceptor的授权,那么它们的核心战略点就是要去抢占授权X1投票的Acceptor集合和授权X2投票的Acceptor集合求交集后至少存在一个的Acceptor节点。好消息是赋值阶段X1轮投票的编号和X2轮投票的编号总是不同的,也总有大小差异的,而Acceptor的工作方式决定了它只会接受编号更大的投票操作的赋值。

这个原理可以扩展到任意多个Proposer,那么有的读者会问了,会不会出现一种每个Acceptor的AcceptedValue属性都被赋值,且都没有达到多数派的特殊情况呢(如下图所示)?

                                                  

答案是不会的,本小节我们将使用反向论证证的方式来进行阐述。首先要出现三个X1的赋值结果Proposer1便不能继续赋值这样的现象,只可能是一种情况即Proposer1在经过两阶段操作,并在进行Acceptor1~Acceptor3赋值的时候,后者三个节点上PrepareVote等于Proposer1的Vote,而在Proposer1准备进行后续三个节点赋值时,却发现Acceptor4~Acceptor6的PrepareVote改变了。如下图所示:

                 

这时发起V2轮次投票的ProposerB,有两种情况,如下图所示:

第一种情况ProposerB还没有接收到至少N/2 + 1个Acceptor节点的授权结果,还处于第一阶段(这里Acceptor节点个数为6个,所以至少应该收到4个Acceptor的授权结果),这时ProposerB会一直等待汇总,如果等待超时都没有收到必要数量的结果,那么ProposerB继续增加它的投票编号,并重新发起。这时同样能够保证Acceptor集合不被之前的Proposer继续赋值——因为投票编号仍然最大。

如果是上图所示的第二种情况,就更简单了。ProposerB节点已经收到了至少4个Acceptor节点的授权结果,而这4个Acceptor节点,至少有一个节点携带了AcceptedValue为X1的值——因为目前被赋予X1的Acceptor节点个数,刚好为一半3个节点,和N/2 + 1个节点求交集后,至少有一个重叠的Acceptor节点(注意,基数为偶数,如果为奇数就更不会出现我们现在讨论的极端情况了)。也就是说当ProposerB节点进入第二阶段赋值操作时,向剩余Acceptor节点传递的值同样为X1,而不是自身原始提议的X2,所以最终本小节最初所述的极端情况不会出现。

2-1-4、一种投票轮次的初始和增加规则

在目前我们讨论的设计中,提到一个投票轮次的确定问题。不同Proposer发起的投票可以不一定全局递增,而同一个Proposer必须保证自己的发起的新一轮投票编号vote,一定是递增的(增量必须为1吗?这个不一定)。这是因为Acceptor在第一阶段的工作原则是,只接受vote大于当前PrepareVote的新一轮投票授权,那么对于某个Proposer而言,最不济的情况就是无论自己本身如何发起新一轮的投票,都不会得到Acceptor的授权,而最终结果只能听命于别的Proposer所提议的值

                   

很显然上图中无论ProposerB如何发起新一轮投票,都会在第一阶段被Acceptor拒绝掉,因为至少有一个Proposer的投票轮次编号一直比他的大。这种算法的简便处理方式虽然一定程度上降低了代码的编写难度,但是只能算作伪算法实现(目前很多系统为了平衡性能、维护性和实现难易度,都会采用这种方式),因为所有投票轮次的发起和赋值只会听从于唯一一个Proposer,在算法的执行过程中根本就不会出现投票冲突的情况;另一个方面,对并发状态下无法最终确认选值的Client来说也不公平,因为所有Client对最终赋值的选择也只会听命于唯一一个Client(忘了说了,Client是整个Paxos中另一个角色,这个角色负责向对应的Proposer提交需要进行表决的提案)。所以我们需要一种投票轮次编号递增的方式,来保证不同轮次的投票请求真正的竞争起来。

有的读者可能第一个想到的就是zookeeper这样的分布式协调器来生成编号,保证编号的唯一性和递增性,甚至很多学术资料、网络文章上都有提到使用这样的方式。拜托!要是能够使用zookeeper解决这个问题,那我们还实现Paxos算法干什么?所有解决冲突的工作都交给zookeeper就好了嘛。之所以要实现Paxos算法,就是因为要建造一个和ZK同属一个工作层的协调组件,所以关于编号的问题就只能自己想办法了。

这里介绍一种对投票轮次的编号方式,可以有效减少编号冲突,并在针提案X的投票过程中真正产生竞争。这个编号方式的前提条件是,各个Proposer工作在局域网环境下,并能通过组播的方式发现对方。在各个Proposer内部有一个使用Proposerid进行排序后的Proposer集合列表。这时就可以通过余数原则进行编号了:

                      

(N × V) + index + 1,这个公式中,N表示集合大小,V表示投票轮次(从0开始),index表示当前Proposer所在集合的位置索引。例如,设N==4时,id为1的存在于这个有序集合列表第一个索引位置的Proposer,可以使用的投票编号就是1、5、9、13……,存在于这个有序集合列表第二个索引位置的Proposer,可以使用的投票编号就是2、6、10、14、18……,存在于这个有序集合列表第三个索引位置的Proposer,可以使用的编号就是3、7、11、15、19……

那么这样的编号设计就不会重复了吗?当然会,特别是在整个Paxos算法系统的启动阶段。例如整个Paxos算法中所有N个节点的Proposer都已经启动了,但是某个Proposer上还暂时只发现了N-2个Proposer节点,这样该Proposer计算得出的自己所使用的编号,就可能和另外某个Proposer所计算得出的自己使用的编号重复。而且这个差异还与使用的不同节点发现方式而不同,例如在后续文章中给出的Basic-Paxos算法实现代码中,就使用组播方式进行Proposer节点发现,而这种节点发现方式就会使每个Proposer节点上的节点发现进度在Paxos算法系统启动之初出现不一致。

不过好消息是,Acceptor在第一阶段的工作原则中,只会授权大于当前AcceptedVote的投票申请(此时AcceptedVote <= PrepareVote)。也就是说当两个或者多个持有相同投票伦次的Proposer向同一个Acceptor申请投票授权时,Acceptor只会授权其中一个,另一个的授权申请将被Acceptor拒绝,这就保证了有相同投票轮次编号的授权请求在同一个Acceptor上不会被重复同意,那么这些有同样投票轮次编号的Proposer就可以决定自己是否拿到了授权多数派。如果没有拿到授权,当Proposer集合稳定存在后,就会有新的且更大的投票轮次编号了。

2-3. Paxos算法变种

上文我们提到,Paxos算法的设计是为了解决多个Proposer针对一个具体提案产生的多个赋值,并最终帮助多个Proposer确定一个大家一致认可的赋值,最终达到一致性。从理论层面上说,如果多个Proposer一直不产生竞争起来,那么算法研究就没有意义。例如上文中提到的一个Proposer A的投票轮次编号始终从1000开始,其它Proposer的投票轮次编号始终从100开始,那样的话Paxos算法系统中其它Proposer发起的提案申请在第一轮就会被拒绝(在Proposer A的第一次提案申请到达Acceptor之前,可能会有其它Proposer发起的提案进入第二轮,但最终也会被大多数Acceptor决绝),所以这样的Proposer编号设定实际上就会让所有Proposer都听命于Proposer A的提案决断。

可是从实际应用的角度出发,却刚好相反——让Paxos算法过程尽可能少的产生竞争有利于提高算法效率。在Basic-Paxos算法中,任何一个提案要被通过都至少需要和单独的Acceptor通讯两次(这里还不考虑多Proposer抢占的问题),如果有N个Acceptor则至少进行 (N/2 + 1)× 2次网络通讯。如果在一个完整的Paxos算法中,网络通讯占去了相当的时间,那么Paxos算法的性能自然不会好。所以Paxos算法会产生了很多变种形式。本节我们就对一些主要的变种形式进行说明

2-4. Paxos变种:Multi-Paxos算法

介绍Basic-Paxos时我们只针对了一个议题形成多数派达成一致的值的算法过程进行了说明,无论有多个Proposer参与了抢占Acceptor赋值权的过程,无论为了形成这个多数派决议发起了多少轮Prepare投票申请,也无论Acceptor拒绝了多少次Acceptor赋值请求。所有这一切都只是为了处理一个议题,如:关于K的赋值是多少、关于你爸的名字是A还是B、关于下一次事件触发时间是在1000秒以后还是2000秒以后。

但实际业务场景往往不是这样的,在分布式存储环境中,拥有Paxos算法功能的整个系统需要在生命周期内对多种议题进行一致性处理,例如日志编号X的操作是将编号K的值赋为A还是赋为B;下一个日志编号X+1的操作,是将K的值赋为Y还是赋为W……

为了将Basic-Paxos算法用于实际应用中,Basic-Paxos算法有一种变种算法叫做Multi-Paxos,其中的“Multi”当然就代表在Paxos算法系统存活的整个生命周期需要处理的多个提案。Multi-Paxos算法的工作过程大致可以概括为选主保持、正式工作、重选恢复,本节我们就和大家一起讨论一下Multi-Paxos算法这几个工作步骤。

2-4-1. Multi-Paxos:选主与保持

Multi-Paxos算法核心过程的工作原则和Basic-Paxos算法核心工作原则是相同的,即多数派决定、Acceptor第一轮只接受更大的投票编号、Acceptor第二轮只接受和目前第一轮授权相同的投票编号。但是为了提高对每一个议题的判定效率,Multi-Paxos算法在第一投票过程中做了一个调整:首先通过一个完整的Paxos算法过程完成一次选主操作,从多个Proposer中选出一个Proposer Leader,待所有Proposer都确认这个Proposer Leader后,后续的带有业务性质的提案,都只由这个Proposer Leader发起和赋值。这样的做法至少有两个好处:

  • 其它Proposer在确定自己不是Proposer Leader后,就不再发起任何提案的投票申请,而只是接受Learner(最终结果学习者)传来的针对某个提案的最终赋值结果。而往往Learner和Proposer都存在于同一个进程中,所以这基本上消除了这些Proposer的网络负载。

  • 一旦Proposer Leader被选出,且后续的提案只由它发起和赋值。那么就不存在投票申请冲突的情况了——因为没有任何其它的Proposer和它形成竞争关系。Acceptor所接受的投票轮次编号也都由它提出,那么这些后续提案的第一阶段都可以跳过,而直接进入提案的赋值阶段。这样类似Basic-Paxos算法中要确定一个提案的赋值至少需要和每个Acceptor都通讯两次的情况就被削减成了只需要一次通讯。

请注意,Multi-Paxos算法的选主过程和上文我们提到的直接决定一个起始投票编号最大的Proposer方式的本质区别是:前者是投票决定的,而后者是被固定的,一旦后者那个投票编号最大的Proposer宕机了,其它Proposer在提案投票过程中就会一直出现冲突。另外,由于多个Proposer中并没有Leader的概念,所以这些Proposer就算提案投票申请被拒,也会不停的再发起新一轮投票申请,而这样并不会减少网络通信压力。

         

      

要说明整个Multi-Paxos算法的可行性,就需要明确一个事实。当我们介绍Basic-Paxos算法时,为了读者能够腾出所有精力缕清它的工作步骤,所以将Paxos算法中的Proposer、Acceptor、Leaner等角色分开介绍。但实际情况是,分布式系统中的每个节点都同时具备这几种角色,例如工作在Server 1节点的Acceptor角色能够实时知晓这个节点上Proposer角色的工作状态;Proposer角色也实时知晓同一节点上Leaner角色对提案最终结果的学习状态。这也是为什么我们讨论Paxos算法时经常会将属于Leaner角色的部分公论讨论到Proposer角色中去的原因。

回到Multi-Paxos算法的选主过程来,这个过程就是一个完整的Basic-Paxos过程,这里就不再赘述了。主要需要说明的是保持方式,其它Proposer角色可以从Basic-Paxos过程了解到“已经选出一个Proposer Leader”这个事实,但是并不能知晓这个Proposer Leader仍然是“工作正常的”。所以一旦Proposer Leader被选出,它就会向所有其它Proposer发出一个“Start Working”指令,明确通知其它Proposer节点,自己开始正常工作了,并且在后续的工作中定时更新自己在各个Proposer节点上的租约时间(本文不介绍租约协议,有兴趣的读者可以自行查找该协议的介绍,这里可以简单理解为心跳)。

2-4-2. Multi-Paxos:正式工作过程

为了说清楚Multi-Paxos对多个业务提案的赋值投票过程,我们举一个连续进行日志写入的业务场景,这里每一条日志都有一个唯一且全局递增的log id——实际上一条日志就对应了一个提案即一个Paxos instance,提案内容就是“针对log id为X的日志,完成日志内容的持久化记录”。

这里有读者会问,这个唯一的log id是怎么被确认的呢?这个问题在Multi-Paxos中就很简单了,因为整个系统中只有一个Proposer Leader能进行提案的投票申请和赋值操作,所以这个来自于外部客户端的log id完全可以由Proposer Leader赋予一个唯一的Paxos instance id,并作为内部存取顺序的依据。如果这个log id来源于分布式系统内部那么这个问题就更简单了,直接由Proposer Leader赋予一个唯一且递增的log id即可。而后者的场景占大多数——试想一下这个基于Multi-Paxos算法工作的系统本来就是一个分布式存储系统……

                

由于Proposer Leader对业务数据的一致性处理跳过了Basic-Paxos算的第一个Prapare阶段,所以对于一条日志Log id,Proposer Leader会直接携带log id(可能还有Paxos instance id)和日志内容,向所有节点发起提案赋值操作。每一个Acceptor在收到赋值请求后,会首先在本地持久化这个日志内容。如果持久化成功,则对这个提案的赋值结果为“完成”,其它任何情况都是“未完成”。

Multi-Paxos的正式工作可以保证一批要进行投票的操作,其序号一定由小到大排列(对于这个日志业务的实例,就是日志的log id一定全局唯一且单调递增)。而且由于只存在一个Proposer Leader能够处理发起投票和赋值操作,所以也避免了操作竞争的问题,这直接导致每一个Paxos instance id都省去了Basic-Paxos的第一阶段,减少了花费在网络上的耗时。但是Multi-Paxos并非没有缺点,最显著的一个问题就是,由于Multi-Paxos中只有一个Proposer在工作,那么当这个Proposer由于任何原因无法在继续工作时怎么办?Multi-Paxos的解决办法是从剩下的Proposer,再重新选择一个Proposer Leader,让整个Multi-Paxos系统继续工作。

话虽简单,但实现起来却并不容易,这也是Multi-Paxos算法的难点所在。为什么呢?我们首先应该思考一下当原有的Proposer Leader宕机时,整个系统的可能处在什么样的状态,如下图所示:

                
(宕机前夜)

  • 首先由于Paxos算法的多数派原理,所以除了当前Proposer Leader(后文称为old leader)所在Server节点上的Log数据是完整的以外,整个系统不能保证任何一个其它Proposer所在Server节点上的Log数据是完整的。既然Log数据不完整,那么怎么做数据恢复呢?

  • 分布式系统的一般要力争 7 × 24小时不间断工作,也就是说即使整个系统连续工作1周时间,积累的LOG数据也是非常庞大的,10GB、100GB、1TB、10TB甚至更庞大的日志数据规模都有可能。

  • 当old leader宕机时,唯一可以确认的一件事情是:当前通过Multi-Paxos第二阶段操作,并持久化存储在Proposer Leader上的每一条日志数据,在整个Paxos算法算法系统中,至少存在N/2 + 1的多数派份额。在Proposer Leader宕机后,多数派需要达到的数量变成了(N - 1) / 2 + 1.

  • 最后,如上图所示Proposer Leader正在对log id为X7的数据进行Multi-Paxos第二阶段操作,且还没有拿到多数派确认。那么这样的数据在另一个Proposer Leader拿到控制权后,是恢复呢还是不恢复呢?

所谓重新恢复一个Proposer Leader(后文件称new leader),要达到的目的可不是简单的重新选举一个就行了。而是要让这个new leader尽量恢复到old leader宕机时的工作状态,当Client查询第X条日志信息时,new leader能够给出正确的值,而不是向Client回复not found或者错误的值。所以要保证new leader能够接替old leader继续工作,在old leader存活期和new leader恢复阶段,它们至少要做以下工作:确认赋值、确保old leader真正下线、重选leader和恢复数据。

2-4-3. Multi-Paxos:确认赋值过程(confirm)

确认赋值过程的目的主要是帮助Paxos系统在恢复阶段尽可能少做无用功,其做法是按照一定规则周期性的批量的发送Proposer Leader上log id的confirm信息。

             

如上图所示,所谓log id的confirm信息是指满足这样条件的log id信息,这个log id通过进行Multi-Paxos的第二阶段操作后,已经得到了大多数Acceptor的赋值确认,并且已经在Proposer Leader完成了持久化存储的log id。confirm信息还可以是乱序,甚至还可以是跳跃的。而且Proposer Leader发送一批og id的confirm信息后,也无需等待Proposer都回发确认信息,甚至可以设定Proposer成本身就不回发确认信息。

Proposer在收到这些confirm信息后,会检查(会可以不检查)log id confirm信息所对应的log id是否存在,如果存在则将这个log id confirm信息进行持久化保存;如果不存在也可以进行log id confirm信息的持久化保存(只是那样做没有意义),所以才会有“可以不预先检查”的说明。为什么要这样做,我们在后续4-4-5介绍数据恢复的小节会进行说明。

2-4-4. Multi-Paxos:确认下线

请考虑这样一个问题:当分布式系统中A节点收不到B节点“工作正常”的信息时,是A节点下线了,还是B节点下线了?答案是根本说不清楚,可能B节点还在正常工作,A节点收不到信息只是由于暂时的网络抖动;由于分布式系统存在分区隔离的问题,虽然A节点收不到B节点“工作正常”的信息,但是整个系统中还可能有大部分节点能够收到这样的信息,所以都不会认为B节点下线了;最后,也可能B节点真的已经下线。

在分布式系统中,对于节点X(既是本文一直在讨论的Proposer Leader节点)是否下线了这个问题很明显不是一个节点能够决定的,而最好是由多数派节点投票确认。在节点X被认定下线的时候,整个分布式系统至少需要保证两件事:

  • 多数派认为节点X下线了:这个原因已经在上一段落进行了描述,这里就不再赘述了。

  • 原来的节点X真的下线了:这是因为如果原有的节点X没有下线,在新的节点X1接替它工作后,就可能出现两个节点都在完成相同的工作职责,而它们接受到的传入数据又可能不完全一致,最终影响数据的一致性。要解决这个问题,只需要在各个节点实现租约协议。当节点X无法向多数派节点续租时即认为自己下线,这时即使自己工作是正常的,也会强制自己停止服务。

                                       

如上图所示,当节点Server N上关于Old Proposer Leader的租约信息过期后,Server N会发起一轮新的议题,议题内容为“Old Proposer Leader已经下线”。分布式系统中的其它节点在收到这个议题后,会检查自己和Old Proposer Leader的租约是否过期,如果没有过期,则可以在Basic-Paxos算法的第一阶段拒绝授权或者在Basic-Paxos算法的第二阶段拒绝赋值

当节点Server N关于“Old Proposer Leader已经下线”的议题通过,确确实实知晓不是由于自身的原因误认为“Old Proposer Leader已经下线”后,才会开始重新选主过程。这里有一个细节,当Server N的议题“Old Proposer Leader已经下线”通过后,Old Proposer Leader也会真实离线。这是因为在Old Proposer Leader上和多数派节点的租约都已经过期,即使Old Proposer Leader工作是正常的,也会强制自己停止服务。

需要注意的是,确认下线这个过程实际上并不是Multi-Paxos算法所独有的,而是所有分布式系统需要具备的功能。确认下线这个过程也并不是一定要使用Basic-Paxos算法进行,其原则是保证多数派确认下线和Old Proposer Leader确实下线这个事实。由于Multi-Paxos算法的特点,在正式工作时只能有一个Proposer Leader,所以在进行Proposer Leader重选前就必须确保原有的Proposer Leader真实下线。

2-4-5. Multi-Paxos:重选和数据恢复

重新选主的过程实际上和2-4-1小节描述的选主过程是一致的,甚至为了简化工作过程也可以将重新选主的提议和确认下线的提议合并在一起。重选步骤的关键并不在于如何选主,而在于如何恢复数据——因为Multi-Paxos的工作过程,我们不能保证任何一个Server节点上拥有完整的数据,所以一个New Proposer Leader在开始工作前就需要对数据完整性进行检查,对缺失的数据进行恢复,这样才能保证客户端在查询第Y条日志数据时,New Proposer Leader会返回正确的查询结果,而不是返回Not Found!下图展示了一种New Proposer Leader被选出后,整个Paxos集群所处理的数据状态

              

好消息是在Old Proposer Leader工作阶段,已经向潜在的成为新主的多个Proposer节点写入了不少的信息。例如,在Old Proposer Leader正式工作开始之前,会向其它Proposer节点写入一个start work的位置,记录前者开始工作时所用的log id的开始编号位置,经历了多少次Proposer Leader选主确认过程,就会有多个start work位置。例如一个月前由于Old Proposer Leader宕机,进行了一次选主,上周5由于Old Proposer Leader宕机又进行了一次选主。期间工作的各个Proposer上就会有两个start work位置。新加入的节点和恢复正常工作的历史节点,也应在加入/重新加入分布式环境时同步这些start work位置。

在Proposer Leader工作工程中,直接使用Basic-Paxos算法的第二阶段进行log id和内容的赋值操作,所以每一条操作成功的log内容都会记录在多个Proposer节点上,且这些节点的数量是多数派。最后,Proposer Leader工作过程中还会批量发送confirm信息,告知所有Proposer节点,哪些log id的内容已经得到了多数派确认。

所以在New Proposer Leader被选出,且开始进行数据恢复前。这个New Proposer Leader至少知道三类信息:一个或者多个Old Proposer Leader的strat working的位置、目前还在线的节点以及在Old Proposer Leader上已经得到多数派确认的若干log id信息。但是New Proposer Leader并不知道全局最大的log id,虽然在New Proposer Leader节点上会有一个max log id,但它并不知道后者是不是就是全局最大的log id。原因是之前Old Proposer Leader下线时,正在等待多数派确认的那个log id还没有得到多数派赋值确认又或者虽然多数派确认了但Old Proposer Leader本身还没有拿到响应信息,而New Proposer Leader自己并不清楚自己是否属于多数派。

数据恢复的原理说起来很简单,既是从数据恢复的开始点,到数据恢复的结束点,将本节点缺失的数据补全。那么问题来了:数据恢复的起点在哪?数据恢复的结束点在哪?哪些数据需要补全?数据来源在哪里?又应该如何进行补全?

  • 数据恢复的开始点很好确定,即是还没有开始恢复的第一个start work位置所对应的log id。

  • 数据恢复的结束点稍微麻烦一点,需要发起一轮Paxos投票来确认由多数派认可的那么全局最大的log id。不过,由于这时整个系统中已经确认了一个唯一的Proposer Leader,所以不存在投票冲突的问题,可以直接进行Basic-Poxos的第二阶段。

  • 从数据恢复的起点到终点,每一条log数据都需要进行一次完整的Basic-Poxos操作用以确保恢复的数据是历史上以形成多数派的正确数据,最终目的是为了保证数据最终一致性。但是之前已存在于New Proposer Leader上,且被标记为confirm的log数据则无需进行这样的操作了,因为Old c已经在之前确定了这些数据是正确的。这样就大量减少了需要恢复的数据规模,加快了数据恢复过程。

  • 注意,如果在数据恢复过程中,某一条Log id无法形成多数派决议,则这条数据就是在Old Proposer Leader中还没有被确认的数据,这样的数据不能进行恢复。即使在New Proposer Leader上有这条记录,如果没有形成多数派决议也不能使用,否则就会出现一些的错误。请看如下所示的极端例子:

                  

                    

如上图所示,Server 2在宕机前正在对log7 、log8 、log9 进行Paxos,并且自身已经完成赋值过程(但是其它节点都还没有完成赋值过程,没有形成多数派)。这时Server 2由于某些原因下线了。Server 3通过选主过程拿到了Proposer Leader,但是它能获取到的最大log id为log6(因为现在存活的各个节点上最大的log id都是log6嘛),所以自己start working的log id 位置就是log7。就在这个时候,Server 2节点又恢复了工作,且技术人员人工命令从新授予Server 2 节点Proposer Leader身份。

这时在其上原有的log7、log8、log9虽然有值,但是也不能再使用了,需要重新进行恢复。那么Server 2依据什么来判断存储在自身节点的log7、log8、log9的值已经失效必须进行恢复的呢?那就是在start working的时候,记录了一个创建时间T1,在随后的工作中,节点每写入一条日志信息就要写入同时写入这个T1。当进行数据恢复时,如果发现某一条数据所记录的T1和其最近的start working标记点所记录的T1不一致,就认为这是一条记录时效的数据,需要进行恢复。

按照如上图所示的极端示例中,log7的值应该恢复成11,而log8、log9的值应该记为一个空值或者是恢复成一个NOP标记。

3. 其它说明

Paxos算法并不是所有最终一致性算法,Multi-Paxos也并不是唯一一种Paxos算法的变种,例如还有Fast-Paxos、Egalitarian-Paxos。Fast-Paxos进一步强调了Leaner角色在整个Paxos算法中的作用,进一步优化了多个Proposer的协作工作过程。在后续的文章中会适时回过头来介绍Fast-Paxos,这里确实不能在耽误时间向后推进了(本来计划3月下旬开始分布式文件系统Ceph的使用和原理介绍)

最终一致性算法除了Paxos外,还有ZAB算法、RAFT算法、GOSSIP算法和PacificA算法。其中ZAB算法应用在一个非常著名的软件Zookeeper中,而PacificA的思想则被用在Apache Kafka进行Partition Leader的选举过程中。

猜你喜欢

转载自blog.csdn.net/shuningzhang/article/details/90509993