Paxos算法原理

1.从ACID到CAP

我们知道传统集中式系统中实现ACID是很简单的,在分布式环境中,涉及到不同的节点,节点内的ACID可以控制,那么节点间的ACID如何控制呢?构建一个可用性和一致性的分布系统成为难题,于是出现了CAP和BASE这样的理论。

CAP定理:

CAP告诉我们一个分布式系统不可能同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。最多只能同时满足其中两项。

放弃CAP定理 说明
放弃P 放弃分区容错性意味着你把所有的数据都放在同一个节点上,这可能会导致出错时系统不可用。同时也放弃了系统的可扩展性。
放弃A 放弃可用性带来的影响就更大了,出现故障系统直接停止提供服务。
放弃C 放弃一致性指的是需要系统保持强一致还是保持最终一致。

为解决分布式一致性问题,涌现出来一大批经典的一致性算法和协议,其中最著名的是二阶段提交协议,三阶段提交协议和Paxos算法。

2PC:

two-Phase-Commit。通常二阶段提交协议也被认为是一种一致性协议,用来保证分布式系统的数据一致性。目前绝大部分关系型数据库都是采用二阶段提交来完成分布式事务处理的。

协议说明:

  1. 提交事务请求;
  2. 执行事务提交;

优缺点:

  1. 二阶段提交协议的优点:原理简单,实现方便。
  2. 二阶段提交协议的缺点:同步阻塞、单点问题、脑裂、太过保守。

3PC:

将二阶段提交协议的“提交事务请求”过程一分为二,形成了由CanCommit,PreCommit和doCommit三个阶段组成的事务处理协议,协议设计如下图:

  1. 阶段1,canCommit
    1. 事务查询;
    2. 各参与者向协调者反馈事务询问的响应。
  2. 阶段2,preCommit
    1. 假如协调者从所有的参与者获得的反馈都是Yes 响应,那么就会执行事务预提交。
    2. 假如任何一个参与者向协调者反馈了No 响应,或者在等待超时之后,协调者尚无能接收到所有参与者的反馈响应,那么就会中断事务。
  3. 阶段3,doCommit
    1. 执行提交;
    2. 进入这一阶段,假设协调者处于正常工作状态,井且有任意一个参与者向协调者反馈了No 响应,或者在等待超时之后,协调者尚无告接收到所有参与者的反馈响应,那么就会中断事务。

优缺点:

三阶段提交协议的优点:相较于二阶段提交协议,三阶段提交协议最大的优点就是降低了参与者的阻塞范围,并且能够在出现单点故障后继续达成一致。

三阶段提交协议的缺点:三阶段提交协议在去除阻塞的同时也引入了新的问题,那就是在参与者接收到preCommit消息后,如果网络出现分区,此时协调者所在的节点和参与者无能进行正常的网络通信,在这种情况下, 该参与者依然会进行事务的提交,这必然出现数据的不一致性。

2. Paxos算法

Paxos算法的核心是一个一致性算法,也就是论文The Part-time Parliament中提到的”synid”算法,我们将从对一致性问题的描述开始来讲解该算法需要解决的实际需求。

问题描述:

假设有一组可以提出提案的进程集合,那么对于一个一致性算越来说需要保证以下几点:

  • 在这些被提出的提案中,只有一个会被选定
  • 如果没有提案被提出,那么就不会有被选定的提案
  • 当一个提案被迫定后,进程应该可以获取被选定的提案信息

对于一致性来说,安全性( Safety )的需求如下:

  • 只有被提出的提案才能被选定( Chosen )
  • 只能有一个值被选定。
  • 如果某个进程认为某个提案被选定了,那么这个提案必须是真的被选定的那个

从整体上来说,Paxos算法的目标就是要保证最终有一个提案会被选定, 当提案被选定后,进程最终也能获取到被选定的提案。

要选定一个唯一提案的最简单方式莫过于只允许一个Accpetor存在,这样的话,Proposer只能发送提案给该Accpetor, Acceptor 会选择接收到的第一个提案作为被选定的提案。这种解决方式尽管实现起来非常简单,但是却很难让人满意,因为一旦这个Accpetor出现问题,那么整个系统就无能工作了。

因此,应该寻找一种更好的解决方式,例如可以使用多个Accpetor 来避免Accpetor 的单点问题。现在我们就来看看在存在多个Acceptor 的情况下如何进行提案的选取:

Proposer向一个Acceptor集合发送提案,同样,集合中的每个Acceptor 都可能会批准(Accept)该提案,当有足够多的Acceptor批准这个提案的时候,我们就可以认为该提案被选定了。那么,什么是足够多呢?我们假定足够多的Acceptor是整个Acceptor集合的一个子集,井且让这个集合大得可以包含Acceptor集合中的大多数成员,因为任意两个包含大多数Acceptor的子集至少有一个公共成员。另外我们再规定,每一个Acceptor最多只能批准一个提案,那么就能保证只有一个提案被选定了。

在没有失败和消息丢失的情况下,如果我们希望即使在只有一个提案被提出的情况下,仍然可以选出一个提案,这就暗示了如下的需求:

条件1: 一个Acceptor 必须批准它收到的第一个提案。

上面这个需求就引出了另外一个问题:如果有多个提案被不同的Proposer 同时提出,这可能会导致虽然每个Acceptor都批准了它收到的第一个提案,但是没有一个提案是由多数人都批准的。

不同的Proposer 分别提出每个提案。

另外,即使只有两个提案被提出,如果每个提案都被差不多一半的
Acceptor 批准了,此时即使只有一个Acceptor出错,都有可能导致无法确定该选定哪个提案。

任意一个Acceptor 出现问题。

因此在需求1的基础上,再加上一个提案被选定需要由半数以上的Acceptor批准的需求暗示着一个Acceptor必须能够批准不止一个提案。

在这里,我们使用一个全局的编号(这种全局唯一编号的生成并不是Paxos算法需要关注的地方,就算法本身而言,其假设当前已经具备这样的外部组件能够生成一个全局唯一的编号)来唯一标识每一个被Acceptor 批准的提案,当一个具有某Value值的提案被半数以上的Acceptor批准后,我们就认为该Value被选定了,此时我们也认为该提案被选定了。需要注意的是,此处讲到的提案已经和Value不是同一个概念了,提案变成了一个由编号和Value组成的组合体,因此我们以“[编号,Value]"来表示一个提案。

根据上面讲到的内容,我们虽然允许多个提案被选定,但同时必须要保证所有被选定的提案都具有相同的Value值这是一个关于提案Value的约定, 结合提案的编号,该约定可以定义如下:

条件2:

如果编号为Mo, Value值为V0的提案(即[Mo, Vo])被选定了,那么所有比编号Mo更高的,且被选定的提案,其Value值必须也是Vo。

因为提案的编号是全序的,条件2就保证了只有一个Value值被选定这一关键安全性属性。同时,一个提案要被选定,其首先必须被至少一个Acceptor 批准,因此我们可以通过满足如下条件来满足条件2:

条件2.1:

如果编号为Mo,Value值为Vo的提案(即[Mo, Vo])被选定了,那么所有比编号Mo更高的,且被Acceptor批准的提案,其Valu值必须也是Vo。

因此,我们在条件1的前提下来发起提案,但是因为通信是异步的,一个提案可能会在某个Acceptor还未收到任何提案时就被选定了。

在Acceptorl没有收到任何提案的情况下,其他4个Acceptor已经批准
了来自Proposer2的提案[Mo,V1],而此时,Proposerl产生了一个具有其他Value值的、编号更高的提案[M1,V2],并发送给了Acceptor1。根据条件1就需要Acceptorl批准该提案,但是这与条件2矛盾,因此如果要同时满足条件1和1,需要对条件2进行如下强化:

条件2.2:

如果一个提案[Mo,Vo]被选定后,那么之后任何Proposer产生的编号是高的提案,其Value值都为Vo。

因为一个提案必须在被Proposer提出后才能被Acceptor批准,因此2.2 包含了2.1,进而包含了2。于是,接下去的重点就是论证2.2成立即可:

假设某个提案[Mo,Vo]已经被选定了,证明任何编号Mn> Mo的提案,其Value值都是Vo。

2.1 证明

我们可以将上述结论转化为如下:

假设编号在Mo到Mn-1之间的提案,其Value值都是Vo,证明编号为Mn的提案的Value值也为Vo。

因为编号为Mo的提案已经被选定了,这就意味着肯定存在一个由半数以上的Acceptor组成的集合c, c中的每个Acceptor都批准了该提案。再结合归纳假设,”编号为Mo的提案被选定”意味着:

C中的每个Acceptor都批准了一个编号在Mo到Mn-范围内的提案,并且每个编号在Mo到Mn-1范围内的被Acceptor批准的提案,其Value值都为Vo。

因为任何包含半数以上Acceptor的集合S都至少包含C中的一个成员,因此我们可以认为如果保持了下面条件2.3的不变性,那么编号为Mn的提案的Value也为Vo。

条件2.3:

对于任意的Mn和Vn,如果提[Mn,Vn]被提出,那么肯定存在一个由半数以上的Acceptor组成的集合S ,满足以下两个条件中的任意一个。

  • S中不存在任何批准过编号小于Mn的提案的Acceptor。
  • 选取S中所有Acceptor批准的编号小于Mn的提案,其中编号最大的那个提案其Value值是Vn。

至此,只需要通过保持条件2.3我们就能够满足条件2.2了。

实际上2.3规定了每个Proposer如何产生一个提案:对于产生的每个提案[Mn,Vn],需要满足如下条件:

存在一个由超过半数的Acceptor 组成的集合S :

  • 要么S中没有Acceptor批准过编号小于Mn的任何提案。
  • 要么S中的所有Acceptor批准的所有编号小于Mn的提案中,编号最大的那个提案的Value值为Vn。

当每个Proposer都按照这个规则来产生提案时,就可以保证满足2.2了。我们继续来证明2.3。

首先假设提案[Mo,Vo]被选定了,设比该提案编号大的提案为[Mn,Vn],我们需要证明的就是在2.3的前提下,对于所有的[Mn,Vn],存在Vn=Vo。

  1. 当Mn= Mo+1时,如果有这样一个编号为Mn的提案,首先我们知道[Mo,Vo]已经被选定了,那么就一定存在一个Acceptor的子集S,且S中的Acceptor已经批准了小于M的提案,于是,Vn只能是多数集S中编号小于M但为最大编号的那个提案的值。而此时因为Mn= Mo+1,因此理论上编号小于Mn但为最大编号的那个提案肯定是[Mo,Vo]同时由于S和通过[Mo,Vo]的Acceptor集合都是多数集,也就是说二者肯定有交集—这样Proposer在确定Vn取值的时候,就一定会选择Vo。

    值得注意的一点是,Paxos算法的证明过程使用的是第二数学归纳法,上面实际上就是数学归纳陆的第一步,验证了某个初始值成立。接下去,就需要假设编号在Mo + 1到Mn-1区间内时成立,并在此基础上推导出当编号为Mn时也成立。

  2. 根据假设,编号在Mo+1到Mn-1区间内的所有提案的Value值为Vo,需要证明的是编号为Mn的提案的Value值也为Vo。根据2.3,首先同样一定存在一个Acceptor的子集S,且S中的Acceptor已经批准了小子Mn的提案,那么编号为Mn的提案的Value值只能是这个多数集S中编号小于Mn但为最大编号的那个提案的值。如果这个最大编号落在Mo+1到Mn-1区间内,那么Value值肯定是Vo,如果不落在Mo+1到Mn-1区间内,那么它的编号不可能比Mo再小了,肯定就是Mo,因为S也肯定会与批准[Mo,Vo]这个提案的Acceptor集合S有交集,而如果编号是Mo,那么它的Value值也是Vo,由此得证。

proposer生成提案

对于一个Proposer来说,获取那些已经被通过的提案远比预恻未来可能会被通过的提案来得简单。因此,Proposer 在产生一个编号为Mn的提案时,必须要知道当前某一个将要或已经被半数以上Acceptor批准的编号小于Mn但为最大编号的提案。并且,Proposer会要求所有的Acceptor都不
要再批准任何编号小于Mn的提案—这就引出了如下的提案生成算法。

  1. Proposer选择一个新的提案编号Mn,然后向某个Acceptor集合的成员发送请求,要求该集合中的Acceptor做出如下回应:

    1. 向Proposer承诺,保证不再批准任何编号小于Mn 的提案。
    2. 如果Acceptor已经批准过任何提案,那么其就向Proposer反馈当前该Acceptor已经批准的编号小于Mn但为最大编号的那个提案的值。
    3. 我们将该请求称为编号为Mn的提案的Prepare请求。
  2. 如果Proposer收到了来自半数以上的Acceptor的晌应结果,那么它就可以产生编号为Mn,Value值为Vn的提案,这里的Vn是所有响应中编号最大的提案的Value值。当然还存在另一种情况,就是半数以上的Acceptor 都没有批准过任何提案,即响应中不包含任何的提案,那么此时Vn值就可以由Proposer任意选择。

Acceptor批准提案:

一个Acceptor可能会收到来自Proposer的两种请求,分别是Prepare请求和Accept请求,对这两类请求做出响应的条件分别如下:

  1. Prepare请求:Acceptor可以在任何时候响应一个Prepare请求。
  2. Accept请求:在不违背Accept现有承诺的前提下,可以任意响应Accept请求。

因此对Acceptor逻辑处理的约束条件大体如下:

一个Acceptor只要尚未响应过任何编号大于Mn的Prepare请求,那么它就可以接受这个编号为Mn的提案。

2.2 算法优化

在上面的内容中,我们分别从Proposer和Acceptor对提案的生成和批准两方面来讲解了Paxos算法在提案选定过程中的算也细节,同时也在提案的编号全局唯一的前提下,获得了一个满足安全性需求的提案选定算法,接下来我们再对这个初步算法做一个小优化,尽可能地忽略Prepare请求:

假设一个Acceptor收到了一个编号为Mn的Prepare请求,但此时该Acceptor已经对编号大于Mn的Prepare请求做出了响应,因此它肯定不会再批准任何新的编号为Mn的提案,那么很显然,Acceptor就没有必要对这个Prepare请求做出响应,于是Acceptor可以选择忽略这样的Prepare 请求。同时,Acceptor也可以忽略掉那些它已经批准过的提案的Prepare 请求。

通过这个优化,每个Acceptor只需要记住它已经批准的提案的最大编号以及它已经做出Prepare请求响应的提案的最大编号,以便在出现故障或节点重启的情况下,也能保证条件2.3的不变性。而对干Proposer来说,只要它可以保证不会产生具有相同编号的提案,那么就可以丢弃任意的提案以及它所有的运行时状态信息。

根据我们的优化,结合Proposer和Acceptor对提案的处理逻辑,就可以得到如下类似于两阶段提交的算能执行过程:

阶段1:

  1. Proposer选择一个提案编号Mn,然后向Acceptor的某个超过半数的子集成员发送编号为Mn的Prepare请求。
  2. 如果一个Acceptor收到一个编号为Mn的Prepare请求,且编号Mn大于该Acceptor已经响应的所有Prepare请求的编号,那么它就会将它已经批准过的最大编号的提案作为响应反馈给Proposer,同时该Acceptor会承诺不会再批准任何编号小于Mn的提案。

举个例子来说,假定一个Acceptor已经响应过的所有Prepare请求对应的提案编号分别为1,2, …,5 和7,那么该Acceptor在接收到一个编号为8的Prepare请求后,就会将编号为7的提案作为响应反馈给Proposer。

阶段2:

  1. 如果Proposer收到来自半数以上的Acceptor对于其发出的编号为Mn 的Prepare请求的晌应,那么它就会发送一个针对[Mn,Vn]提案的Accept请求给Acceptor。注意,Vn的值就是收到的响应中编号最大的提案的值,如果响应中不包含任何提案,那么它就是任意值。
  2. 如果Acceptor收到这个针对[Mn,Vn]提案的Accept请求,只要该Acceptor尚未对编号大于Mn的Prepare请求做出响应,它就可以通过这个提案。

当然,在实际运行过程中,每一个Proposer都有可能会产生多个提案,但只要每个Proposer都遵循如上所述的算陆运行,就一定能够保证算怯执行的正确性。值得一提的是,每个Proposer都可以在任意时刻丢弃一个提案,哪怕针对该提案的请求和响应在提案被丢弃后会到达,但根据Paxos 算法的一系列规约,依然可以保证其在提案选定上的正确性。事实上,如果某个Proposer已经在试图生成编号更大的提案,那么丢弃一些旧的提案未尝不是一个好的选择。因此,如果一个Acceptor因为已经收到过更大编号的Prepare请求而忽略某个编号更小的Prepare或者Accept请求,那么它也应当通知其对应的Proposer,以便该Proposer也能够将该提案进行丢弃—这和上面“算法优化”部分中提到的提案丢弃是一致的。

Learner获取提案:

Learner获取提案大致有以下几种方案:

方案一:

Learner获取一个已经被选定的提案的前提是,该提案已经被半数以上的Acceptor批准。因此,最简单的做怯就是一旦Acceptor批准了一个提案,就将该提案发送给所有的Learner。

很显然,这种做法虽然可以让Learner尽快地获取被选定的提案,但是却需要让每个Acceptor与所有的Learner逐个进行一次通信,通信的次数至少为二者个数的乘积。

方案二:

另一种可行的方案是,我们可以让所有的Acceptor将它们对提案的批准情况,统一发送给一个特定的Learner(下文中我们将这样的Learner称为”主Learner”),在不考虑拜占庭将军问题的前提下,我们假定Learner之间可以通过消息通信来互相感知提案的选定情况。基于这样的前提,当主Learner被通知一个提案已经被选定时,它会负责通知其他的Learner。

在这种方案中,Acceptor首先会将得到批准的提案发送给圭Learner,再由其同步给其他Learner,因此较方案一而言,方案二虽然需要多一个步骤才能将提案通知到所有的Learner,但其通信次数却大大减少了,通常只是Acceptor和Learner的个数总和。但同时,该方案引入了一个新的不稳定因素:主Learner随时可能出现故障。

方案三:

在讲解方案二的时候,我们提到,方案二最大的问题在于主Learner存在单点问题,即主Learner随时可能出现故障。因此,对方案二进行改进,可以将主Learner的范围扩大,即Acceptor可以将批准的提案发送给一个特定的Learner集合,该集合中的每个Learner都可以在一个提案被选定后通知所有其他的Learner。这个Learner集合中的Learner个数越多,可靠性就越好,但同时网络通信的复杂度也就越高。

2.3 通过选举出主Proposer保证算法的活性

根据前面的内容讲解,我们已经基本上了解了Paxos算法的核心逻辑,下面我们再来看看Paxos算法在实际运作过程中的一些细节。假设存在这样一种极端情况,有两个Proposer依次提出了一系列编号递增的议案,但是最终都无法被选定,具体流程如下:

Proposer P1提出了一个编号为M1的提案,并完成了上述阶段一的流程。但与此同时,另外一个Proposer P2提出了一个编号为M2(M2>M1)的提案,同样也完成了阶段一的流程,于是Acceptor已经承诺不再批准编号小于M2 的提案了。因此,当P1进入阶段二的时候,其发出的Acceptor请求将被Acceptor忽略,于是P1再次进入阶段一并提出了一个编号为M3(M3 > M2)的提案,而这又导致P2在第二阶段的Accept请求被忽略,以此类格,提案的选定过程将陷入死循环。

为了保证Paxos算法流程的可持续性,以避免陷入上述提到的“死循环”,就必须选择一个主Proposer,并规定只有主Proposer才能提出议案。这样一来,只要主Proposer和过半的Acceptor能够正常进行网络通信,那么但凡主Proposer提出一个编号更高的提案,该提案终将会被批准。当然,如果Proposer发现当前算法流程中已经有一个编号更大的提案被提出或正在接受批准,那么它会丢弃当前这个编号较小的提案,并最终能够选出一个编号足够大的提案。因此,如果系统中有足够多的组件(包括Proposer,Acceptor和其他网络通信组件)能够正常工作,那么通过选择一个主Proposer,整套Paxos算住流程就能够保持活性。

posted @ 2018-04-22 09:52  rickiyang  阅读(1333)  评论(0编辑  收藏  举报