Paxos算法分析

时间:2021-07-30 07:36:07

作者:吴香伟 发表于 2014/09/30
版权声明:可以任意转载,转载时务必以超链接形式标明文章原始出处和作者信息以及版权声明

一致性问题

Paxos算法分析

如上图所示,服务器Ai(i=1,2,..5)组成存储集群,每份数据在5台服务器中各保留一个副本。当客户端C1和C2同时修改存储在集群中的同一份数据时,由于网络延迟的存在无法保证两个修改数据的请求到达每台服务器的先后顺序。也就是说,可能出现C1的请求先到达A1服务器,而C2的请求先到达A2服务器的情况。这种情况下,在A1和A2服务器上对数据的修改顺序不同,而修改顺序的不同直接导致了这两个副本数据的不一致。

要保持副本一致只要保证在每台服务器上对数据修改的操作顺序相同。保证修改操作的顺序相同的最简单方法是采用Primary-Secondary模式,所有对数据的修改操作都先发送到Primary主机,然后由Primary主机分发给Secondary主机,也就是说修改操作的顺序由Primary主机决定。但这种方法存在单点故障问题。另一种方法是选举。当一台主机接收到修改数据的请求时并不直接修改数据,而是发起一次选举,只有赢得选举的请求才可以修改数据,并且修改操作在所有主机上进行。也就是说修改操作的顺序由集群中的所有主机共同决定。举个例子,如果在A1接收到C1请求的同时,A2也接收到了C2的请求,这时它们同时发起选举以执行自己的修改操作,选举的结果只有一个请求被批准。假如C2请求得到批准,那么所有主机上先执行C2请求。A1选举失败后重新发起选举,在得到批准后,所有主机执行C1的请求。从而,通过两轮选举确定了C1和C2请求在所有主机上的执行顺序。

上面的第二个方法的关键是如何由集群中的所有节点共同选择一个值。

选择一个值(Value)

执行一次Paxos算法只能批准(chonsen)一个值,否则,仍旧无法确定值的先后顺序。
提案得到批准的条件是,存在一个多数派,它的每个成员都接受(Accept)这个提案。假定每个成员最多只能接受一个提案,那么如果不是多数派,就会出现一次Paxos算法批准多个值的问题。例如,假设只要有2个节点接受提案就批准提案,当A1、A2同时提出提案,A1和A3接受A1的提案,A2和A4接受A2的提案,因为它们都满足被批准的条件,所以A1和A2的提案都得到批准。 因为任意两个多数派之间至少存在一个公共的成员,所以可以保证得到多数派支持的提案是唯一的。继续上面的例子,假设A5接受了A1的提案,那么就不能再接受A2的提案,从而A1的提案得到多数派的支持获得批准。

P1.每个Acceptor必须接受它接收到的第一个提案
提出约束条件P1的原因是,我们希望在只有一个节点提出提案的情况下,这个提案可以得到批准。但P1会引入另外一个问题,就是可能无法形成多数派。例如,A1到A5每个节点在同一时刻提出提案,并且自己率先接受了提案。由于每个节点最多只能接受一个提案,因此每个提案都只有一个节点接受,无法形成多数派。为了可以形成多数派,放宽条件,让每个Acceptor节点可以接受多个提案。但这又会引入新问题,继续前面的例子,如果每个节点接受了所有接收到的提案,那么5个提案都会得到批准,从而又出现了执行一次算法批准多个提案的问题。

P2. 如果一个值为v的提案被批准,那么提案号更大的提案得到批准的条件是它的值也为v
为解决上述问题,提出了约束条件P2。在每个Acceptor可以接受多个提案的情况下,会导致多个提案得到批准。这是不允许的,但是如果每个被批准的提案的值都相同,那就相当于只批准了一个提案,这是可以接受的。我们直接对提案的结果进行约束,允许批准多个提案,但是这些提案的值必须相同,也就是必须同第一个被批准的提案的值相同。

P2a. 如果一个值为v的提案被批准,那么Acceptor只能接受提案号更大并且值为v的提案
事实上,只要满足P1和P2两个约束条件,就能够保证执行一次算法只批准一个提案了。但P2直接对结果进行约束,可操作性差。为此,需要寻找一个比P2更强的约束P2a,只要满足了P2a也就满足了P2。因为每个提案都需要Acceptor接受后才能够被批准,因此P2a对Acceptor进行约束,让它只接受提案号更大并且值为v的提案。考虑到P1也是对Acceptor的约束,分析下这两者有无冲突。假设在A1提出提案时,A2处于离线状态,并且由于消息丢失A3没有接收到A1的提案。因此,A1的提案只被A1、A4和A5接受,但这已经形成多数派,所以A1的提案被批准。随后,A2上线提出了提案号更大但值不同的提案,A3接收到A2的提案,并且这是它接收到的第一个提案,根据P1的约束,它应该无条件接受这个提案。但是,根据P2a约束,因为提案的值和已经被批准的提案的值不相同,所以不能接受A2的提案。P1和P2a出现了冲突。

P2b. 如果一个值为v的提案被批准,那么Proposer只能提出提案号更大并且值为v的提案
上面导致P1和P2a冲突的根源是,A2上线后贸然提出了值和已经批准的提案的值不相同的提案。为此,我们对Proposer进行约束,让它提出提案号更大的提案的值同已经被批准的提案的值相同。这样就避免了P1和P1a的冲突。另一方面,因为所有被Acceptor接受的提案都是由Proposer提出的,所以满足了P2b约束就满足了P2a约束,满足了P2a约束就满足了P2约束。

P2c. 如果有编号为n值为v的提案,那么存在一个多数派S,(a)要么S中的成员都没有接受过编号小于n的提案,(b)要么S接受的所有编号小于n的提案中提案号最大的提案的值为v
感觉从P2b到P2c有点跳跃,不过还是可以证明只要满足了P2c就能满足P2b。即要证明:如果一个编号为m值为v的提案被批准(P2b的假设条件),在满足P2c的条件下,那么Proposer提出的编号为n(n>m)的提案的值为v。证明过程采用数学归纳法。

当n=m+1时,采用反证法。即假设Proposer提出的编号为n的提案的值不为v而为w,那么必定会和P2c的条件相冲突。根据P2c,如果有编号为n值为w的提案,那么存在一个多数派S1,(a)要么S1中所有成员都没有接受过编号小于n的提案,(b)要么S1中所有接受到的编号小于n的提案中编号最大的提案的值为w。因为编号为m值为v的提案已经被批准,批准该提案的多数派C和S1之间至少存在一个公共成员,这个成员接受过编号为m值为n的提案,所以(a)不成立。因为小于n的最大编号是m,而编号m的提案的值为v不为w,所以(b)不成立。从而,假设不成立,即Proposer提出的编号为n的提案的值为v。

当n>m+1时,采用数学归纳法,即P2b在编号为m+1到n-1时都成立,要证明在编号为n时P2b依旧成立。证明过程仍然采用反证法。假设Proposer提出的编号为n的提案的值不为v而为w。那么根据P2c,存在一个多数派S1,(a)要么S1没有接受过编号小于n的提案,(b)要么S1接受的编号小于n的所有提案中编号最大的提案的值为w。因为编号为m值为v的提案被批准,而批准该提案的多数派C和S1之间存在至少一个公共成员,所以(a)不成立。另外,因为编号从m到n-1的提案的值都为v,所以(b)也不成立。因此假设不成立,P2c包含P2b的结论成立。

至此,我们只要满足P1和P2c两个约束条件,就可以达到执行一次算法只批准一个提案(可以是多个提案,但提案的值相同)的目的了。满足条件P1很容易,但为满足P2c,每个Proposer提出编号为n的提案时都要先学习所有Acceptor已经接受的编号小于n且最大的提案的值,并将新提案的值设置成该提案的值。学习Acceptor已经接受的提案容易,但是要预测Acceptor将会接受的提案就比较麻烦。对此,Paxos使用了承诺策略。当Proposer向Acceptor获取编号小于n的提案时,Acceptor一方面将自己接受的所有编号小于n的提案中编号最大的提案返回给Proposer,另一方面承诺不再接受编号小于n的提案。也就是说在Proposer学习过程中,Acceptor不会再接受编号小于n的提案,从而Proposer就不用去预测Acceptor在这段时间内会接受哪些编号小于n的提案了。

为满足P2c约束,Proposer提出提案的过程分以下两个步骤:
1、选择一个新的提案号n,向所有Acceptor发送Prepare请求,以获取:(a) 让Acceptor承诺不再接受编号小于n的提案;(b) Acceptor返回它接受的所有编号小于n的提案中编号最大的提案。
2、Proposer接受到所有Prepare请求的回应后,提出编号为n值为v的提案。值v是所有Acceptor已经接受的所有编号小于n的提案中的编号最大的提案的值。如果所有的Acceptor都没有接受过编号小于n的提案,那么v的值可以由Proposer自己指定。

P1a. 如果Acceptor承诺过不再接受编号小于m(m>n)的提案,则拒绝接受编号为n的提案,否则,接受编号为n的提案
当Proposer提出编号为n的提案后,向Acceptor发送Accept请求,以让Acceptor接受自己的提案。Acceptor根据P1a的原则来决定是否接受提案。

算法流程

为满足P1a和P2c两个约束,将算法的执行流程分成两个阶段:
Phase1

(a) Proposer选择一个提案号n,向所有的Acceptor发送Prepare消息;

(b) 如果Acceptor还没有承若过不接受编号小于m(m>n)的提案,则承诺不再接受编号小于n的提案,并返回它已经接受的编号小于n的提案中编号最大的提案。

Phase2

(a) Proposer接受到所有Acceptor对Prepare请求的回应后,提出编号为n值为v的提案。值v是所有Acceptor接受到的所有编号小于n的提案中编号最大的提案的值。如果没有Acceptor接受过编号小于n的提案,则值v可由Proposer自己决定。提出提案后,向所有的Acceptor发送Accept消息,以期Acceptor接受提案。

(b) 如果Acceptor没有向其它的Prepare请求承诺过不再接受编号小于m(m>n)的提案,则接受编号为n的提案。

活锁问题

考虑这样一种情况:
Ai(i=1,2,...,5)都没有接受过任何提案,A1提出编号为n的Prepare请求并发送给其它节点,A2~A5节点接收到A1的Prepare请求后都承诺不再接受编号小于n的提案。但是,在A1还没有发出Accept请求的时候,A2向所有节点发送了编号为n+1的Prepare请求,由于n+1大于n,所以所有节点都承诺不再接受编号小于n+1的提案。当A1提出的编号为n提案发送到各个节点时,每个节点都会拒绝接受编号为n的提案。同样地,在A2的Accept请求还没送到Ai节点时,Ai节点又接受到编号为n+2的Prepare请求并承诺不再接受编号小于n+2的提案,因此A2的提案又无法获得批准。如此循环往复,始终无法批准提案。这就是活锁问题。

产生活锁问题的原因在于,无法控制Proposer提出提案的时机,在一轮算法还没执行结束时就提出提案导致前面的提案被撤销。控制提案进度的方法是,选择一个Leader节点,只允许Leader节点提出提案。Leader节点可以将提案保存在队列中,等一个提案批准后再从队列中取出另外一个提案,这就避免了活锁问题。

引入Leader后,Paxos算法似乎变回到了Primary-Secondary模式,值的执行顺序完全由Leader决定,并且Leader存在单点故障。但Paxos算法的优势在于无论是Leader、Acceptor宕机都能保证正常工作。 如果Leader宕机,那么就要执行选举算法从现有的节点中选举出一个Leader。新Leader将从其余节点中收集信息,恢复状态。如果Acceptor宕机,只要能够形成多数派,就可以保持算法正常执行。

全局唯一提案号

全局唯一的提案号是影响Paxos算法正常运转的关键。如果两个Proposer提出相同的提案号,并且该编号的提案得到批准,那么在某些节点上更新这个值,在另外一些节点上更新那个值,就会出现副本不一致的问题。在单机上提出唯一的编号比较容易,只要依次递增即可。但在不同的机器上提出不同的编号,就要开动脑筋了。

Ceph是这么计算提案号的:
last_pn = (last_pn / 100 + 1)100 + rank
last_pn是Leader节点最近提出的提案号,rank是Leader节点的编号。也就是说,所有的提案号都是100的整数倍加上节点自己的编号。当rank值小于100时,可以保证每个节点提出的提案号都是全局唯一的,具体代码参考Paxos::get_new_proposal_number()函数。

参考资料

1、Paxos Made Simple
2、Paxos算法
3、Paxos算法2-算法过程
4、