Paxos是啥,引用大牛Leslie Lamport的论文《Paxos Make Simple》的一句话:
In fact, it is among the simplest and most obvious of distributed algorithms.
At its heart is a consensus algorithm—the “synod” algorithm of “The part-time parliament”.
因此,Basic Paxos是一个共识(consensus)算法,用于解决分布式共识问题。其目的是在一个分布式系统中如何就某一个值(proposal) 达成一致。
在常见的分布式系统中,总会发生诸如机器宕机或网络异常(包括消息的延迟、丢失、重复、乱序,还有网络分区)等情况。Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。
具体地,对于分布式共识问题,很多进程提出(propose)不同的提案(Proposal,最终要达成一致的value就在提案里),共识算法保证最终只有其中一个值被选定,Safety表述如下:
Paxos以这几条约束作为出发点进行设计,只要算法最终满足这几点,正确性就不需要证明了。
Paxos算法中共分为三种角色:
在具体的实现中,一个进程可能同时充当多种角色。比如一个进程可能既是Proposer又是Acceptor又是Learner。实际上,通常实现中每个进程都同时扮演这三个角色。这些角色之间在异步的非拜占庭场景下(the customary asynchronous, non-Byzantine model),通过发送消息的方式来相互通信。
假设只有一个acceptor(多个proposer),只要acceptor接受它收到的第一个提案,则该提案被选定,该提案里的value就是被选定的value。这样就保证只有一个value会被选定。
但是一个共识模块最关键的特性是:对于一个系统来说,只要有大多数的服务器是可用的,那么它就可以提供所有的服务。所以如果我们有一个 5 台服务器的集群,那么它可以在仅有 3 台服务器可用的情况下,仍然能正常提供服务。所以我们可以容忍 5 台其中的 2 台宕掉。通常情况下,集群的大小会是一个奇数,如 3、5 或 7 。
因此在该方案下,如果这个唯一的acceptor宕机GG了,那么系统就废了,不满足上述要求。So,必须要有多个acceptor,通常是一个奇数,如 3 、5 或 7 。如果一个值被大多数 接受者acceptors选定,那么我们认为这个值被认为是选定的。这样即使在少数服务器崩溃的情况下,还有多数服务器可以接受值。仲裁(quorum)方法可以让我们在某些服务器崩溃后,仍然能保证集群能正常工作。
现在问题变成了:如何在多个proposer和多个acceptor的情况下选定一个value。
proposers向acceptors提出proposal,为了保证最多只有一个值被选定(chosen),proposal必须被超过一半的acceptors(majority)所接受(accept),且每个acceptor只能接受一个值。由于任意两个majority的acceptors至少有一个公共 成员,因此如果每一个acceptors只能批准一个提案的话,那么就能保证只有一个提案被选定了。
因为消息是有可能丢失的,因此,当只有一个value被提出的时候,acceptor应该接受它,这暗示了如下的需求:
P1. An acceptor must accept the first proposal that it receives.
一个acceptor必须批准它收到的第一个提案。
但单单这个会导致其他问题:如果有多个提案被不同的proposer同时提出,这可能会导致虽然每个acceptor都批准了它收到的第一个提案,但是没有一个提案是由大部分acceptor批准的,如下图。
例如,我们假设每个acceptor都接受它第一次收到的值,然后让多数票的值获胜,如上图我们可以看到,也存在没有任何值是大多数的情况。服务器 S1 和 S2 接受的值是 red ,服务器 S3 和 S4 接受的值是 blue ,服务器 S5 接受的值是 green 。没有任何值在五个服务器中的三个达成一致的。这也就意味着acceptors有时需要改变他们的想法,在某些情况下,它们接受了一个值后需要接受另外一个不同的值。也就是说,几乎无法在一轮投票下就能达成一致,往往需要进行几轮的投票才能达到一致。这里接受(accepted)并不代表被选定(chosen),一个值只有在集群大多数节点接受之后才被认为是选定的。
但走到这里又会造成另外的问题,导致违背了Safety里的要求:
所以总结如下:我们需要一个两段协议(two-phase protocol)。在发起请求前先进行检查,然后我们需要请求有序,这样就能消除老的请求。
显然,既要满足Safety里只能选定一个值的要求,又要满足一个acceptor必须能够批准不止一个提案,且要求提案有序,那么【提案=value】已经不能满足需求了,于是需要给每个提案再加上一个提案编号,表示提案被提出的顺序。令【提案=提案编号+value】,这样就能满足上述几个条件啦。proposer生成全局唯一且递增的提案 ID(Proposalid,例如以高位时间戳 + 低位机器 IP 可以保证唯一性和递增性)。
通过上述经历,我们虽然允许多个提案被选定,但同时必须要保证所有被选定的提案都具有相同的value值,则需要提案value进行约束,如下:
P2. If a proposal with value v is chosen, then every higher-numbered proposal that is chosen has value v.
如果编号为M0、value值为V0的提案(即[M0,V0])被选定了,那么所有比编号M0更高的,且被选定的提案,其value值必须也是V0。
因为提案的编号是全序的,P2就满足了Safety里只能选定一个值的要求。同时,一个提案要被选定(chosen),其首先必须被至少一个acceptor批准,因此可以满足如下条件进而来满足P2.
P2a. If a proposal with value v is chosen, then every higher-numbered proposal accepted by any acceptor has value v.
如果编号为M0、value值为V0的提案(即[M0,V0])被选定了,那么所有比编号M0更高的,且被Acceptor批准的提案,其value值必须也是V0。
由于通信是异步的,一个提案可能会在某个acceptor还未收到任何提案时就被选定了,如下图:
假设有 5 个 Acceptor。proposer2 提出 [M1,V1]的提案,acceptor2 ~ 5(半数以上)均接受了该提案,于是对于 acceptor2~5 和 proposer2 来讲,它们都认为 V1 被选定。acceptor1 刚刚从 宕机状态 恢复过来(之前 acceptor1 没有收到过任何提案),此时 Proposer1 向 Acceptor1 发送了 [M2,V2] 的提案 (V2≠V1且M2>M1)。对于 acceptor1 来讲,这是它收到的 第一个提案。根据 P1(一个 acceptor 必须接受它收到的第一个提案),acceptor1 必须接受该提案,但又与P2a矛盾。因此如果要同时满足P1和P2a,需要对P2a进行如下强化:
P2b. If a proposal with value v is chosen, then every higher-numbered proposal issued by any proposer has value v.
如果编号为M0、value值为V0的提案(即[M0,V0])被选定了,那么之后任何proposer产生的编号更高的提案,其value值必须也是V0。
因为一个提案必须在被proposer提出后才能被acceptor批准,因此P2b包含了P2a,进而包含了P2。
更进一步地,为了满足P2b,还需要保持下面P2c的不变性。
P2c. For any v and n, if a proposal with value v and number n is issued, then there is a set S consisting of a majority of acceptors such that either (a) no acceptor in S has accepted any proposal numbered less than n, or (b) v is the value of the highest-numbered proposal among all proposals numbered less than n accepted by the acceptors in S.
对于任意的Mn和Vn,如果提案[Mn,Vn]被提出,那么肯定存在一个由半数以上的acceptor组成的集合S,满足以下条件中的任意一个。
- S中不存在任何批准过编号小于Mn的提案的acceptor。
- 选取S中所有acceptor批准的编号小于Mn的提案,其中编号最大的那个提案其value值是Vn。
至此,只需要通过保持P2c,就能够满足P2b了,而满足P2b,就能够满足P2了。通过P1和P2来保证一致性。
通过上述P2及一系列扩展,可以自然地引出如下的提案生成算法。
在确定提案之后,proposer就会将该提案再次发送给某个acceptor集合(同样需要满足majority),并期望获得它们的批准,该请求称之为accept请求。需要注意的是,这里的acceptor集合不一定是之前相应prepare请求的acceptor集合,只需要满足majority即可,因为任意两个半数以上的acceptor集合,必定包含至少一个公共acceptor。
根据proposer的生成提案流程,一个acceptor可能会收到来自proposer的两种请求,分别是prepare请求和accept请求。Paxos算法允许acceptor可以忽略任何请求(包括Prepare请求和Accept请求)而不用担心破坏算法的安全性,对acceptor接受提案给出如下约束:
P1a. An acceptor can accept a proposal numbered n iff it has not responded
to a prepare request having a number greater than n.
一个acceptor只要尚未相应过任何编号大于Mn的prepare请求,那么它就可以接受这个编号为Mn的提案。
因此acceptor批准提案流程为:
把上述proposer生成提案以及对应的acceptor批准提案合起来,就是Basic Paxos算法了。以一张图作为总结吧。
Prepare阶段:
Accept阶段:
显然,为了保证算法在容灾(节点故障重启)场景下的正确性,acceptor上需要持久化(minProposal、acceptedProposal 、acceptedValue )。
大体上有三种方案:
竞争提议可能会导致死锁。
假设有两个proposer依次提出编号递增的提案,最终谁都不服谁,会陷入死循环,没有提案被选定,从而无法保证算法的活性。
如图,假设服务器 S1 成功接收到请求,并处于准备阶段(P 3.1)。在接受值 X 之前(A 3.1 X),另外一个服务器 S5 正处于它的准备阶段(P 3.5),这会阻止前序值的接受(A 3.1 X)。然后 S1 会重新选择提议序号并再次开始提议过程(P 4.1),假设它正进入了第二轮的准备阶段,在接受值之前,服务器 S5 正试图完成接受值的选定 Y (A 3.5 Y),不过此时因为(P 4.1)的序号高于(A 3.5 Y),所以它阻止了(A 3.5 Y)的接受,这样 S5 的提议就失败了,然后 S5 又重新开始下一轮的提议,如此往复,这个过程会无限循环下去。
为了不发生死锁,Paxos 需要以某种补充机制来保证它可以正确运行。
以上。
后续有机会再把Multi-Paxos以及相关开源实现PhxPaxos源码分析补上呢,有点懒的写呢。