分布式一致性(1):Paxos真的很简单!

Leslie Lamport老爷子说,Paxos很简单,简单的不能再简单🐶 我敢说,简单到全世界搞计算机起码有90%的人都得怀疑自己的智商。

“The Paxos algorithm, when presented in plain English, is very simple.” —— Leslie Lamport

为了让自己保持在自己的10%之内,最近几天又重新来学习一下Paxos。回头来看,Paxos made simple的确很简单地描述了Paxos算法。本文即记录理解此文的一些思考。

Paxos共识算法

共识问题

共识问题就是一组程序需要能够为选择某个值达成共识。首先来说,需要有至少一个提议(proposal),然后各程序通过类似投票的方式进行选举,达成共识后,这个提议的值被称为选中(chosen)。为了保证这个过程是完备的,需要遵循几个安全性要求(safety requirements):

FLP不可能原理(FLP Impossibility)已经证明,在异步的网络模型(即无超时边界)下,不存在一种共识算法能够完全满足下面三个条件:

但是如果牺牲掉一些特性(比如3选2)或者使用同步的情况下,是可以实现一致性算法的。Paxos中假定遵循异步、非拜占庭容错(non-Byzantine)的网络模型:

Paxos中,将参与者区分为提议者(proposers)、接受者(acceptors)、学习者(learners)。简单来说,提议者可以提出待选择的提议;接受者决定选择哪一个提议;学习者能够获悉被选中的值。

一般情况下为了简化这个过程,通常让一个分布式节点即是提议者、又是接受者。学习者不是必须的。

确定选取的提议

在Paxos made simple文中,作者通过一步一步从简单到复杂的推理过程,来得出一个最终的算法,让人印象深刻。

从最简单的情况开始。最简单的情况是,只有一个接受者,并且选择接受到的第一个提议。这样肯定可以达成一致,但是一旦这个接受者挂掉,那么就不可能会选取一个值。因此,在分布式共识算法中,需要有多个接受者,只有其中的大多数接受者都选取了一个值,才认为其被选中(chosen)。为了达成“大多数”的情况,节点总数必须为奇数𝑛=2𝑘+1。

这种方式下,接受者如果最多只能接受一个值,那么可以形成一个多数派,从而达成共识。考虑到消息可能丢失或者节点失败,节点是无法预测是否会有第二个消息到来,那么假设只有一个提议者,提交了一个提议,这个提议一定需要被接受。否则这种情况下,无法达成共识。这种方式被归纳为:

𝑃1: 接受者必须接受其第一次接收到的提议。 An acceptor must accept the first proposal that it receives.

这种方式存在的问题是,有可能会有多个提议者几乎同时发起了提议,而接受者分别接受到了不同的提议,假定有5个接受者、三个提议:

这种情况下,提议1、2、3没有一个形成多数派(至少3个接受者接受),因此无法达成一致。即便只有2个提议者1、2,一旦E挂掉,提议1、2也无法达成一致。因此,可以推论出,接受者必须要允许接受多个提议。

为了方便表示,将提议进行编号(以自然数正序),以<𝑛,𝑣>代表编号为𝑛的提议值为𝑣。没一个不同的提议其编号是不一样的(即便是两个提议值相同,也是不同的提议,其编号一定不相同)。在这种方式表示的情况下,一个值𝑣被选中的条件是,有一个值为𝑣的提议被多数派接受。

为了满足𝑃1,同时又能够支持接受者多次接受,那么推导出𝑃2:

𝑃2: 如果一个值为v的提议被选中,那么其他被选中的更高序号的提议值必然为v。 If a proposal with value v is chosen, then every higher-numbered proposal that is chosen has value v.

这个推论看起来没什么问题,但却困扰了我一段时间:既然v已经选中,又何来再选中之说呢?我们这可是basic paxos阿!选中一个值,不就结束了么?

应该这么来看待:选中首先是要站在事实的角度来看(即以上帝视角来审视),如果超过半数的接受者接受了某个提议,那么它事实上已经被选中。这是从所有接受者的角度来看的,但对于观察者(提议者)而言,是不能保证他们一定能够获悉这个结论的。提议者通过将自己的提议发送给所有的接受者,并从结果中判断是否有超过半数的接受者接受,来判断自己的提议是否被接受。

    比如两个提议者(A,B)分别提议了1,2,acceptor一定只能接受一个,
    比如1被多数接受者接受,那么<1, v1>就被选中了。
    但是假设返回给提议者的消息丢失了,对于提议者A无法确定自己的提议是否被接受;
    从而需要再发起一次投票
    但是此时,B已经获悉自己的提议被接受。
            (Acceptor 1)  (Acceptor 2)  (Acceptor 3)
   ________ ___________  ____________  _______________
   <1, v1>  | accept      | accept      |   reject 
   <2, v2>  |   reject    |   reject    | accept

因为消息可能丢失,或者节点挂掉重启。因此,当这样的情况发生时,观察者需要重新发起提议,向所有的接受者重新发起提议,然后再判断是否选中。

因此,“选中”这个事件,不论是在观察者角度、还是接受者角度,都可能发生多次;只有保证他们全部是一致的,才能满足安全性需求。所以说,即使有多个提议被事实上选中,那么他们的值必然要一致,才能保证对于所有的观察者而言,选中了一个唯一的值。

那么如何才能保证𝑃2呢?𝑃2说,如果𝑣被选中,那么更高编号的提议被选中必然值为𝑣;本来“选中”只需要超过半数的接受者接受即可,但是我们可以用一个更严格的条件来满足它:如果𝑣被选中,后续所有接受者接受的更高编号的提议必然值为𝑣。这个约束比𝑃2更加严格,也能够保证𝑣被选中,因此得到:

𝑃2𝑎: 如果一个值为𝑣的提议被选中,那么其他被任意接受者接受的的更高序号的提议值都为𝑣。 If a proposal with value v is chosen, then every higher-numbered proposal accepted by any acceptor has value v.

P2a必须跟P1一起才能保证正确,因为𝑃2𝑎无法适用只有一次提议的场景。而因为是异步模型,可能存在一种场景是,某个接受者接受到的第一次提议,对于其他接受者而言不是第一次了(比如一个接受者挂掉,进行第二轮投票的时候才恢复)。对于其他接受者,可以只接受自己曾经接受过的值;但是对于这个接受者,按照𝑃1,它必需要接受这个提议,无论其值是什么。这样就可能出现与𝑃2𝑎冲突的情况。解决的办法也很简单,既然值是由提议者决定的,那么我们要求一旦值被选中后,提议者提出的更高序号的提议的值必须为𝑣:

𝑃2𝑏: 如果一个值为v的提议被选中,那么任意提议者提出的任意更高序号的提议值都为v。 If a proposal with value v is chosen, then every higher-numbered proposal issued by any proposer has value v

𝑃2𝑏是比𝑃2𝑎更严格的条件,因此𝑃2𝑏可以满足𝑃2𝑎,当然也满足𝑃2。那么如何做才能满足𝑃2𝑏这个条件呢?通过数学归纳法,可以证明:假设提议<𝑚,𝑣>被选中,那么对于任意提议<𝑛,𝑣𝑛>(𝑛>𝑚)必定有𝑣𝑛=𝑣。

数学归纳法分为两步:

那么,如果应用到上述的条件,可以有:

基础步骤: 为m的时候,<𝑚,𝑣>被选中是预设的前提,无需证明 归纳假设:假设提议𝑚..(𝑛−1)的值都为𝑣。 证明目标:现在要证明对于提议𝑛,其值也为𝑣。

为了完成这一目标,还需要先增加一个提议的条件𝑃2𝑐:

𝑃2𝑐: 对于任意的提议<𝑛,𝑣>被提出的条件是,存在一个多数派集合𝑆要么满足(a):这个集合中没有一个接受者接受过任意小于𝑛的提议(也就是说第一次接受提议);要么满足(b):所有接受者中,其接受的小于𝑛的最大编号提议的值为𝑣。

因为<𝑚,𝑣>已经选中,所以一定存在一个多数派𝐶,其中所有的接受者都接受了提议𝑚。从而推断出:

𝐶中的所有接受者都接受了一个𝑚..(𝑛−1)的提议(至少接受了𝑚),且其中所有提议的值均为𝑣(从假设可以得出)。

对于任意一个多数派集合𝑆,一定与𝐶至少有一个交集,因此条件(a)不可能成立;而根据上面的推论,条件(b)是满足的。从而,一个新的提议(𝑛)的值必然也为𝑣。

从而,证明了𝑃2𝑏。

要满足𝑃2𝑐,提议者在提议𝑛之前,必须要获悉当前小于𝑛的最高提议的值,为了能够得到确定的结果,要求接受者不能再接受任何小于𝑛的提议,并得到如下的一个算法:

  1. 提议者准备提交𝑛,首先发送一个准备请求(𝑝𝑟𝑒𝑝𝑎𝑟𝑒(𝑛))给接受者,要求其: = (a) 承诺不再接受任何小于n的提议

      1. 返回其接受的小于𝑛的最大提议(如果有的话)
  2. 如果提议者从多数接受者获得的提议值为𝑣,那么提议者可以提议<𝑛,𝑣>;或者没有从接受者获悉已经接受的提议,那么提议者可以提议任意的值<𝑛,𝐴𝑛𝑦>。这个过程被称为接受请求(𝑎𝑐𝑐𝑒𝑝𝑡(𝑛))

对于接受者而言,可以响应任意的𝑝𝑟𝑒𝑝𝑎𝑟𝑒请求,而对于𝑎𝑐𝑐𝑒𝑝𝑡请求,必须要保证:

𝑃1𝑎: 接受者当且仅当在没有响应大于𝑛的𝑝𝑟𝑒𝑝𝑎𝑟𝑒请求的时候,才可以接受𝑛。 An acceptor can accept a proposal numbered n iff it has not responded to a prepare request having a number greater than n

很明显,𝑃1𝑎是包含𝑃1的。最后,加入了一个小的优化。如果接受者已经响应了𝑝𝑟𝑒𝑝𝑎𝑟𝑒的请求且大于𝑛,那么接受者无论如何也不会再接受提议𝑛了,因此不需要响应𝑝𝑟𝑒𝑝𝑎𝑟𝑒(𝑛)和𝑎𝑐𝑐𝑒𝑝𝑡(𝑛);同样对于已经接受的提议的𝑝𝑟𝑒𝑝𝑎𝑟𝑒请求也不需要响应。

最终,完整的算法如下:

是不是真的很简单?

Ref: