分布式系统开发实战:Paxos算法

Paxos算法

Leslie Lamport在The Part-Time Parliament(1990年提交,1998年发表)中提出了Paxos——是一种基于消息传递的一致性算法。人们觉得这篇论文太难理解了,所以这篇论文就被“不幸”地忽略了。直到ButlerLampson在How to Build a Highly Availability System usingConsensus(1996年发表)中重新提到这个算法,这篇论文对如何构建容错系统和Paxos进行了很好的介绍。后来Lamport又发表了Paxos MadeSimple(2001年发表),对相关的概念进行了简化。直到2006年Google的关于Chubby的论文发表,由于Chubby锁服务使用Paxos作为Chubbycell中的一致性算法,因此使得Paxos的人气从此“一路狂飙”。

业界采用Paxos算法的实际案例有Chubby、MegaStore、Spanner、DynamoDB、S3、ZooKeeper等。

Paxos一般包含以下术语。

  • Proposer:提案者,它可以提出一个提案。
  • Proposal:提案,由Proposer提出。一个提案由一个编号及value形成的对组成,编号是为了防止混淆,保证提案的可区分性,value即代表了提案本身的内容。
  • Acceptor:提案的受理者,有权决定它本身是否接受该提案。
  • Choose:被选定的提案,当有半数以上Acceptor接受该提案时,就认为该提案被选定了。
  • Learner:需要知道被选定的提案信息的参与者。

问题描述

假设有一组可以提出提案的进程集合。一个一致性算法需要保证以下3点。

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

对于Paxos,安全性原则如下。

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

存活原则如下。

  • ·最终会批准某个提案的value。
  • ·一个value被批准了,其他服务器最终会学习到这个value。

安全性和存活在分布式算法中是两个最重要的属性,简单来说安全性就是指那些需要保证永远都不会发生的事情,存活是指那些最终一定会发生的事情。整体上来说,一致性算法的目标就是要保证最终有一个提案会被选定,当提案被选定后,进程最终也能获取到被选定的提案。

在该Paxos一致性算法中,有3种参与角色,我们用Proposer、Acceptor和Learner来表示。在具体的实现中,一个进程可能充当不止一种角色,在这里我们并不关心进程如何映射到各种角色。

使用Paxos算法的前提是,假设不同参与者之间可以通过发送消息来通信,并使用普通的非拜占庭模式的异步模型,即每个参与者以任意的速度执行,可能会出错而停止,也可能会重启。当一个提案被选定后,所有的参与者都有可能失败或重启,因此除非那些失败或重启的参与者可以记录某些信息,否则是不可能存在一个解的。消息在传输中可能花费任意的时间,可能会重复、丢失,但是不会被损坏,即其内容不会被篡改,不会发生拜占庭式的问题。

1982年,Leslie Lamport与另两人共同发表的论文描述了一种计算机容错理论。为了形象地表达其中的问题,Lamport设想出了一种场景:某国有许多支军队,军队的将军们必须制订一个统一的行动计划——进攻或者撤退。将军们在地理上是分隔开来的,只能靠通讯员进行通信,并且将军中存在叛徒。叛徒可以任意篡改消息,欺骗某些将军进攻或撤退。理论研究显示,在一个3N+1的系统中,只有叛徒数目小于等于N的情况下,才有可能设计出一种协议,使得不管叛徒怎样作梗也能达成一致。由于大多数系统在同一局域网中,消息被篡改的情况很罕见;因硬件和网络造成的消息不完整,只需简单的校验,丢弃不完整的消息。因此Paxos算法可以假设所有消息都是完整的,没有被篡改。

提案的选定

那么如何来选定提案?分为以下3种场景。

1.单个Acceptor的提案选定

选定提案的最简单方式就是只允许一个Acceptor存在。Proposer发送提案给Acceptor,Acceptor会选择它接收到的第一个提案作为被选定的提案。尽管简单,但是这个解决方式却很难让人满意,因为如果Acceptor出错,那么整个系统就无法工作了,也就违反了存活的原则。

因此,需要用多个Acceptor来避免一个Acceptor时的单点问题。

2.多个Acceptor的提案选定

现在,Proposer向一个Acceptor集合发送提案,某个Acceptor可能会通过这个提案。当有足够多的Acceptor通过它时,我们就可以认为这个提案被选定了,也就是少数服从多数。我们再规定一个Acceptor最多只能通过一个提案,那么就能保证只有一个提案被选定。

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

P1:一个Acceptor必须通过它收到的第一个提案。

上述需求引出了另外一个问题。如果有多个提案被不同的Proposer同时提出,这可能会导致虽然每个Acceptor都通过了一个提案,但是没有一个提案是由多数人都通过的。即使是只有两个提案,如果每个都被差不多一半的Acceptor通过了,此时即使只有一个Acceptor出错,都可能使得无法确定该选定哪个提案。比如有5个Acceptor,其中2个通过了提案a,另外3个通过了提案b,此时如果通过b的3个中有一个出错了,那么a、b的通过者都变成了2个,这就不清楚该如何决定了。

3.选定多个提案之一

P1再加上一个提案被选定需要由半数以上的Acceptor通过的需求,暗示着一个Acceptor必须能够通过不止一个提案。我们为每个提案设定一个编号来记录一个Acceptor通过的那些提案。为了避免混淆,需要保证不同的提案具有不同的编号。如何实现这种保证取决于实现,目前我们假设已经提供了这种保证。当具有某value值的提案被半数以上的Acceptor通过后,我们就认为该value被选定了。此时我们也认为该提案被选定了。此时的提案已经与value变成了不同的东西,提案是由编号+value组成的。

我们允许多个提案被选定,但是我们必须保证所有被选定的提案都具有相同的value值。提案编号需要保证以下条件。

P2:如果具有value值v的提案被选定了,那么所有比它编号更高的被选定的提案的value值也必须是v。

因为编号是全序的,条件P2就保证了只有一个value值被选定的这一关键安全性属性。编号我们可以简单理解为是一个递增的序列,编号小意味着时间更早。

被选定的提案,首先必须被至少一个Acceptor通过,因此我们可以通过以下条件来满足P2。

P2a:如果具有value值v的提案被选定了,那么所有比它编号更高的被Acceptor通过的提案的value值也必须是v。

我们仍然需要P1来保证提案会被选定。但是因为通信是异步的,一个提案可能会在某个Acceptor c还未收到任何提案时就被选定了。假设一个新的Proposer重启了,然后产生了一个具有另一个value值的更高编号的提案,根据P1,就需要c通过这个提案,但是这与P2a矛盾。因此如果要同时满足P1和P2a,需要对P2a进行强化。

P2b:如果具有value值v的提案被选定,那么所有比它编号更高的被Proposer提出的提案的value值也必须是v。

一个提案被Acceptor通过之前肯定要由某个Proposer提出,因此P2b就隐含了P2a,进而隐含了P2。

我们来看看如何证明P2b成立。我们假设某个具有编号m和value值v的提案被选定了,需要证明具有编号n(n>m)的提案都具有value值v。我们可以通过对n使用归纳法来简化证明,这样我们就可以在额外的假设下,即编号在m~n-1的提案具有value值v,来证明编号为n的提案具有value值v。因为编号为m的提案已经被选定了,这意味着肯定存在一个由半数以上的Acceptor组成的集合C,C中的每个Acceptor都通过了这个提案。再结合归纳假设,m被选定意味着:

C中的每个Acceptor都通过了一个编号在m~n-1的提案,每个编号在m~n-1的被Acceptor通过的提案都具有value值v。

因为任何包含半数以上的Acceptor的集合S都至少包含C中的一个成员,我们可以通过维护以下不变性来保证编号为n的提案具有value值v。

P2c:对于任意的n和v,如果编号为n和value值为v的提案被提出,那么肯定存在一个由半数以上的Acceptor组成的集合S,可以满足以下条件中的一个。

  • ·S中不存在任何的Acceptor通过编号小于n的提案。
  • ·v是S中所有Acceptor通过的编号小于n的具有最大编号的提案的value值。

通过维护P2c我们就可以保证P2b了,即P2c->P2b->P2a->P2+P1=>保证了一致性

获取被选定的提案值

为了获取被选定的value,一个Learner必须确定一个提案已经被半数以上的Acceptor通过。最明显的算法是,每个Acceptor,只要它通过了一个提案,就通知所有的Learner,告知它们通过的提案。这可以让Learner尽快找出被选定的value,但是它需要每个Acceptor都与每个Learner通信——所需通信的次数等于二者个数的乘积。

在假定非拜占庭错误的情况下,一个Learner可以很容易地通过另一个Learner了解到一个值已经被选定。我们可以让所有的Acceptor将它们的通过消息发送给一个特定的Learner,当一个value被选定时,再由它通知其他的Learner。这种方法,需要多一个步骤才能通知到所有的Learner。而且也是不可靠的,因为那个特定的Learner可能会失败。但是这种情况下的通信次数,只是Acceptor和Learner的个数之和。

更一般地,Acceptor可以将它们通过的消息发送给一个特定的Learner集合,它们中的每个都可以在一个value被选定后通知所有的Learner。这个集合中的Learner个数越多,可靠性就越好,但是通信复杂度就越高。

由于消息的丢失,一个value被选定后,可能没有Learner会发现。Learner可以询问Acceptor它们通过了哪些提案,但是一个Acceptor出错,都有可能导致无法判断出是否已经有半数以上的Acceptor通过的提案。在这种情况下,只有当一个新的提案被选定时,Learner才能发现被选定的value。因此,如果一个Learner想知道是否已经选定一个value,它可以让Proposer利用上面的算法产生一个提案。

这引出一种需要考虑的异常情况,当一个value被(n/2+1)的Acceptor选定后,其中一个Acceptor出错而“死掉了”,那么对于这种情况,Paxos算法能否正确处理呢?因为这种情况下,某个Learner可能会在这个Acceptor还活着的时候获知这个选定的value,但是其他Learner获取信息时该Acceptor可能已经“死掉了”。对于这种情况,虽然Learner可能一时无法判断哪个value被选定了,但是它可以保证此时被选定的value将一直是被选定的那个value,因为如果Acceptor出错“死掉了”,这并不影响保证多数集合之间肯定存在一个交集,因为该出错的Acceptor对于两个多数集合来说,它们都是“死掉”的那个,根据算法执行过程,我们可以看到多数集合都是通过接受响应来体现的,也就是说它们肯定都是还“活着”的Acceptor,这样不同执行过程中的Acceptor阶段的多数集之间肯定存在一个还活着的公共Acceptor。如果一个死掉的Acceptor是两个(n/2+1)多数集唯一的公共元素,那么它应该是无法满足收到多数集合的Acceptor的响应的。

进展性

很容易构造出一种情况,在该情况下,两个Proposer持续地生成编号递增的一系列提案,但是没有提案会被选定。Proposer p为一个编号为n1的提案完成了Prepare阶段,然后另一个Proposer q为编号为n2(n2>n1)的提案完成了Prepare阶段。Proposer p的针对编号n1的提案的Acceptor阶段的所有Accept请求被忽略,因为Acceptor已经承诺不再通过任何编号小于n2的提案。这样Proposer p就会用一个新的编号n3(n3>n2)重新开始并完成Prepare阶段,这又会导致在Acceptor阶段里Proposer q的所有Accept请求被忽略,如此循环往复。

为了保证进度,必须选择一个特定的Proposer来作为一个唯一的提案提出者。如果这个Proposer可以同半数以上的Acceptor通信,同时可以使用一个比现有的编号都大的编号的提案的话,那么它就可以成功地产生一个可以被通过的提案。再通过当它知道某些更高编号的请求时,舍弃当前的提案并重做,这个Proposer最终一定会产生一个足够大的提案编号。

如果系统中有足够的组件(Proposer、Acceptor及通信网络)工作良好,通过选择一个特定的Proposer,活性就可以达到。著名的FLP结论指出,一个可靠的Proposer选举算法要么利用随机性要么利用实时性来实现——比如使用超时机制。然而,无论选举是否成功,安全性都可以保证。即使同时有2个或以上的Proposer存在,算法仍然可以保证正确性。

实现

Paxos一致性算法假设了一组进程网络。在它的一致性算法中,每个进程扮演着Proposer、Acceptor及Learner的角色,该算法选定一个Learner来扮演那个特定的Proposer和Learner。Paxos一致性算法就是上面描述的请求和响应都是以普通消息的方式发送(响应消息通过对应的提案的编号来标识以防止混淆)。使用可靠性的存储设备来存储Acceptor需要记住的消息来防止出错。Acceptor在真正送出响应之前,会将它记录在可靠性存储设备中。

剩下的就是需要描述保证提案编号唯一性的机制了。不同的Proposer会从不相交的编号集合中选择自己的编号,这样任何两个Proposer就不会有相同编号的提案了。每个Proposer需要将它目前生成的最大编号的提案记录在可靠性存储设备中,然后用一个比已经使用的所有编号都大的提案开始Prepare阶段。

Paxos一致性算法分为了二阶段提交:Prepare阶段和Acceptor阶段。

1.Prepare阶段

  • ·Prepare阶段A:如图18-4所示,Proposer选择一个提案编号n并将Prepare请求发送给所有的Acceptor。
  • ·Prepare阶段B:如图18-5所示,Acceptor收到Prepare消息后,如果提案的编号大于它已经回复的所有Prepare消息,则Acceptor将自己上次接受的提案回复给Proposer,并承诺不再回复小于n的提案。

2.Acceptor阶段

  • ·Acceptor阶段A:如图18-6所示,当一个Proposer收到了多数Acceptor对Prepare的回复(Promised)后,就进入批准阶段。如果没有发现有一个Acceptor接受过一个value,那么向所有的Acceptor发起自己的value和提案编号n。否则,从所有接受过的value中选择对应的提案编号最大的,作为提案的value,提案编号仍然为n。
  • ·Acceptor阶段B:如图18-7所示,在不违背自己向其他Proposer的承诺的前提下,Acceptor收到Accept请求后即接受这个请求。Prepare阶段有两个目的,第一,检查是否有被批准的值,如果有,就改用批准的值。第二,如果之前的提案还没有被批准,则阻塞它们,避免它们和我们发生竞争,当然最终由提案编号的大小决定。

18.6.6 总结

上面的图例中,P1广播了Prepare请求,但是给A3的丢失,不过A1、A2成功返回了,即该Prepare请求得到多数派的应答,然后它可以广播Accept请求,但是给A1的丢了,不过A2,A3成功接受了这个提案。因为这个提案被多数派(A2,A3形成多数派)接受,我们称被多数派接受的提案对应的value被选定。

3个Acceptor之前都没有接受过Accept请求,所以不用返回接受过的提案,但是如果接受过提案,则根据第一阶段B,要带上之前Accept的提案中编号小于n的最大的提案。

Proposer广播Prepare请求之后,收到了A1和A2的应答,应答中携带了它们之前接受过的{n1,v1}和{n2,v2},Proposer则根据n1、n2的大小关系,选择较大的那个提案对应的value,比如n1>n2,那么就选择v1作为提案的value,最后它向Acceptor广播提案{n,v1}。图18-8展示了该选择过程。

18.6.7 缺陷

Paxos算法的主要缺陷是该算法难以被大多人理解,缺乏实现的细节,不是作为构建实际应用的好基础。

  • 11
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值