1 引言
为了实现一个带有容错性的分布式系统而提出的Paxos算法被认为是非常难以理解的。事实上它是一个很简单而且是最平淡无奇的分布式算法之一。它的核心是一个一致性的算法---【5】中提出的“synod”算法,接下来的部分将会讲述这个一致性算法。最后的部分来解释这个复杂的Paxos算法,从一直性的直观应用到建立一个分布式系统的有限状态自动机的模型,应该是众所周知的方法,因为它可能是分布式系统理论中被引用最多的主题。
2 一致性算法
2.1问题
假设一些process可以提出一些提案(value),一个一致性算法保证在这些提出(propose)的提案中有一个会被选择(chosen)。如果没有提案被提出就没有提案会被选择。如果一个提案被选择,则processes就会学习这选择的提案。保证一致性的安全条件为:
* 在提交的提案中,只有一个会被选择。
* 只有一个提案会被选择,而且
* 一个process不会知道一个提案被选择了,直这个提案确实被选择了。
我们不会明确的指出精确的时间性要求,然而,我们的目标是确保一些提交的value最终能够被选择而且如果一个value被选择,那么一个process最终可以学习到这个value。
在这个一致性算法中我们提出三个角色:proposers,acceptors和learners。在一个实现中,一个process可能会由多个代理来扮演,然而由代理到process的映射我们在这里并不关心。假设代理们可以通过发送消息的方式来相互通信。我们用常用的异步通信方式,non-Byzantine模型,
* 代理可以以任何速率操作,可能会因为停止而失败,可能会重启。因此所有的代理可能会在某个value被选择之后而失败,或重启,所以我们的解决方案就要求代理必须能够在记忆一些信息以便在重启后可以使用它们。
* 消息可能是任意的长度,可能会重复可能会丢失但是却不能损坏。
2.2 选择一个Value
最简单的选择Value的方法是只用一个acceptor代理,一个propser 向这个acceptor发送一个proposal,第一个提交上来的value将会被接收。虽然简单,但这个解决方法是不安全的因为如果这个acceptor失效将会导致以后的处理不能运行。
所以让我们尝试另一种方法去选择一个value,我们用多个acceptor代理而非一个。一个proposer发送一个proposed value向一个acceptor的集合。一个acceptor可能会接收这个proposed value。当足够多的acceptor接收这个value时,该value将会被选择。那么足够多的acceptor是多少呢?为了保证只有一个value会被选择,我们可以用集合中的多数代理。因为任意两个多数派最少有一个共同的acceptor,如果一个acceptor只能接受一个value,这个方法是可行的。
如果没有失败和消息丢失,我们希望一个value会被选择当只有一个value被提交,这就有了下面的需求:
P1.一个acceptor必须通过它的第一个决议。
但是这个需求会产生一个问题,很多value会被不同的proposer在同一时间提出,导致当每一个acceptor都通过了一个value,但是没有一个value被他们中的大多数通过。如果只有两个提交上来的value,如果每一个都得到半数acceptor的通过,单个acceptor失效也可能导致不知道哪个value被选择了。
P1和这个必要条件表示只有当一个value被大多数acceptor通过并且这个acceptor必须可以通过超过一个proposal。我们为每个不同的proposal分配一个不同的编号,这样一个proposal就拥有一个proposal编号和一个value。为了防止混乱,我们需要不同的proposal拥有不同的编号。这个取决于如何实现,我们先假设可以做到这一点。当一个proposal的value被大多数acceptor通过,则这个value将会被选择。这样我们就说这个proposal(包括他的value)就被选择了。
P2. 如果一个proposal包括他的value:v被选择了,那么所有编号更高的proposal的value都是v。
因为编号是全序的,P2保证了只有一个value会被选择这一关键安全属性。一个proposal必须被至少一个acceptor通过才能够被选择。因此我们可以认为P2必须满足如下条件:
P2a. 如果一个含有value v的proposal被选择,那么acceptor通过的编号更高的proposal的value都是v。我们仍然通过保证P1来确定哪些proposal被选择,因为通信是异步加载的,一个proposal可能会被一个从未接受任何proposal的acceptor c 选择。为了唤醒一个proposal并且接受一个新的value,P1需要c接受这个proposal,这违反了P2b,为了同时保证P1和P2,我们对P2a进行如下升级:
P2b. 如果一个包含value v的proposal被选择,那么任何编号更好的proposal的value都是v。因为一个proposal在一个acceptor通过它之后才会被选择,所以P2b包含P2a,也包含P2.
为了发现如何才能满足P2b,让我们考虑如何能证明它是成立的。我们可以假设一些proposal编号为m,value为的proposal被选择,然后证明任何编号n大于m的proposal的value也是v,我们可以通过归纳法更为简单的证明它,根据条件每一个编号为n的proposal都有一个value v,其他编号为m---(n-1)的proposal的value也为v,i~j表示表示编号为i至j的集合。编号为m的proposal被选择了,必然会存在一个集合C,其中包含acceptor的多数派,并且都通过了这个proposal。将这个于假设合并,假设m被选择意味着
在集合C中的每一个acceptor都通过一个proposal编号为M~N-1,每一个编号为M-N-1的proposal的value都为v。
因为任何多数派组成的集合S都至少包含C中的一个成员,我们可以得出结论,如果下面的不变性成立,则编号为n的proposal的value为v。
P2c. 对于任何的n和v,如果一个编号为n,value为v的proposal被提交,则会有一个集合S包含acceptor的多数派,(a)S中没有编号比n小的proposal,或者(b)在S中的acceptor通过的proposal中v的编号是最大的。
通过保持P2c,我们就能保证P2b.
为了保证P2C的不变式,一个proposer想要提出一个编号为n的proposal,必须要知道所有编号小于n的最大的那个,如果有的话,它已经或者将要被某些个多数派通过。知道一个proposal已经被通过的方法很简单。预存一个proposal是否会被通过则很难。我们不去预测一个proposal是否能够被通过,我们假设这种情况不会发生。也就是说,这个proposer请求不会通过任何一个编号比它小的proposal。这将会产生一个提交proposal的算法:
1.一个Proposer选择一个新的编号n然后向所有acceptor发送请求,并要求回应
(a) 保证不会接受编号小于n的proposal 和
(b) proposal含有比n小的最大的编号已经通过被通过(如果存在的话)。
我们称这个请求为预备请求。
2.如果这个proposer接受到了多数派的acceptor的回应,则他可以提交一个编号为n,value为v的proposal,v为编号最大的proposal的v,如果这个这个请求没有返回任何proposal它将会是proposer选择的任意值。一个proposer向一些acceptor发送已经被通过的proposal,不一定是回应proposal的acceptor集合,我们称之为accept请求.
这描述了一个proposer的算法。那么acceptor呢?它可以从proposer那里收到两种请求prepare请求和accept请求.一个accept请求,一个acceptor可以忽略任何一个请求而不用担心安全性,任何时候都可以回应一个请求,它可以一直回应prepare请求,它可以回应一个accept请求,通过一个proposal,当且仅当它还没有承诺过。也就是说:
P1a. 一个acceptor可以接受一个编号为n的proposal,当且仅当它没有回应一个编号大于n的prepare请求。
P1a包含P1
我们现在有了一个完整的选择proposal算法,并且满足安全属性要求----假设编号唯一。最终的算法将会通过一些小的优化而得到。
假设一个acceptor收到一个编号为n的proposal,但是它已经回应了一个编号大于n的prepare请求,于是没有必要回应这个编号为n的请求了因为它不会通过这个编号为n的proposal,所以我们可以忽略像这样的prepare请求,我们也可以忽略已经通过的请求。
这样的优化后,一个acceptor只要记住它通过的编号最高的proposal,因为任何失败的情况下都要保证P2c,acceptor必须记住这些信息,待它失败后可以重启。注意proposer可以放弃一个proposal并且放弃有关它的一切信息只要它不会发送编号相同的proposal。
将proposer和acceptor的行为放在一起,我们可以看到算法有如下两个步骤:
步骤1:a) proposer选择一个编号为n的proposal,并且发送一个prepare请求向大量的acceptor
b) 如果一个acceptor接受到一个编号为n的prepare请求并且n大于它已经回应的任何prepare请求,它将会回应这个请求并且保证不会接受比n小的请求,然后将n设为它回应的最高编号(如果有的话)。
步骤2:a) 如果一个proposer接受到一个编号为n的prepare回应,它将会发送一个accept请求向那些回应它的acceptor,编号为n,value为v,v为这些回应中编号最高的proposal的vale或者如果它没有proposal则为一个任意值,
b) 如果一个acceptor接受了一个编号为n的proposal,它将会通过这个proposal除非它收到了一个编号比它大的prepare请求。
一个proposer可以产生很多proposal,只要它对于每一个都遵循这个算法。 它可以放弃一个proposal在协议的中间或者任何时候。即使一个请求或一个回应在被放弃后才到达终点,它的正确性也能够得到保持。在一个proposer想要发出编号更高的proposer放弃是一个很好的做法。因此一个acceptor因为已经收到一个编号更大的proposal而放弃当前的proposal,然后它会通知哪个proposer会放弃这个proposal。这是一个并不影响正确性的优化。
2.3 获知选择的vlaue
为了获知一个value已经被选择,一个leaner必须能够发现一个proposal已经被多数派的acceptor通过,一个很显然的算法是在素有的acceptor中无论何时它通过了一个proposal,向所有的leaner发送这个proposal。这样leaner可以尽快知道选择的proposal,但这需要每个acceptor通知每个leaner,需要的消息数量为两者的积。
基于non-Byzantine假设,一个leaner可以很简单的从其他leaner知道一个value是否被选择过了。我们可以使一个acceptor向一个主要的acceptor发送一个它接受某个value的通知。这需要在所有的acceptor中存在一个额外的行为来发现这个被选择的value。这也同样不可靠,因为主acceptor有可能失效但是需要的消息数仅为acceptor和leaner的和。
一般来说,一个acceptor可以将它的通过信息发向一组主acceptor,他们可以将某个value被选择的信息发送给所有的leaner。主acceptor的数量越多越可靠但是通信的复杂度也会增加(消息数增加)。
因为消息丢失,可能没有leaner知道value被选择,leaner可能会询问acceptor哪些proposal被选择了。但是由于acceptor的失效可能不会知道多数派是否通过了一个proposal。这样,leaner只有在一个新的proposal被选择的时候才会知道哪个value被选择了。如果一个leaner需要知道一个value是否被选择了它可以让proposer用上面的算法发出一个proposal。
2.4处理流程
可以构造这样一个场景,两个proposer分别维护一个不断增长的proposal队列,哪一个都没有被选择。proposer P 完整的按照步骤1发出一个编号为n1的proposal。另外一个proposer Q 完整的按照步骤1发出一个编号为n2>n1的proposal。proposer P 的步骤2中的请求将会被忽略,因为acceptor保证了不会通过编号小于n2的proposal,然后P继续开始步骤1并且选了编号n3>n2导致Q的步骤被忽略,如此类推。。
为了保证流程,必须选择一个主的proposer,只有主proposer才能提出proposal,如果主proposer发出的proposal成功的被多数派通过并且它用的编号比以前的任何一个都要大,则可以认为它成功的发出了一个proposal并且被通过了。如果已存在编号更高的proposal,它将会放弃并且重试直至选择了一个足够高的编号。
如果系统可以正常工作(proposer,acceptor以及网络通信)。通过选择一个主的proposer系统就可以保持响应。Fischer, Lynch, and Patterson【1】的人的研究结果表明主proposer的选择必须是随机并且实时的例如选择超时机制。然而不论选择选择失败与否,安全性都能够得到保持。
2.5 实现
paxose算法假设了一组网络进程。在一致性算法下,每一个进程都扮演者主proposer,主leaner,主acceptor。在paxose一致性算法中如同上面描述的那样,请求和回应将会以消息的方式发送(回应的消息将会被打上proposal的编号改防止混淆)。使用持久化存储来保证acceptor失效后也能够记忆重要的消息。acceptor在发送响应前需要持久化这个回应。
下面是描述保证proposal不会有相同编号的机制。不同的proposer从不相交的集合中选择编号,所以两个不同的proposal永远不会使用相同的编号。每一个proposer记住它发出的编号最高的proposal然后发送一个编号更高的proposal,直到所有的编号都使用过。
3实现一个状态机
实现一个状态机的简单方法是用一些客户端向中心服务器发送指令。服务器可以被描述成为一个按照一定顺序执行指令的有限状态自动机。自动机包含当前状态,它通过一个输入并产生一个输出结果并切换到一个新的状态来执行一个步骤。例如一个分布式的银行系统的客户端可以是一个柜员,状态机就是所有用户的帐户结余。一个提款的操作会产生一个状态机的指令来减少帐户的结余当且仅当要提取的款项小于用户的结余时输出新旧余额数。
使用一个单个的服务端会因为服务端失效而失效,因此我们使用一组服务器,每一个单独的实现一个状态机。因为这个状态机是确定性的,所有的服务器会根据指令产生相同的输出和状态,所以一个客户端可以用任意一个服务器的结果。
为了确保所有的服务器执行相同的状态机指令的序列。我们实现一个一致性paxose算法实例的序列。被选择的value通过序列中的第i个实例成为第i个状态机的指令。每一个服务端都扮演着所有的角色在每一个算法的实例中。现在,我假设服务器的集合是固定的,所以一致性算法的实例用的是相同的代理集合。
在一个正确的操作中,一个单独的服务器被选择为leader,它扮演着主proposer(唯一的一个能提交proposal的proposer)在一致性算法的实例中。客户端向这个leader发送命令,leader决定每个命令应该出现的顺序。如果一个leader决定某一个客户端的指令要在第135,它将会试图将这个指令选择为第135个算法的实例的value。这通常会成功。有可能因为失效而失败,或者因为令一个服务端想让自己成为leader,并且对第135个指令有其他的安排。但是一致性算法保证了最多只有一个命令被选择为第135个。
一致性算法的有效性在于,在第二步之前,提交的value是不会被选择的。回想一下,在完整的执行完proposer算法的步骤一之后,proposal已经被选择了或者可以提出任意一个value。
我现在将要描述在正确的操作下基于Paxose的状态机将会如何工作。然后,我们将会讨论什么会导致错误发生。我会考虑前一个leader失效,新的leader被选择后会发生什么情况。(系统启动时是一个特例,这时还没有任何指令)
新的leader,也是所有一致性算法中的leaner,需要知道被选择的大多数指令。假设它知道指令1-134,138和139,也就是算法实例中1-134,138和139选择的实例(后面我们将会看到指令之间的间隔是如何引起的)。实例135-137将会执行第一步骤和所有大于139的实例。假设这次执行的结果是value将会决定第135-140实例提出,但不会影响其他proposal的结果。然后leader为实例135-140执行步骤2,并选择了135-140的指令。
一个leader和其他所有知道它的指令的服务器,现在可以执行指令1-135。然而它不能执行138-140.因为136和137号指令还没有选择。leader可以把后面的两个指令选择136和137,一个特殊的指令"no-op"将不会导致状态的改变。一旦当这个“no-op"指令被选择了,指令138-140将可以被选择。
现在指令1-140都被选择了。leader也已经完整对于编号大于140的算法实例的执行了步骤1,并且可以在第二步提出任何proposal。它把客户端发来到下一条请求编号为141并当作算法实例的141的步骤2的proposal。下一条为142,以此类推。。。
在leader知道它发出的第141号proposal被选择之前可以发出指令142。它发出的141号指令的消息全部丢失或者142号指令已经被选择了都是有可能发生的。当leader在实例141的第二步中没有收到预期的回应时,它将会重新提交这些消息。如果一切正常,它提交的指令将会被选择。然而它也有可能先失效,在命令序列中留下了一个间隔,一般的假设一个leader可以先选择前部分的a个指令,也就是说它可以提取出指令i+1到i+a在指令1-i被选择之后。那么最多可以产生a-1个间隔。
一个新选择的leader执行步骤一中无穷多的实例---在上面的场景中,实例135-137和所有大于139的实例。对于所有的实例用相同proposal编号,它可以通过向所有服务器发送一个合理大小的消息。在步骤一中一个acceptor不只是简单的回应一个OK除非当它已经收到了步骤2的消息从一些proposer中。所以,一个服务器可以向所有的实例回应一个足够简单的消息,多次执行步骤1也不会出现问题。
由于leader的失效和选择一个新的leader应该是一个小概率事件,有效的执行一条状态机命令的开销达到对 命令/value的一致性仅仅是步骤2的开销。可以证明,在允许失效的情况下Paxose一致性算法中的步骤2可能是所有一致性算法中开销最小的算法。因此Paxose算法在本质上是最优的。
上面对系统正常操作的讨论是基于只有一个leader的情况下的除去当前leader失效和选出新leader这一短暂的时间段。在一个异常的周期中,leader可能会失效,如果没有一个服务器扮演leader的角色,那么就不会有新的指令被提交。如果很多服务器都认为自己是leader,他们会在相同的实例中提交value,这将会导致所有的value都不会被选择。然而安全性是得到保证的----两个不同的服务器永远不会在第i个状态机指令上选择的value达成一致。选择单一的leader只是为了保证流程。
如果服务器的集合是可以改变的,必须要有方法决定哪个服务器实现哪个算法的实例。最简单的方法是通过状态机本身来做。当前的状态机集合可以作为状态的一部分也可以改变状态机指令的序列。我们可以使一个leader预取a个指令,并执行i+a的算法实例在执行第i个状态机指令之后。可以使用简单的重配置算法来增强算法的可靠性。