Paxos算法

文章目录
  1. 1. 问题和假设
  2. 2. 算法
    1. 2.1. 算法的提出与证明
    2. 2.2. 算法的内容
      1. 2.2.1. 决议的提出与批准
      2. 2.2.2. 实例
        1. 2.2.2.1. 情况一
        2. 2.2.2.2. 情况二
        3. 2.2.2.3. 情况三
      3. 2.2.3. 决议的发布
      4. 2.2.4. Progress的保证

Paxos算法是莱斯利·兰伯特(英语:Leslie Lamport,LaTeX中的“La”)于1990年提出的一种基于消息传递且具有高度容错特性的一致性算法。

问题和假设

分布式系统中的节点通信存在两种模型:共享内存(Shared memory)和消息传递(Messages passing)。基于消息传递通信模型的分布式系统,不可避免的会发生以下错误:进程可能会慢、被杀死或者重启,消息可能会延迟、丢失、重复,在基础Paxos场景中,先不考虑可能出现消息篡改即拜占庭错误的情况。Paxos算法解决的问题是在一个可能发生上述异常的分布式系统中如何就某个值达成一致,保证不论发生以上任何异常,都不会破坏决议的一致性。一个典型的场景是,在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点都执行相同的操作序列,那么他们最后能得到一个一致的状态。为保证每个节点执行相同的命令序列,需要在每一条指令上执行一个“一致性算法”以保证每个节点看到的指令一致。一个通用的一致性算法可以应用在许多场景中,是分布式计算中的重要问题。因此从20世纪80年代起对于一致性算法的研究就没有停止过。

为描述Paxos算法,Lamport虚拟了一个叫做Paxos的希腊城邦,这个岛按照议会民主制的政治模式制订法律,但是没有人愿意将自己的全部时间和精力放在这种事情上。所以无论是议员,议长或者传递纸条的服务员都不能承诺别人需要时一定会出现,也无法承诺批准决议或者传递消息的时间。但是这里假设没有拜占庭将军问题(Byzantine failure,即虽然有可能一个消息被传递了两次,但是绝对不会出现错误的消息);只要等待足够的时间,消息就会被传到。另外,Paxos岛上的议员是不会反对其他议员提出的决议的。

对应于分布式系统,议员对应于各个节点,制定的法律对应于系统的状态。各个节点需要进入一个一致的状态,例如在独立Cache的对称多处理器系统中,各个处理器读内存的某个字节时,必须读到同样的一个值,否则系统就违背了一致性的要求。一致性要求对应于法律条文只能有一个版本。议员和服务员的不确定性对应于节点和消息传递通道的不可靠性。


算法

算法的提出与证明

首先将议员的角色分为proposers,acceptors,和learners(允许身兼数职)。proposers提出提案,提案信息包括提案编号和提议的value;acceptor收到提案后可以接受(accept)提案,若提案获得多数acceptors的接受,则称该提案被批准(chosen);learners只能“学习”被批准的提案。划分角色后,就可以更精确的定义问题:

  • 决议(value)只有在被proposers提出后才能被批准(未经批准的决议称为“提案(proposal)”);
  • 在一次Paxos算法的执行实例中,只批准(chosen)一个value;
  • learners只能获得被批准(chosen)的value。

另外还需要保证progress。这一点以后再讨论。

作者通过不断加强上述3个约束(主要是第二个)获得了Paxos算法。

批准value的过程中,首先proposers将value发送给acceptors,之后acceptors对value进行接受(accept)。为了满足只批准一个value的约束,要求经“多数派(majority)”接受的value成为正式的决议(称为“批准”决议)。这是因为无论是按照人数还是按照权重划分,两组“多数派”至少有一个公共的acceptor,如果每个acceptor只能接受一个value,约束2就能保证。

于是产生了一个显而易见的新约束:

1
P1:一个acceptor必须接受(accept)第一次收到的提案。

注意P1是不完备的。如果恰好一半acceptor接受的提案具有value A,另一半接受的提案具有value B,那么就无法形成多数派,无法批准任何一个value。

约束2并不要求只批准一个提案,暗示可能存在多个提案。只要提案的value是一样的,批准多个提案不违背约束2。于是可以产生约束P2:

1
P2:一旦一个具有value v的提案被批准(chosen),那么之后批准(chosen)的提案必须具有value v。

注:通过某种方法可以为每个提案分配一个编号,在提案之间建立一个全序关系,所谓“之后”都是指所有编号更大的提案。

如果P1和P2都能够保证,那么约束2就能够保证。

批准一个value意味着多个acceptor接受(accept)了该value.因此,可以对P2进行加强:

1
P2a:一旦一个具有value v的提案被批准(chosen),那么之后任何acceptor再次接受(accept)的提案必须具有value v。

由于通信是异步的,P2a和P1会发生冲突。如果一个value被批准后,一个proposer和一个acceptor从休眠中苏醒,前者提出一个具有新的value的提案。根据P1,后者应当接受,根据P2a,则不应当接受,这中场景下P2a和P1有矛盾。于是需要换个思路,转而对proposer的行为进行约束:

1
P2b:一旦一个具有value v的提案被批准(chosen),那么以后任何proposer提出的提案必须具有value v。

由于acceptor能接受的提案都必须由proposer提出,所以P2b蕴涵了P2a,是一个更强的约束。
但是根据P2b难以提出实现手段。因此需要进一步加强P2b。

假设一个编号为m的value v已经获得批准(chosen),来看看在什么情况下对任何编号为n(n>m)的提案都含有value v。因为m已经获得批准(chosen),显然存在一个acceptors的多数派C,他们都接受(accept)了v。考虑到任何多数派都和C具有至少一个公共成员,可以找到一个蕴涵P2b的约束P2c:

1
2
P2c:如果一个编号为n的提案具有value v,那么存在一个多数派,要么他们中所有人都没有接受(accept)编号小于n 
的任何提案,要么他们已经接受(accept)的所有编号小于n的提案中编号最大的那个提案具有value v。

可以用数学归纳法证明P2c蕴涵P2b:
假设具有value v的提案m获得批准,当n=m+1时,采用反证法,假如提案n不具有value v,而是具有value w,根据P2c,则存在一个多数派S1,要么他们中没有人接受过编号小于n的任何提案,要么他们已经接受的所有编号小于n的提案中编号最大的那个提案是value w。由于S1和通过提案m时的多数派C之间至少有一个公共的acceptor,所以以上两个条件都不成立,导出矛盾从而推翻假设,证明了提案n必须具有value v;

若(m+1)..(N-1)所有提案都具有value v,采用反证法,假如新提案N不具有value v,而是具有value w’,根据P2c,则存在一个多数派S2,要么他们没有接受过m..(N-1)中的任何提案,要么他们已经接受的所有编号小于N的提案中编号最大的那个提案是value w’。由于S2和通过m的多数派C之间至少有一个公共的acceptor,所以至少有一个acceptor曾经接受了m,从而也可以推出S2中已接受的所有编号小于n的提案中编号最大的那个提案的编号范围在m..(N-1)之间,而根据初始假设,m..(N-1)之间的所有提案都具有value v,所以S2中已接受的所有编号小于n的提案中编号最大的那个提案肯定具有value v,导出矛盾从而推翻新提案n不具有value v的假设。根据数学归纳法,我们证明了若满足P2c,则P2b一定满足。

P2c是可以通过消息传递模型实现的。另外,引入了P2c后,也解决了前文提到的P1不完备的问题。

算法的内容

要满足P2c的约束,proposer提出一个提案前,首先要和足以形成多数派的acceptors进行通信,获得他们进行的最近一次接受(accept)的提案(prepare过程),之后根据回收的信息决定这次提案的value,形成提案开始投票。当获得多数acceptors接受(accept)后,提案获得批准(chosen),由proposer将这个消息告知learner。这个简略的过程经过进一步细化后就形成了Paxos算法。

在一个paxos实例中,每个提案需要有不同的编号,且编号间要存在全序关系。可以用多种方法实现这一点,例如将序数和proposer的名字拼接起来。如何做到这一点不在Paxos算法讨论的范围之内。

如果一个没有chosen过任何proposer提案的acceptor在prepare过程中回答了一个proposer针对提案n的问题,但是在开始对n进行投票前,又接受(accept)了编号小于n的另一个提案(例如n-1),如果n-1和n具有不同的value,这个投票就会违背P2c。因此在prepare过程中,acceptor进行的回答同时也应包含承诺:不会再接受(accept)编号小于n的提案。这是对P1的加强:

1
P1a:当且仅当acceptor没有回应过编号大于n的prepare请求时,acceptor接受(accept)编号为n的提案。

现在已经可以提出完整的算法了。

决议的提出与批准

通过一个决议分为两个阶段:

  • prepare阶段:
    • proposer选择一个提案编号n并将prepare请求发送给acceptors中的一个多数派;
    • acceptor收到prepare消息后,如果提案的编号大于它已经回复的所有prepare消息,则acceptor将自己上次接受的提案回复给proposer,并承诺不再回复小于n的提案;
  • 批准阶段:
    • 当一个proposer收到了多数acceptors对prepare的回复后,就进入批准阶段。它要向回复prepare请求的acceptors发送accept请求,包括编号n和根据P2c决定的value(如果根据P2c没有已经接受的value,那么它可以自由决定value)。
    • 在不违背自己向其他proposer的承诺的前提下,acceptor收到accept请求后即接受这个请求。

这个过程在任何时候中断都可以保证正确性。例如如果一个proposer发现已经有其他proposers提出了编号更高的提案,则有必要中断这个过程。因此为了优化,在上述prepare过程中,如果一个acceptor发现存在一个更高编号的提案,则需要通知proposer,提醒其中断这次提案。

实例

用实际的例子来更清晰地描述上述过程:
有A1, A2, A3, A4, A5 5位议员,就税率问题进行决议。议员A1决定将税率定为10%,因此它向所有人发出一个草案。这个草案的内容是:

1
现有的税率是什么?如果没有决定,则建议将其定为10%.时间:本届议会第3年3月15日;提案者:A1

在最简单的情况下,没有人与其竞争;信息能及时顺利地传达到其它议员处。
于是, A2-A5回应:

1
我已收到你的提案,等待最终批准

而A1在收到2份回复后就发布最终决议:

1
税率已定为10%,新的提案不得再讨论本问题。

这实际上退化为二阶段提交协议。
现在我们假设在A1提出提案的同时, A5决定将税率定为20%:

1
现有的税率是什么?如果没有决定,则建议将其定为20%.时间:本届议会第3年3月15日;提案者:A5

草案要通过侍从送到其它议员的案头. A1的草案将由4位侍从送到A2-A5那里。现在,负责A2和A3的侍从将草案顺利送达,负责A4和A5的侍从则不上班. A5的草案则顺利的送至A3和A4手中。

现在, A1, A2, A3收到了A1的提案; A3, A4, A5收到了A5的提案。按照协议, A1, A2, A4, A5将接受他们收到的提案,侍从将拿着

1
我已收到你的提案,等待最终批准

的回复回到提案者那里。
而A3的行为将决定批准哪一个。

情况一

假设A1的提案先送到A3处,而A5的侍从决定放假一段时间。于是A3接受并派出了侍从. A1等到了两位侍从,加上它自己已经构成一个多数派,于是税率10%将成为决议. A1派出侍从将决议送到所有议员处:

1
税率已定为10%,新的提案不得再讨论本问题。

A3在很久以后收到了来自A5的提案。由于税率问题已经讨论完毕,他决定不再理会。但是他要抱怨一句:

1
税率已在之前的投票中定为10%,你不要再来烦我!

这个回复对A5可能有帮助,因为A5可能因为某种原因很久无法与与外界联系了。当然更可能对A5没有任何作用,因为A5可能已经从A1处获得了刚才的决议。

情况二

依然假设A1的提案先送到A3处,但是这次A5的侍从不是放假了,只是中途耽搁了一会。这次, A3依然会将”接受”回复给A1.但是在决议成型之前它又收到了A5的提案。这时协议有两种处理方式:
1.如果A5的提案更早,按照传统应该由较早的提案者主持投票。现在看来两份提案的时间一样(本届议会第3年3月15日)。但是A5是个惹不起的大人物。于是A3回复:

1
我已收到您的提案,等待最终批准,但是您之前有人提出将税率定为10%,请明察。

于是, A1和A5都收到了足够的回复。这时关于税率问题就有两个提案在同时进行。但是A5知道之前有人提出税率为10%.于是A1和A5都会向全体议员广播:

1
税率已定为10%,新的提案不得再讨论本问题。

一致性得到了保证。

  1. A5是个无足轻重的小人物。这时A3不再理会他, A1不久后就会广播税率定为10%.
情况三

在这个情况中,我们将看见,根据提案的时间及提案者的权势决定是否应答是有意义的。在这里,时间和提案者的权势就构成了给提案编号的依据。这样的编号符合”任何两个提案之间构成偏序”的要求。

A1和A5同样提出上述提案,这时A1可以正常联系A2和A3; A5也可以正常联系这两个人。这次A2先收到A1的提案; A3则先收到A5的提案. A5更有权势。

在这种情况下,已经回答A1的A2发现有比A1更有权势的A5提出了税率20%的新提案,于是回复A5说:

1
我已收到您的提案,等待最终批准。

而回复了A5的A3发现新的提案者A1是个小人物,不予理会。

A1没有达到多数,A5达到了,于是A5将主持投票,决议的内容是A5提出的税率20%.

如果A3决定平等地对待每一位议员,对A1做出”你之前有人提出将税率定为20%”的回复,则将造成混乱。这种情况下A1和A5都将试图主持投票,但是这次两份提案的内容不同。

这种情况下, A3若对A1进行回复,只能说:

1
有更大的人物关注此事,请等待他做出决定。

另外,在这种情况下, A4与外界失去了联系。等到他恢复联系,并需要得知税率情况时,他(在最简单的协议中)将提出一个提案:

1
现有的税率是什么?如果没有决定,则建议将其定为15%.时间:本届议会第3年4月1日;提案者:A4

这时,(在最简单的协议中)其他议员将会回复:

1
税率已在之前的投票中定为20%,你不要再来烦我!

决议的发布

一个显而易见的方法是当acceptors批准一个value时,将这个消息发送给所有learner。但是这个方法会导致消息量过大。
由于假设没有Byzantine failures,learners可以通过别的learners获取已经通过的决议。因此acceptors只需将批准的消息发送给指定的某一个learner,其他learners向它询问已经通过的决议。这个方法降低了消息量,但是指定learner失效将引起系统失效。

因此acceptors需要将accept消息发送给learners的一个子集,然后由这些learners去通知所有learners。
但是由于消息传递的不确定性,可能会没有任何learner获得了决议批准的消息。当learners需要了解决议通过情况时,可以让一个proposer重新进行一次提案。注意一个learner可能兼任proposer。

Progress的保证

根据上述过程当一个proposer发现存在编号更大的提案时将终止提案。这意味着提出一个编号更大的提案会终止之前的提案过程。如果两个proposer在这种情况下都转而提出一个编号更大的提案,就可能陷入活锁,违背了Progress的要求。这种情况下的解决方案是选举出一个leader,仅允许leader提出提案。但是由于消息传递的不确定性,可能有多个proposer自认为自己已经成为leader。Lamport在The Part-Time Parliament一文中描述并解决了这个问题。