背景
分布式系统的可靠性指的是当分布式系统中一台或部分机器宕掉后都不会导致系统不可用。对于无状态服务,水平扩展即可。但对数据服务的分布式系统,则一般采取replicate的方式,每个节点都可能是其他节点的快照,这是保证分布式系统高可靠性的关键。
然而,而存在多个复制节点就会存在数据不一致的问题,这时一致性就成了分布式系统的核心。
paxos目标
针对上述背景,Paxos算法解决的问题就是在一个可能发生通信异常的分布式系统中如何就某个值达成一致。
通信问题
分布式多个节点就会存在节点间通信的问题,存在着两种节点通讯模型:共享内存(Shared memory)、消息传递(Messages passing).基于消息传递通信模型的分布式系统,不可避免地会发生以下错误:进程可能会慢、crash、重启,消息可能会延迟、丢失、重复。这里不考虑拜占庭将军问题。
推导过程
为了理解paxos算法,我们从具体问题场景出发,进行模型定义与抽象,再一步步解决问题,最终推导出算法的内容。
具体问题场景
考虑一个Key-Value集群中,多个client并发更新某个key为不同value的场景。
每个节点是对等的,这里client请求的正好是集群中不同的node,再把数据复制到其他node。 nodeA收到请求顺序分别为set value 1,3,5;nodeB收到请求顺序分别为set value 3,5,1。 针对这种场景,不同node收到的请求顺序是不一致的,但需要保证不同node执行的操作序列一致,才能保证一致性。
推广一下也就是,在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点都执行相同的操作序列,那么他们最后能得到一个一致的状态。为保证每个节点执行相同的命令序列,需要在每一条指令上**执行一个“一致性算法”**以保证每个节点看到的指令一致。
问题模型抽象
对上述具体问题抽象出一个问题模型,即在分布式系统中,针对一组proposer提出的提案,通过一组acceptor的投票后选定提案中的一个value。
角色说明
- Proposer:提案发起者
- Acceptor:提案接受者
- proposal:提案,包含具体value
当nodeA收到client发送的set value = 1的指令,nodeA就作为一个proposer,向所有的acceptor(其他2个node + 自己)发起提案,即设置某个key为某个值,希望大家都把value = 1这条数据写到3个node上面;每个node同时充当了2个角色:Proposer/Acceptor。
要求
- value只有在被 proposers 提出的 proposal中时 才能被最终选定;
- 只选定一个最终决议 (决议即被选定的value)
这两点即安全性(safety)的要求,另外还有活性(liveness)的要求,在下文后续讨论。
解决问题
简单方案——只有一个Acceptor
假设只有一个Acceptor(可以有多个Proposer),只要Acceptor接受它收到的第一个提案,则该提案被选定,该提案里的value就是被选定的value,这样就保证只有一个value会被选定。或者采用多个Proposer通过互斥方式访问这个Acceptor,但还涉及加锁后proposer宕机导致锁未释放问题。
考虑Acceptor宕机
但是,如果这个上述方案中唯一的Acceptor宕机了,那么整个系统就无法工作了!因此,必须要有多个Acceptor!
那么,如何保证在多个Proposer和多个Acceptor的情况下选定一个value呢?
首先我们知道的是需要确保这些提案中有一个被选中, 那么也就意味着即使只有一个提案我们也要有提案被选中, 那么很自然地我们可以做出如下的约束来满足这个需要:
P1:一个Acceptor必须接受它收到的第一个提案。
但是,这又会引出另一个问题:如果每个Proposer分别提出不同的value,发给不同的Acceptor。根据P1,Acceptor分别接受自己收到的value,那会出现不同Acceptor接受了不同value,怎么选定一个value呢?
于是,就提出了一个大多数(majority)的概念, 具体来讲就是被超过一半的acceptor接受的提案可以被选中。所以规定:一个提案被选定需要被半数以上的Acceptor接受 。
这个规定又暗示了:『一个Acceptor必须能够接受不止一个提案!』不然可能无法形成多数派,导致最终没有value被选定。因为每个提案到达acceptor的顺序是不确定的, 如果提案数量大于1,就可能无法形成多数派。比如上图的情况,v1、v2、v3都没有被选定,因为它们都只被一个Acceptor的接受。
现在,每个acceptor都可以接受多个提案, 为了后续描述方便我们给每个提案一个全局唯一递增的编号, 表示提案被提出的顺序,这样提案就由编号和值共同构成(我们最终的目标是选出值),令**『提案=提案编号+value』**。
再看看上文中说到的safety要求,是说要选定一个value,但并没有要求只能选定一个提案,暗示可能存在选定多个提案。只要提案的 value 是一样的,选定多个提案不违背safety的要求。所以必须保证所有被选定的提案都具有相同的value值。
于是有了下面的约束:
P2:如果某个value为v的提案被选定了,那么每个编号更高的被选定提案的value必须也是v。
又一个提案只有被Acceptor接受才可能被选定,因此我们可以把P2约束改写成对Acceptor接受的提案的约束P2a。
P2a:如果某个value为v的提案被选定了,那么每个编号更高的被Acceptor接受的提案的value必须也是v。
只要满足了P2a,就能满足P2。
但是由于通信是异步的,P2a 和 P1 会发生冲突。冲突的场景如下,如果一个 value 被选定后,一个 proposer 和一个 acceptor 从休眠中苏醒,这个 proposer提出一个具有新的 value 的提案。根据 P1,acceptor应当接受,根据 P2a,则不应当接受。这种场景下 P2a 和 P1 有矛盾。
即一个proposal[m, v]在已经被选定之后可能还有某个acceptor a还没有接收到任何提案, 这个时候如果另一个proposer提出了一个版本更高的提议: [m+k, v2], 而且如果[m+k, v2]是a收到的第一个提案, 根据P1的约束, a就必须接受它. 这样就与P2a矛盾了。
解决矛盾
换个思路,P2a是对Acceptor接受的提案约束,但其实提案是Proposer提出来的,所以我们可以对Proposer提出的提案进行约束,如看到如果上面的例子中v2=v的话, P2a还是可以成立的。所以转而对 proposer 的行为进行约束:
P2b:如果某个value为v的提案被选定了,那么之后任何Proposer提出的编号更高的提案的value必须也是v。
由P2b可以推出P2a进而推出P2。那么现在的问题是如何保证P2b, 即尝试证明P2b成立, 然后在这个过程中找到可以满足P2b的充分条件.
假设一个编号为 m 的 value v 已经被选定,来看看在什么情况下对任何编号为 n(n>m)的提案 value为 v。
转化为证明题:假设我们有[m, v]被选定, 那么我们现在要证明的是对于n>m, [n, v’]可以被选定必须满足v’=v. 我们采用第二数学归纳法来证明。
过程如下:
因为[m, v]被选定, 所以必然存在一个超过半数的(majority) acceptor的集合C, C中的每个acceptor都接受了该提案; …… (1)
假设编号在 m…(n-1)的提案时命题成立,则他们提案的值value都是v ;…… (2)
那么需要推理出N=n时,[n, v’] 可以被选定必须满足v’=v . …… (3)
由(1),(2)我们可以得出C中的每个acceptor都接受了m…(n-1)之间的某个提案, 且这个被接受的提案值为v.
要满足[n, v’] 可以被选定,那么需要一个majority S接受该提案。
因为C是majority, 所以它和另外任意一个majority S一定会有一个非空的交集. 那么我们可以确定, 对于任意一个majority S, 它里面肯定存在acceptor接受了m…(n-1)之间的某个提案, 且这个被接受的提案值为v.
据此我们可以得出让P2b满足的条件,即P2c约束:
P2c. 对于任意的n和v, 如果[n, v]被提出, 那么肯定存在一个由半数以上(majority)的acceptor组织的集合S, 满足下面两个条件之一
(a) S中不存在任何接受过编号小于n的提案的Acceptor
or (b) S中所有Acceptor接受的编号小于n的提案中编号最大的提案的value为v
这里的可以看到(b)就是我们上面推导出来的情况,即对于任意一个majority S, 它里面肯定存在acceptor批准了m…(n-1)之间的某个提案, 且这个被批准的提案值为v。 那么对于情况(a)怎么理解? 实际上我们的推导是基于P2b的”如果值为v的提案被选中…”, 那如果没有编号小于n的提案被选中呢? 也就是没有限制,即v’为任何值都行。
小结:
只要满足P2c就可以满足P2(P2c => P2b => P2a => P2), 进而只要满足P1&P2c, 我们就可以确保该算法的安全性. 之所以我们需要把P2限定到P2c, 是因为P2还不够具体, 不足以指导我们设计出一个算法. 而P2c就能为具体算法定义奠定基础了。另外P1& P2c实际上只是充分条件而不是必要条件, 实现一致性算法也不是只有这一条路可以走.
充分条件的证明
上面我们找到了满足P2b的充分条件P2c,我们再严格证明下P2c => P2b。也就是需要证明如下命题:
如果P2c成立,即对于任意的n和v, 如果[n, v]被提出, 那么肯定存在一个由半数以上(majority)的acceptor组成的集合S, 满足下面两个条件之一:(a) S中不存在任何接受过编号小于n的提案的Acceptor; (b) S中所有Acceptor接受的编号小于n的提案中编号最大的提案的value为v。那么,如果某个value为v的提案被选定了,则之后任何Proposer提出的编号更高的提案的value必须也是v。
采用第二数学归纳法证明:这里P2c成立和某个value为v的提案被选定属于2个条件,编号更高的提案的value必须也是v 是需要证明的结论。证明如下:
当提案为[n,v]的value v被选定时,说明存在一个大多数acceptor集合S 接受了提案[n,v]。若编号为x=n+1的提案[n+1, w]被提出,根据P2c条件,则存在一个大多数acceptor集合S2满足下面两个条件之一:(条件a) S2中不存在任何接受过编号小于x的提案的Acceptor,或(条件b) S2中所有Acceptor接受的编号小于x的提案中编号最大的提案的value为w。假设w!=v,因为集合S2与S至少存在一个公共的acceptor,这个acceptor不可能既满足接受了提案[n,v],又满足条件a或b,所以假设不成立,w必须为v,即编号为x=n+1时命题成立;……(1)
假设编号x=(n+1)…(k-1)之间的提案时命题成立,则他们的value都是v,又v被选定,所以存在一个大多数acceptor集合S接受了value为v的提案;……(2)
当编号为k的提案[k, v2]被提出时,根据P2c,则存在一个大多数acceptor集合Q满足下面两个条件之一:(条件a) Q中不存在任何接受过编号小于k的提案的Acceptor,或(条件b) Q中所有Acceptor接受的编号小于k的提案中编号最大的提案的value为v2。假设v2!=v,因为集合Q与S至少存在一个公共的acceptor,根据(1)(2)可知这个公共的acceptor接受的提案编号在范围 n…(k-1)之间,所以不可能再满足条件a或b,即假设不成立。所以v2必须为v。……(3)
综合(1)(2)(3),可得P2c => P2b。
保证P2c成立
对proposer的要求
为了满足P2c,这里有个比较重要的思想:proposer生成提案之前,应该先去了解某个大多数集合中各个acceptor目前接受的小于n最高的提案。
这里的难点是目前接受的小于n最高的提案可能是变化的。 比如,某个acceptor接受了编号为n-2的提案[n-2, v1],当前proposer要生成提案n,它先知道了该acceptor目前接受的最高编号提案为n-2了,但是还有可能出现 在提案n之前的提案[n-1, v2]随后请求到达该acceptor,于是又接受了提案编号为n-1的提案[n-1, v2],即acceptor接受的小于n最高的提案接下来发生了变化, 这就影响到proposer提案内容如何提出value了。
所以,proposer通过向acceptor们索取承诺来解决这个问题。换句话说, proposer知道了acceptor目前接受的小于n最高的提案后,要求acceptor们不要再接受任何低于n的提案了.
上面这个阶段即通过一个『Prepare请求』实现的,即提案生成算法如下:
Proposer选择一个新的提案编号N,然后向某个大多数Acceptor集合(半数以上)发送请求,要求该集合中的每个Acceptor做出如下响应(promise)。
(a) 向Proposer承诺保证不再接受任何编号小于N的提案。
(b) 如果Acceptor已经接受过提案,那么就向Proposer响应已经接受过的编号小于N的最大编号的提案。
我们将该请求称为编号为N的Prepare请求。
如果Proposer收到了半数以上的Acceptor的响应,那么它就可以生成编号为N,Value为V的提案[N,V]。这里的V是所有的响应中编号最大的提案的Value。如果所有的响应中都没有提案,那么此时V就可以由Proposer自己选择。
生成提案后,Proposer将该提案发送给某个大多数Acceptor集合,并期望这些Acceptor能接受该提案。我们称该请求为Accept请求。(注意:此时接受Accept请求的Acceptor集合不一定是之前响应Prepare请求的Acceptor集合)
对acceptor的要求
acceptor可能收到两种请求,prepare和accept请求,对这两类请求做出响应的条件为:
- prepare请求:acceptor可以在任意时刻响应prepare request;
- accept请求:acceptor可以在不违背现有响应prepare request的承诺前提下, 响应(接受)任何accept request
所以,对Acceptor逻辑处理的约束为:
P1a:一个Acceptor只要尚未响应过任何编号大于N的Prepare请求,那么他就接受这个编号为N的提案。
很明显P1a => P1 (P1:一个Acceptor必须接受它收到的第一个提案。)。
因此,一个Acceptor只需记住:1. 已接受的编号最大的提案 2. 已响应的请求的最大编号。以便在故障或者重启的情况下可以正确恢复.
而对于proposer而言, 只要它可以保证不会产生具有相同编号的提案那么就可以丢弃任意的提案以及它所有的运行时的状态信息。
paxos算法内容
经过上面的推导,我们总结下Paxos算法的流程。 Paxos算法分为两个阶段。
阶段一:(prepare)
(a) Proposer选择一个提案编号N,然后大多数的Acceptor发送编号为N的Prepare请求。
(b) 如果一个Acceptor收到一个编号为N的Prepare请求,且N大于该Acceptor已经响应过的所有Prepare请求的编号,那么它就会将它已经接受过的编号最大的提案(如果有的话)作为响应反馈给Proposer,同时该Acceptor承诺不再接受任何编号小于N的提案。
阶段二:(accept)
(a) 如果Proposer收到半数以上Acceptor对其发出的编号为N的Prepare请求的响应,那么它就会发送一个针对[N,V]提案的Accept请求给大多数Acceptor。注意:V就是收到的响应中编号最大的提案的value,如果响应中不包含任何提案,那么V就由Proposer自己决定。这次的大多数Acceptor与阶段一的大多数Acceptor不一定相同。如果没收到足够多的回复,则需要重回到阶段一。
(b) 如果Acceptor收到一个针对编号为N的提案的Accept请求,只要该Acceptor没有对编号大于N的Prepare请求做出过响应,它就接受该提案。否则拒绝或不回应。
整体思路
可以简单描述为:Proposer先从大多数Acceptor那里学习提案的最新内容,然后根据学习到的编号最大的提案内容组成新的提案提交,如果提案获得大多数Acceptor的投票通过就意味着提案被通过。
算法流程
acceptor记录的是已接受的提案[编号,value],minProposal为已承诺过的最大编号,这里min个人理解是可以响应的提案编号的最小阈值。
示例
已有value选定,继续提案
如上图,提案[3.1,X]已经得到S1,S2,S3的接受,即value X被选定后,有新的提案在阶段一[4.5]到达S3时,S3会返回给proposer已接受的提案,于是在阶段二提案的value设置为X,最终都达成一致。
proposer学习到新的value,发起提案
如图,本来想提议value为Y的proposer通过阶段一学习到最新的提案内容,重新组织提案,达成一致。
阶段二失败,重回阶段一
上图中,提案[3.1,X]在阶段二到S3时,由于此前S3在提案[4.5]的阶段一prepare请求时进行了承诺,所以会拒绝[3.1,X],在过半acceptor接受提案[4.5,Y]后,Y被选定。被拒绝的proposer会重回阶段一重新学习最终达成一致。
算法活性(liveness)保证
活性liveness要求
- 决议总会产生,保证决议过程能在有限时间内完成。
- learners 最终能获得被选定的决议。
Learner学习(获取)被选定的value
在前面推导、算法形成的过程中,还有一个角色未介绍,那就是** learner角色**,即提案学习者。
learner需要最终获取到被选定的value,有如下几种模型:
保证最终能产生决议
活锁问题:
如下图所示,假设有两个proposer S1和S5,请求时序如下
- proposer S1向S1、S2、S3发起prepare请求 P3.1,阶段一完成得到大多数响应后;
- 在S1进入阶段二之前proposer S5向S3、S4、S5发起prepare请求 P3.5,同样也得到了大多数响应;
- proposer S1进入阶段二,发起accept请求,因为S3响应了P3.5,所以拒绝了,S1未得到过半的响应,重新进入阶段一,发起编号更高的prepare请求 P4.1,并得到大多数响应;
- proposer S5进入阶段二,发起accept请求,同样因为S3响应了P4.1,被拒绝后,未得到过半的响应,重新进入阶段一,发起编号更高的prepare请求 P5.5,并得到大多数响应;
- 两个proposer S1和S5重复步骤3、4,依次提出编号更高的提案,阶段二失败重回阶段一,一直运行下去,最终无法选定value产生决议。
活锁解决:
- 方案1:可通过 随机改变 Proposal编号的增长幅度 或者 使proposer随机某时间间隔后再发送新一轮提案 来解决。
- 方案2:选取一个主proposer,只有主proposer才能提出提案,自行google参考multi-paxos
总结
概述paxos
proposer提出一个提案前,首先要和大多数acceptors进行通信,获得他们进行的最新接受的提案(prepare过程),之后根据回收的信息决定这次提案的value,形成提案开始投票。当获得多数acceptors接受(accept)后,提案获得批准(chosen),再将这个消息告知learner。这个简略的过程经过进一步细化后就形成了Paxos算法。
这里所说的paxos为** basic paxos**算法。
算法两点要求
- 安全性要求(safety)
- value只有在被 proposers 提出的 proposal中时 才能被最终选定;
- 只选定(chosen)一个最终决议 (决议即被选定的value)
- 活性liveness要求
- 决议总会产生,保证决议过程能在有限时间内完成。
- learners 最终能获得被选定的决议。
提案编号如何实现全局唯一递增
如何产生唯一的编号呢?在《Paxos made simple》中提到的是让所有的Proposer都从不相交的数据集合中进行选择,例如系统有5个Proposer,则可为每一个Proposer分配一个标识j(0~4),则每一个proposer每次提出决议的编号可以为5*i + j(i可以用来表示提出议案的次数)
在实践过程中,可以用 时间戳 + 提出提案的次数 + 机器 IP
来保证唯一性和递增性。
名词解释补充
proposal:提案 = 提案的值 + 提案编号;
Acceptor对proposer有两个动作:promise和accept。
promise:Acceptor对Proposer承诺,不会批准任何编号小于它的proposal
accept(接受 ):Acceptor没有响应过比当前的proposal更大编号的proposal,就接受了该proposal
chosen(选定):当Acceptor的多数派都accept一个proposal时,该proposal就被最终选择,也称为决议
参考
blog.csdn.net/dellme99/ar… 全面 zh.wikipedia.org/wiki/Paxos%… 维基百科 en.wikipedia.org/wiki/Paxos_… 英文维基百科 ramcloud.stanford.edu/~ongaro/use… 本文示例部分的图来源于此