本篇文章来源于网络资源整理而来,如有侵权,请联系删除
[前言]()[相关概念]()[问题描述]()[推导过程]()[最简单的方案——只有一个Acceptor]()[多个Acceptor]()[Proposer生成提案]()[Acceptor接受提案]()[Paxos算法描述]()[Learner学习被选定的value]()[如何保证Paxos算法的活性]()[Paxos 优化]()
前言
paxos是什么?
- 在分布式系统中保证多副本数据强一致的算法.
paxos有啥用?
- 没有paxos的一堆机器, 叫做分布式;
- 有paxos协同的一堆机器, 叫分布式系统.
Google Chubby的作者Mike Burrows说过:
这个世界上只有一种一致性算法,那就是Paxos …
其他一致性算法, 都可以看做paxos在实现中的变体和扩展.
另外一个经常被提及的分布式算法是raft, raft的贡献在于把一致性算法落地. 因为 Leslie Lamport 的理论很抽象, 要想把他的理论应用到现实中, 还需要工程师完全掌握他的理论再添加工程必要的环节才能跑起来.
经常有人问起raft和paxos的区别, 或在实现中应该选择哪个, 在不了解paxos之前可能会有这种疑问. 对于这个问题, 就像是被问及四则运算和算盘有什么区别, 小店老板应该使用四则运算还是用算盘结账一样.
记得 Leslie Lamport 2015年时来了一次北京, 那时会场上有人也问了老爷子 paxos和raft有啥区别.
老爷子当时给出的回答是: 没听过raft…
相关概念
在Paxos算法中,有三种角色:
- Proposer
- Acceptor
- Learners
在具体的实现中,一个进程可能同时充当多种角色。比如一个进程可能既是Proposer又是Acceptor又是Learner。
还有一个很重要的概念叫提案(Proposal)。最终要达成一致的value就在提案里。
注:
- 暂且认为『提案=value』,即提案只包含value。在我们接下来的推导过程中会发现如果提案只包含value,会有问题,于是我们再对提案重新设计。
- 暂且认为『Proposer可以直接提出提案』。在我们接下来的推导过程中会发现如果Proposer直接提出提案会有问题,需要增加一个学习提案的过程。
Proposer可以提出(propose)提案;Acceptor可以接受(accept)提案;如果某个提案被选定(chosen),那么该提案里的value就被选定了。
回到刚刚说的『对某个数据的值达成一致』,指的是Proposer、Acceptor、Learner都认为同一个value被选定(chosen)。那么,Proposer、Acceptor、Learner分别在什么情况下才能认为某个value被选定呢?
- Proposer:只要Proposer发的提案被Acceptor接受(刚开始先认为只需要一个Acceptor接受即可,在推导过程中会发现需要半数以上的Acceptor同意才行),Proposer就认为该提案里的value被选定了。
- Acceptor:只要Acceptor接受了某个提案,Acceptor就任务该提案里的value被选定了。
- Learner:Acceptor告诉Learner哪个value被选定,Learner就认为那个value被选定。
问题描述
假设有一组可以提出(propose)value(value在提案Proposal里)的进程集合。一个一致性算法需要保证提出的这么多value中,只有一个value被选定(chosen)。如果没有value被提出,就不应该有value被选定。如果一个value被选定,那么所有进程都应该能学习(learn)到这个被选定的value。对于一致性算法,安全性(safaty)要求如下:
- 只有被提出的value才能被选定。
- 只有一个value被选定,并且
- 如果某个进程认为某个value被选定了,那么这个value必须是真的被选定的那个。
我们不去精确地定义其活性(liveness)要求。我们的目标是保证最终有一个提出的value被选定。当一个value被选定后,进程最终也能学习到这个value。
Paxos的目标:保证最终有一个value会被选定,当value被选定后,进程最终也能获取到被选定的value。
假设不同角色之间可以通过发送消息来进行通信,那么:
- 每个角色以任意的速度执行,可能因出错而停止,也可能会重启。一个value被选定后,所有的角色可能失败然后重启,除非那些失败后重启的角色能记录某些信息,否则等他们重启后无法确定被选定的值。
- 消息在传递过程中可能出现任意时长的延迟,可能会重复,也可能丢失。但是消息不会被损坏,即消息内容不会被篡改(拜占庭将军问题)。
推导过程
最简单的方案——只有一个Acceptor
假设只有一个Acceptor(可以有多个Proposer),只要Acceptor接受它收到的第一个提案,则该提案被选定,该提案里的value就是被选定的value。这样就保证只有一个value会被选定。
但是,如果这个唯一的Acceptor宕机了,那么整个系统就无法工作了!
因此,必须要有多个Acceptor!
多个Acceptor
多个Acceptor的情况如下图。那么,如何保证在多个Proposer和多个Acceptor的情况下选定一个value呢?
下面开始寻找解决方案。
如果我们希望即使只有一个Proposer提出了一个value,该value也最终被选定。
那么,就得到下面的约束:
P1:一个Acceptor必须接受它收到的第一个提案。
但是,这又会引出另一个问题:如果每个Proposer分别提出不同的value,发给不同的Acceptor。根据P1,Acceptor分别接受自己收到的value,就导致不同的value被选定。出现了不一致。如下图:
刚刚是因为『一个提案只要被一个Acceptor接受,则该提案的value就被选定了』才导致了出现上面不一致的问题。因此,我们需要加一个规定:
规定:一个提案被选定需要被半数以上的Acceptor接受
这个规定又暗示了:『一个Acceptor必须能够接受不止一个提案!』不然可能导致最终没有value被选定。比如上图的情况。v1、v2、v3都没有被选定,因为它们都只被一个Acceptor的接受。
最开始讲的『提案=value』已经不能满足需求了,于是重新设计提案,给每个提案加上一个提案编号,表示提案被提出的顺序。令『提案=提案编号+value』。
虽然允许多个提案被选定,但必须保证所有被选定的提案都具有相同的value值。否则又会出现不一致。
于是有了下面的约束:
P2:如果某个value为v的提案被选定了,那么每个编号更高的被选定提案的value必须也是v。
一个提案只有被Acceptor接受才可能被选定,因此我们可以把P2约束改写成对Acceptor接受的提案的约束P2a。
P2a:如果某个value为v的提案被选定了,那么每个编号更高的被Acceptor接受的提案的value必须也是v。
只要满足了P2a,就能满足P2。
但是,考虑如下的情况:假设总的有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必须接受该提案!同时Acceptor1认为V2被选定。这就出现了两个问题:
- Acceptor1认为V2被选定,Acceptor2~5和Proposer2认为V1被选定。出现了不一致。
- V1被选定了,但是编号更高的被Acceptor1接受的提案[M2,V2]的value为V2,且V2≠V1。这就跟P2a(如果某个value为v的提案被选定了,那么每个编号更高的被Acceptor接受的提案的value必须也是v)矛盾了。
所以我们要对P2a约束进行强化!
以下是原PO的解释,我觉得换一种方式更好理解:
P2a是对Acceptor接受的提案约束,但其实提案是Proposer提出来的,所有我们可以对Proposer提出的提案进行约束。得到P2b:
> P2b:如果某个value为v的提案被选定了,那么之后任何Proposer提出的编号更高的提案的value必须也是v。
由P2b可以推出P2a进而推出P2。
那么,如何确保在某个value为v的提案被选定后,Proposer提出的编号更高的提案的value都是v呢?
只要满足P2c即可:
> P2c:对于任意的N和V,如果提案[N, V]被提出,那么存在一个半数以上的Acceptor组成的集合S,满足以下两个条件中的任意一个:
- S中每个Acceptor都没有接受过编号小于N的提案。
- S中Acceptor接受过的最大编号的提案的value为V。
另一种理解思路:
既然分布式是为了达成一致性,那么v1和v2矛盾后的下一个约束p2b应该是:acceptor接受的提案是可以被修改的,以求快速达到提案一致性。那么应该如何修改acceptor已接受的提案?当然是少数服从多数,把少数的一方改为多数的一方就行了。那么怎么判定谁是多数的一方呢?通过编号和学习过程。我们会不断把编号较大的提案的值辐射到其余acceptor中。尝试获取过半的acceptor的中最大编号的提案,然后继续发提案。为什么是较大呢,因为网络有延迟啊、断网什么的,我们只需要过半就可以辐射一次,不需要非要找到最大编号的提案。不断重复这一步,大部分acceptor所接受的提案的值就会等于编号最大的提案的值。是不是逐渐达到一致性了。
Proposer生成提案
为了满足P2b,这里有个比较重要的思想:Proposer生成提案之前,应该先去『学习』已经被选定或者可能被选定的value,然后以该value作为自己提出的提案的value。如果没有value被选定,Proposer才可以自己决定value的值。这样才能达成一致。这个学习的阶段是通过一个『Prepare请求』实现的。
于是我们得到了如下的提案生成算法:
- Proposer选择一个新的提案编号N,然后向某个Acceptor集合(半数以上)发送请求,要求该集合中的每个Acceptor做出如下响应(response)。
(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请求)而不用担心破坏算法的安全性。因此,我们这里要讨论的是什么时候Acceptor可以响应一个请求。
我们对Acceptor接受提案给出如下约束:
P1a:一个Acceptor只要尚未响应过任何编号大于N的Prepare请求,那么他就可以接受这个编号为N的提案。
如果Acceptor收到一个编号为N的Prepare请求,在此之前它已经响应过编号大于N的Prepare请求。根据P1a,该Acceptor不可能接受编号为N的提案。因此,该Acceptor可以忽略编号为N的Prepare请求。当然,也可以回复一个error,让Proposer尽早知道自己的提案不会被接受。
因此,一个Acceptor只需记住:1. 已接受的编号最大的提案 2. 已响应的请求的最大编号。
Paxos算法描述
经过上面的推导,我们总结下Paxos算法的流程。
Paxos算法分为两个阶段。具体如下:
- 阶段一:
(a) Proposer选择一个提案编号N,然后向半数以上的Acceptor发送编号为N的Prepare请求。
(b) 如果一个Acceptor收到一个编号为N的Prepare请求,且N大于该Acceptor已经响应过的所有Prepare请求的编号,那么它就会将它已经接受过的编号最大的提案(如果有的话)作为响应反馈给Proposer,同时该Acceptor承诺不再接受任何编号小于N的提案。 - 阶段二:
(a) 如果Proposer收到半数以上Acceptor对其发出的编号为N的Prepare请求的响应,那么它就会发送一个针对[N,V]提案的Accept请求给半数以上的Acceptor。注意:V就是收到的响应中编号最大的提案的value,如果响应中不包含任何提案,那么V就由Proposer自己决定。
(b) 如果Acceptor收到一个针对编号为N的提案的Accept请求,只要该Acceptor没有对编号大于N的Prepare请求做出过响应,它就接受该提案。
Learner学习被选定的value
Learner学习(获取)被选定的value有如下三种方案:
如何保证Paxos算法的活性
通过选取主Proposer,就可以保证Paxos算法的活性。至此,我们得到一个既能保证安全性,又能保证活性的分布式一致性算法——Paxos算法。
“活锁”的根本原因在于两个proposer交替提案,避免“活锁”的方式为,如果一个proposer通过accpter返回的消息知道此时有更高编号的提案被提出时,该proposer静默一段时间,而不是马上提出更高的方案,静默期长短为一个提案从提出到被接受的大概时间长度即可,静默期过后,proposer重新提案。其实就是类似于raft那种,产生冲突的时候重新随机等待一段时间。系统中之所以要有主proposer的原因在于,如果每次数据更改都用paxos,那实在是太慢了,还是通过主节点下发请求这样来的快,因为省去了不必要的paxos时间。所以选择主proposer用paxos算法,因为选主的频率要比更改数据频率低太多。但是主proposer挂了咋整,整个集群就一直处于不可用状态,所以一般都用租约的方式,如果proposer挂了,则租约会过期,其它proposer就可以再重新选主,如果不挂,则主proposer自己续租。
然而,有leader 就以意味着有单点故障的问题。
Paxos 优化
第一个优化 multi-paxos:
paxos诞生之初为人诟病的一个方面就是每写入一个值就需要2轮rpc: phase-1和phase-2. 因此一个寻常的优化就是用一次rpc为多个paxos实例运行phase-1。
例如, Proposer X可以一次性为i₁~i₁₀这10个值, 运行phase-1,例如为这10个paxos实例选择rnd为1001, 1002…1010。这样就可以节省下9次rpc,而所有的写入平均下来只需要1个rpc就可以完成了。
这么看起来就有点像raft了:
- 再加上commit概念(commit可以理解为: 值v送达到多数派这件事情是否送达到多数派了)
- 和组成员变更(将quorum[acceptor 的多数派] 的定义从”多于半数”扩展到”任意2个quourm必须有交集”)
第二个优化 fast-paxos:
fast-paxos通过增加quorum的数量来达到一次rpc就能达成一致的目的. 如果fast-paxos没能在一次rpc达成一致, 则要退化到classic paxos.