用于实现高容错性分布式系统的Paxos算法,一直以来老是被认为是难以理解的,或许是由于对不少人来讲,初始版本就像是”希腊语"同样(最初的论文是以希腊故事展开的形式)[5]。实际上,它也算是最浅显易见的分布式算法之一了。它的核心就是一个一致性算法——论文[5]中的“synod”算法。在下一个章节能够看到,它基本上是根据一个一致性算法所必需知足的条件天然而然地推断出来的。最后一个章节,咱们经过将Paxos算法做为构建一个实现了状态机的分布式系统的一致性实现,来完整地描述它。这种使用状态机方法的论文[4]应该早已广为人知,由于它可能已是分布式系统理论研究领域被引用最普遍的了。算法
假设有一组能够提出提案的进程集合。一个一致性算法须要保证:
在这些被提出的提案中,只有一个会被选定。
若是没有提案被提出,则不会有被选定的提案。
当一个提案被选定后,进程应该能获取被选定提案的信息。安全
对于一致来讲,安全性(Safety)需求是这样的:
只有被提出的提案才能被选定。
只能有一个值被选中(chosen),同时
进程不能认为某个提案被选定,除非它真的是被选定的那个。性能优化
咱们不会尝试去精确地描述活性(Liveness)需求。可是从整体上看,最终的目标是保证有一个提案被选定,而且当提案被选定后,进程最终也能获取到被选定提案的信息。
一个分布式算法,有两个重要的属性:Safety和Liveness,简单来讲:
Safety是指那些须要保证永远都不会发生的事情
Liveness是指那些最终必定会发生的事情服务器
在这个一致性算法中,有三个参与角色,咱们分别用Proposer,Acceptor和Learner来表示。在具体实现中,一个进程可能充当不止一种角色,可是在这里咱们并不关心它们之间的映射关系。网络
假设不一样的参与者之间能够经过发消息来进行通讯,咱们使用普通的非拜占庭模式的异步模型:
每一个参与者以任意的速度运行,可能会因中止而执行失败,也可能会重启。当一个提案被选定后,全部的参与者都有可能失败而后重启,除非这些参与者能够记录某些信息,不然是不可能存在一个解法的。
消息在传输中可能花费任意时间,可能会重复,也可能丢失,但不会被损坏(不会被篡改,即不会发生拜占庭问题)。异步
选定提案最简单的方式就是只有一个Acceptor存在。Proposer发送提案给Acceptor,Acceptor会选择它接收到的第一个提案做为被选提案。虽然简单,这个解决方案却很难让人满意,由于当Acceptor出错时,整个系统就没法工做了。分布式
所以,咱们应该选择其余方式来选定提案,好比能够用多个Acceptor来避免一个Acceptor的单点问题。这样的话,Proposer向一个Acceptor集合发送提案,某个Acceptor可能会经过(accept)这个提案。当有足够多的Acceptor经过它时,咱们就认为这个提案被选定了。那么怎样才算是足够多呢?为了确保只一个提案被选定,咱们可让这个集合大到包含了Acceptor集合中的多数成员。由于任意两个多数集(majority)至少包含一个公共成员,若是咱们再规定一个Acceptor只能经过一个提案,那么就能保证只有一个提案被选定(这是不少论文都研究过的多数集的一个普通应用[3])。oop
假设没有失败和消息丢失的状况,若是咱们但愿在每一个Proposer只能提出一个提案的前提下仍然能够选出一个提案来,这就意味着以下需求:性能
P1. 一个Acceptor必须经过它收到的第一个提案。
可是这个需求会引起另外的问题。若是有多个提案被不一样的Proposer同时提出,这会致使虽然每一个Acceptor都经过了一个提案,可是没有一个提案是由多数人经过的。甚至即便只有两个提案被提出,若是每一个都被差很少一半的Acceptor经过了,哪怕只有一个Acceptor出错均可能致使没法肯定该选定哪一个提案。
好比有5个Acceptor,其中2个经过了提案a,另外3个经过了提案b,此时若是经过提案b的3个当中有一个出错了,那么a和b的经过数都为2, 这样就没法肯定了。优化
P1再加一个提案被选定须要由半数以上Acceptor经过的这个需求,暗示着一个Acceptor必需要能经过不止一个提案。咱们为每一个提案分配一个编号来记录一个Acceptor经过的那些提案,因而一个提案就包含一个提案编号以及它的value值。为了不形成混淆,须要保证不一样的提案具备不一样编号。如何实现这个功能依赖于具体的实现细节,在这里咱们假设已经实现了这种保证。当一个具备value值的提案被多数Acceptor经过后,咱们就认为该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,其中i..j表示从i到j的集合。由于编号为m的提案已经被选定了,这就意味着存在一个多数Acceptor组成的集合C,C中的每一个成员都经过了这个提案。结合概括的假设,m被选定意味着:
C中的每个Acceptor都经过了一个编号在m..(n-1)之间的提案,而且每一个编号在m..(n-1)之间的被Acceptor经过的提案都具备value值v。
因为任何包含多数Acceptor的集合S都至少包含一个C中的成员,咱们能够经过保持以下不变性来确保编号为n的提案具备value值v:
P2c. 对于任意v和n,若是一个编号为n,value值为v的提案被提出,那么确定存在一个由多数Acceptor组成的集合S知足如下条件中的一个: a. S中不存在任何Acceptor经过了编号小于n的提案 b. v是S中全部Acceptor已经经过的编号小于n的具备最大编号的提案的value值。
经过维护P2c的不变性咱们就能够知足P2b的条件了。
为了维护P2c的不变性,一个Proposer在提出编号为n的提案时,若是存在一个将要或者已经被多数Acceptor经过的编号小于n的最大编号提案,Proposer须要知道它的信息。获取那些已经被经过的提案很简单,可是预测将来会被经过的却很困难。为了不去预测将来,Proposer经过提出承诺不会有那样的经过状况来控制它。换句话说,Proposer会请求那些Acceptor不要再经过任何编号小于n的提案了。这就致使了以下的提案生成算法:
Proposer选择一个新的提案编号n,而后向某个Acceptor集合中的成员发送请示,要求它做出以下回应:
(a)保证再也不经过任何编号小于n的提案。 (b)当前它已经经过的编号小于n的最大编号提案,如何存在的话。 咱们把这样的请求称为编号为n的prepare请求。
若是Proposer收到来自集合中多数成员的响应结果,那么它能够提出编号为n,value值为v的提案,这里v是全部响应中最大编号提案的value值,若是响应中不包含任何提案,那么这个值就由Proposer自由决定。
Proposer经过向某个Acceptor集合发送须要被经过的提案请求来产生一个提案(这里的Acceptor集合不必定是响应前一个请求的集合)。这们把这个叫作accept请求。
目前咱们描述了Proposer端的算法。那么Acceptor端是怎样的呢?它可能会收到来自Proposer端的两种请求:prepare请求和accept请求。Acceptor能够忽略任意请求而不用担忧破坏算法的安全性。所以咱们只须要说明它在什么状况下能够对一个请求做出响应。它能够在任什么时候候响应prepare请求也能够在不违反现有承诺的状况下响应accept请求。换句话说:
P1a. 一个Acceptor能够经过一个编号为n的提案,只要它还未响应任何编号大于n的prepare请求。
能够看出P1a包含了P1。
如今咱们就得到了一个知足安全性需求的提案选定算法——假设在提案编号惟一的前提下。只要再作点小优化,就能获得最终的算法了。
假设一个Acceptor收到了一个编号为n的prepare请求,可是它已经对编号大于n的prepare请求做出了响应,所以它确定不会再经过任何新的编号为n的提案。那么它就没有必要对这个请求做出响应,由于它确定不会经过编号为n的提案,因而咱们会让Acceptor忽略这样的prepare请求,咱们也会让它忽略那些它已经经过的提案的prepare请求。
经过这个优化,Acceptor只须要记住它已经经过的提案的最大编号以及它已经响应过prepare请求的提案的最大编号。由于必需要在出错的状况下也保证P2c的不变性,因此Acceptor要在故障和重启的状况下也能记住这些信息。Proposer能够随时丢弃提案以及它的全部信息——只要它能够保证不会提出具备相同编号的提案便可。
把Proposer和Acceptor的行为结合起来,咱们就能获得算法的以下两阶段执行过程:
Phase 1:
Proposer选择一个提案编号n,而后向Acceptor的多数集发送编号为n的prepare请求。
若是一个Acceptor收到一个编号为n的prepare请示,且n大于它全部已响应请求的编号,那么它就会保证不会再经过任意编号小于n的提案,同时将它已经经过的最大编号提案(若是存在的话)一并做为响应。
Phase 2:
若是Proposer收到多数Acceptor对它prepare请求(编号为n)的响应,那么它就会发送一个编号为n,value值为v的提案的accept请求给每一个Acceptor,这里v是收到的响应中最大编号提案的值,若是响应中不包含任何提案,那么它就能够是任意值。
若是Acceptor收到一个编号为n的提案的accept请求,只要它还未对编号大于n的prepare做出响应,它就能够经过这个提案。
一个Proposer能够提出多个提案,只要它能遵循以上算法约定。它能够在任意时刻丢弃某个提案(即便针对该提案的请求或响应在提案丢弃后好久才到达,正确性依然能够保证)。若是Proposer已经在尝试提交更大编号的提案,那么丢弃也何尝不是一件好事。所以,若是一个Acceptor由于已经收到更高编号的prepare请求而忽略某个prepare或者accept请求,它应该通知对应的Proposer,而后该Proposer能够丢弃这个提案。这是一个不影响正确性的性能优化。
为了获取被选定的值,一个Learner必需要能知道一个提案已经被多数Acceptor经过了。最直观的算法是,让每一个Acceptor在经过一个提案时就通知全部Learner,把经过的提案告知它们。这可让Learner尽快找到被选定的值,但这须要每一个Acceptor和Learner之间互相通讯——通讯次数等于两者数量的乘积。
在假设非拜占庭错误的前提下,一个Learner能够很容易地经过另外一个Learner了解一个值已经被选定了。咱们可让全部Acceptor将它们的经过信息发送给一个特定的Learner,当一个value被选定时,由它来通知其余Learner。这种方法须要额外一个步骤才能通知到全部Learner,并且它也不是可靠的,由于那个特定的Learner可能会发生一些故障。可是这种状况下的通讯次数,只须要两者数量之和。
更通常地,Acceptor能够将它们的经过信息发送给一个特写的Learner集合,它们中的任何一个均可以在某个value被选定后通知全部Learner。这个集合中的Learner越多,可靠性就越好,通讯复杂度也相应更高。
由于消息可能会丢失,一个value被选定后,可能没有Learner会发现。Learner能够向Acceptor询问它们经过了哪些提案,可是任一Acceptor出错,都有可能致使没法分辨是否有多数Acceptor经过了某个提案。在这种状况下,只有当一个新的提案被选定时,Learner才能发现被选定的value。若是一个Learner想知道是否已经选定一个value,它可让Proposer利用上面的算法提出一个提案。
很容易能够构造出这样一种状况,两个Proposer持续地提出序号递增的提案,可是没有提案会被选定。Proposer p为编号为n1的提案完成Phase 1, 而后另外一个Proposer q为编号为n2(n2>n1)的提案完成Phase 1。Proposer p对于编号n1的Phase 2的accept请求会被忽略,由于Acceptor承诺再也不经过任何编号小于n2的提案。这样Proposer p就会用一个新的编号n3(n3>n2)从新开始并完成Phase 1,这又致使了Proposer q对于Phase 2的accept请求被忽略,如此往复。
为了保证进度,必须选择一个特定的Proposer做为惟一的提案提出者。若是这个Proposer能够和多数Acceptor进行通讯,而且可使用比已用编号更大的编号来进行提案的话,那么它提出的提案就能够成功被经过。若是知道有某些编号更高的请求,它能够经过舍弃当前的提案并从新开始,这个Proposer最终必定会选到一个足够大的提案编号。
若是系统中有足够的组件(Proposer, Acceptor以及网络通讯)工做良好,经过选举一个特定的Proposer,活性就可以达到。著名的FLP理论[1]指出,一个可靠的Proposer选举算法要么利用随时性要么利用实时性来实现——好比使用超时机制。然而不管选举是否成功,安全性均可以保证。
Paxos算法[5]假设了一组进程网络。在它的一致性算法中,每一个进程都扮演着Proposer, Acceptor以及Learner的角色。该算法选择了一个Leader来扮演那个特定的Proposer和Learner。Paxos一致性算法就是上面描述的那个,请求和响应都以普通消息的方式发送(响应消息经过对应的提案编号来标识以免混淆)。使用可靠的存储设备存储Acceptor须要记住的信息来防止出错。Acceptor在真正发送响应以前,会将它记录到可靠的存储设备中。
剩下的就是描述若是保证不会用到重复编号的机制了。不一样的Proposer从不相交的编号集合中选择本身的编号,这样任何两个Proposer就不会用到相同的编号了。每一个Proposer都记录(在可靠存储设备中)它使用过的最大编号,而后用比这更大编号的提案开始Phase 1。
有一种实现分布式系统的简单方式,就是使用一组客户端集合向中央服务器发送命令。服务器能够当作一个以某种顺序执行客户端命令的肯定性状态机。这个状态机有个当前状态,经过接收一个命令看成输入来产生一个输出和新状态。好比,分布式银行系统的客户端多是一些出纳员,状态机的状态则由全部用户的帐户余额组成。一个取款操做,经过执行一个减小帐户余额的状态机命令(当且仅当余额大于取款数目时)实现,而后将新旧余额做为输出。
使用单点中央服务器的系统在该服务器故障的状况下,整个系统都将运行失败。所以咱们用一组服务器来代替它,每一个服务器都独立实现了该状态机。由于这个状态机是肯定性的,若是全部服务器都以一样的顺序执行命令,那么它们将产生相同的状态机状态和输出。一个提出命令的客户端,可使用任意服务器为它产生的输出。
为了保证全部服务器都能执行相同的状态机命令序列,咱们须要实现一系列独立的Paxos一致性算法实例,第i个实例选定的值做为序列中的第i个状态机命令。在算法的每一个实例中,每一个服务器担任全部角色(Proposer,Acceptor和Learner)。如今,咱们假设服务器的集合是固定的,这样全部的一致性算法实例都具备相同的参与者集合。
在正常执行中,一个服务器被选举成为Leader,它会在全部一致性算法实例当中扮演特定的Proposer(惟一的提案提出者)。客户端给Leader发送命令,它来决定每条命令出如今序列当中的位置。若是Leader决定某个客户端命令应该是第135个,它会尝试让该命令成为第135个一致性算法实例选定的value值。这一般都会成功,可是在一些故障或者有另外的服务器也认为本身是Leader而且对第135个命令持有异议时,它可能会失败。可是一致性算法能够保证,最多只有一条命令会被选定为第135条。
这个方法的关键在于,在Paxos一致性算法中,被提出的value值只在Phase 2才会被选定。回忆一下,在Proposer完成Phase 1时,要么提案的value值被肯定了,要么Proposer能够自由提出任意值。
咱们如今描述了Paxos状态机实现是怎样在正常状况下运行的,接下来咱们看看会有哪些出错的状况,看下以前的Leader故障以及新的Leader被选举出来后会发生什么(系统启动是一种特殊状况,此时尚未命令被提出)。
新的Leader被选举出来后,首先要成为全部一致性算法实例的Learner,须要知道目前已经选定的大部分命令。假设它知道命令1-134,138以及139——也就是一致性算法实例1-134,138以及139选定的值(后面咱们会看到这样的命令缺口是如何产生的)。接下来它会执行135-137以及139之后的算法实例的Phase 1(下面会描述如何来作)。假设执行结果代表,实例135和140的提案值已被肯定,可是其余执行实例的提案值是没有限制的。那么Leader能够执行实例135和140的Phase 2,进而选定第135和140条命令。
Leader以及其余已经获取Leader全部已知命令的服务器,如今能够执行命令1-135。然而它还不能执行命令138-140,由于命令136和137还未被选定。Leader能够将接下来两条客户端请求的命令看成命令136和137。同时咱们也能够提出一个特殊的“noop”指令来当即填补这个空缺但保持状态不变(经过执行一致性算法实例136和137的Phase 2来完成)。一旦该no-op指令被选定,命令138-140就能够被执行了。
命令1-140目前已经被选定了。Leader也已经完成了全部大于140的一致性算法实例的Phase 1,并且它能够在Phase 2中自由地为这些实例指定任意值。它为下一个从客户端接收的命令分配序号141, 并在Phase 2中将它做为第141个一致性算法实例的value值。它将接收到的下一个客户端命令做为命令142, 并以此类推。
Leader能够在它提出的命令141被选定前提出命令142。它发送的关于命令141的提案信息可能所有丢失,所以在全部其余服务器获知Leader选定的命令141以前,命令142就可能已被选定。当Leader没法收到实例141的Phase 2的指望回应时,它会重传这些信息。若是一切顺利的话,它的提案命令将被选定。可是仍然可能会失败,形成在选定的命令序列中出现缺口。通常来讲,假设Leader能够提早肯定a个命令,这意味着命令i被选定以后,它就能够提出i+1到i+a的命令了。这样就可能造成长达a-1的命令缺口。
一个新选定的Leader须要为无数个一致性算法实例执行Phase 1——在上面的场景中,就是135-137以及全部大于139的执行实例。经过向其余服务器发送一条合适的消息,就可让全部执行实例使用同一个提案编号(计数器)。在Phase 1中,只要一个Acceptor已经收到来自某Proposer的Phase 2消息,那么它就能够为不止一个实例做出经过回应(在上面的场景中,就是针对135和140的状况)。所以一个服务器(做为Acceptor时)能够用一条适当的短消息对全部实例做出回应。执行这样无限多的实例的Phase 1也不会有问题。
这里应该是指稳定的Paxos模型,Phase 1能够被省略,只要编号计数器是惟一的。
因为Leader的故障和新Leader的选举是不多见的状况,那么执行一条状态机命令的主要开销,即在命令值上达成一致性的开销,就是执行一致性算法中Phase 2的开销。能够证实,在容许失效的状况下,Paxos一致性算法的Phase 2在全部一致性算法中具备最小可能的时间复杂度[2]。所以Paxos算法基本上是最优的。
在系统正常运行的状况下,咱们假设老是只有一个Leader,只有在当前Leader故障及选举出新Leader之间的短期内才会违背这个假设。在特殊状况下,Leader选举可能失败。若是没有服务器扮演Leader,那么就没有新命令被提出。若是同时有多个服务器认为本身是Leader,它们在一个一致性算法执行实例中可能提出不一样value值,这可能致使没有任何值能被选定。可是安全性是能够保证的——不可能有两个不一样的值被选定为第i条状态机命令。单个Leader的选举只是为了保证流程能往下进行。
若是服务器的集合是变化的,那么必须有某种方法能够决定哪些服务器来实现哪一些一致性算法实例。最简单的方式就是经过状态机自己来完成。当前的服务器集合能够是状态的一部分,同时也能够经过状态机命令来改变。经过用执行完第i条状态机命令后的状态来描述执行一致性算法i+a的服务器集合,咱们就能让Leader提早获取a个状态机命令。这就容许任意复杂的重配置算法有一个简单实现。
参与文献
[1] Michael J. Fischer, Nancy Lynch, and Michael S. Paterson. Impossibility of distributed consensus with one faulty process. Journal of the ACM, 32(2):374–382, April 1985. [2] Idit Keidar and Sergio Rajsbaum. On the cost of fault-tolerant consensus when there are no faults—a tutorial. TechnicalReport MIT-LCS-TR-821, Laboratory for Computer Science, Massachusetts Institute Technology, Cambridge, MA, 02139, May 2001. also published in SIGACT News 32(2) (June 2001). [3] Leslie Lamport. The implementation of reliable distributed multiprocess systems. Computer Networks, 2:95–114, 1978. [4] Leslie Lamport. Time, clocks, and the ordering of events in a distributed system. Communications of the ACM, 21(7):558–565, July 1978. [5] (1, 2, 3, 4) Leslie Lamport. The part-time parliament. ACM Transactions on Computer Systems, 16(2):133–169, May 1998.