Paxos算法推导
文章目录
关于一致性算法,看大家的文章基本上都是认为Raft比Paxos要好理解很多,事实上在学习的过程中也的确是这样。Raft学习理解完毕后我也来学习一下Paxos算法。毕竟如果这个世界上只有一种一致性算法,那么一定是Paxos。Paxos算法在分布式领域具有无可撼动的地位,缺点就是难以理解,工程实现也很难。
为了描述Paxos,Lamport虚拟了一个叫做Paxos的希腊城邦,这个岛按照议会民主制的政治模式制定法律,但是没有人愿意将自己的全部时间和精力放在这种事情上。所以无论是议员、议长或者是传递纸条的服务员都不能承诺别人在需要时一定会及时出现,也无法承诺批准决议或传递消息的时间。Paxos算法的场景中,我们假设不出现拜占庭将军问题,但是会出现网络丢失消息,或者存在消息延迟的情况,就是说可能会存在到达顺序和发送顺序不一样的情况。但是只要等待足够的时间,消息就会被传到。另外,Paxos岛上的议员不会反对其他议员提出的决议,也就是说大家的意愿是尽快形成一致的决议,而不是执着于让自己提出的决议通过。
Paxos定义
Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一
下面我就把推导的一步步过程通过这篇文章记录下来,也是为了加深自己的理解,有问题的地方大家可以随时指正。
Paxos算法出现所解决的问题
在常见的分布式系统中,总会发生机器宕机或者网络异常等情况,对应到消息传递上就是会导致消息的延迟、丢失、重复、乱序,网络分区等情况。Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生以上任何异常,都不会破坏系统的一致性。
Paxos可以拆分为两个问题:Basic Paxos、Multi Paxos,其中Basic Paxos就是我们经常提到的Paxos。假设有一组服务器,其中有些服务器会提议特定的值。Basic Paxos的目的是挑选这些值中唯一的一个,这个被选定的值成为chosen。Basic Paxos只挑选一个唯一值,它不会挑选第二个值,也不会更改它的选择。
而对于日志,会存在很多条日志记录,所以就需要为多条日志创建多个Basic Paxos实例,一条日志对应一个Basic Paxos实例,这就是Multi Paxos。
这篇文章主要是对Basic Paxos的推导过程记录。
Basic Paxos的需求
假设有一组可以提出提案(proposal)的进程集合。Basic Paxos需要保证提出的这么多proposal中,最终只有一个value被选定(value在提案proposal中)。如果没有proposal提出,就不该有value被选定。如果一个value被选定,那么所有进程都应该能learn到这个被选定的value。对于Basic Paxos,有如下两个需求:
- 安全性(Safety):
- 只有被提出的value才能被选定
- 只有一个value被选定
- 如果某个进程认为某个value被选定了,那么这个value必须是真的被选定的那个
- 可用性(Liveness):
- 最终一个会选定某个提议的值
- 服务器通过学习,最终会知道value已经被选定
- 可用性的前提是大多数的服务器是活着的,并能在合理的时间内通讯。
Basic Paxos的组件
- Proposer:提议者是主动性的,它们会主动做一些事情,通常它们会接收来自客户端的请求,获得特定的选定值,然后它会传递这个值,并让集群里的其他服务器也达成一致选择这个值。
- Acceptor:接受者是被动的,它们简单地接收来自于Proposer的请求并作出响应,可以把这种响应当成投票,Proposer会尝试获得Acceptor所投的多数票,Acceptor会存储多个状态,比如可能被选定或者未被选定的值,以及响应的投票结果。
- Learners:称为学习者,这些节点想要知道被选定的值。Learner是处于Acceptor内的
在具体的实现中,一个进程可能同时充当多种角色。比如一个进程可能既是Proposer又是Acceptor又是Learner。还有一个很重要的概念叫Proposal提案,最终要达成一致的value就在提案里。
Proposer、Acceptor、Learner分别在什么情况下才认为某个value被选定呢?
- Proposer:只要Proposer发出的提案被Acceptor接受(半数以上的Acceptor同意才行),Proposer就认为该提案里的value被选定了。
- Acceptor:只要Acceptor接受了某个提案,Acceptor就认为该提案里的value被选定了。
- Learner:Acceptor告诉Learner哪个value被选定,Learner就认为那个value被选定。
推导过程
我们先假设不同组件之间可以通过发送消息来进行通信,那么:
- 每个组件以任意的速度执行,可能因出错而停止,也可能会重启。一个value被选定后,所有的组件可能失败然后重启,除非那些失败后重启的组件能记录某些信息,否则等他们重启后无法确定被选定的value
- 消息在传递过程中可能出现任意时长的延迟,可能会重复,也可能丢失,但是消息不会被篡改
我们先从最简单的方案开始探讨:
单Acceptor
假设只有一个Acceptor,多个Proposer,只要Acceptor接受它收到的第一个提案,则该提案被选定,该提案里的value就是被选定的value。这样就可以保证只有一个value会被选定。但是这样的系统可用性很低,一旦唯一的Acceptor宕机,整个系统就不可用。因此Acceptor必须要有多个。
多个Acceptor
如何保证在多个Proposer和多个Acceptor的情况下选定一个value呢?
如果我们希望即使只有一个Proposer提出了一个value,该value也最终被选定。那么就可以得到下面的约束:
P1:一个Acceptor必须接受它收到的第一个提案Proposal
但是这会导致出现新的问题:如果每个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的约束条件,形成P2a。
P2a:如果某个value为v的提案被选定了,那么每个编号更高的被Acceptor接受的提案的value也必须是v。
只要满足了P2a,就能满足P2.
但是目前的约束条件还不够强,考虑下面的情况:
假设总共有5个Acceptor,Proposer2提出[M1,V1]的提案,Acceptor2-5(已达majority)均接受了该提案,于是对于Acceptor2-5和Proposer2来说,它们都认为V1被选定了。Acceptor1刚从宕机状态恢复且Acceptor1没有收到过任何提案,此时Proposer1向Acceptor1发送了[M2,V2]的提案(V2!=V1且M2>M1),对于Acceptor1来讲,这是它收到的第一个提案,根据P1,Acceptor1必须接受该提案,这样Acceptor1就成功接受了Proposer1的提案[M2,V2],Acceptor1也认为V2被选定。这里出现了两个问题:
- Acceptor1认为V2被选定,Acceptor2-5和Proposer2认为V1被选定,这里出现不一致情况。
- V1被选定了,但是编号更高的被Acceptor1接受的提案[M2,V2]的value是V2,V2!=V1。这就是和P2a矛盾。
因此我们还要在P2a的条件上进一步强化约束,P2a是对Acceptor接受的提案约束,但是提案是Proposer提出的,所以我们对Proposer提出的提案进行约束,得到P2b:
P2b:如果某个value为v的提案被选定了,那么之后任何Proposer提出的编号更高的提案的value也必须是v。
那我们如何保证Proposer提出的编号更高的提案中的value都是v呢?我们再进行约束:
P2c:对于任意的M和V,如果提案[M,V]被提出,那么存在一个半数以上的Acceptor组成的集合S,满足以下两个条件中的任意一个:
- S中每个Acceptor都没有接受过编号小于M的提案
- S中Acceptor接受过的最大编号的提案的value为V
Proposer生成提案
为了满足P2b,这里涉及到一个重要的问题:Proposer生成提案之前,应该先去学习已经被选定或者可能被选定的value,然后以该value作为自己提出的提案的value。如果没有value被选定,Proposer才可以自己决定value的值,这样才能满足一致性。这个学习阶段在Basic Paxos算法中是通过Prepare请求实现的。
提案生成算法如下:
- Proposer选择一个新的提案编号M,然后向某个Acceptor集合(半数以上)发送请求,要求该集合中的每个Acceptor做出如下响应:
- 向Proposer承诺保证不再接受任何编号小于M的提案
- 如果Acceptor已经接受过提案,那么就向Proposer响应已经接受过的编号小于M的最大编号的提案
我们将该请求称为编号M的Prepare请求。如果Proposer收到了半数以上Acceptor的响应,那么它就可以生成编号为M,value为V的提案[M,V]。这里的V是所有的响应中编号最大的提案的value。如果所有的响应中都没有提案,那么此时V就可以由Proposer自己选择。在生成提案后,Proposer将该提案发送给半数以上的Acceptor集合,并期望这些Acceptor集合能接受该提案,我们称为Accept请求(此时接受Accept请求的Acceptor集合不一定是之前响应Prepare请求的Acceptor集合)
Acceptor接受提案
Acceptor可以忽略任何请求(包括Prepare和Accept请求)而不用担心破坏算法的安全性。因此,我们这里要讨论的是什么时候Acceptor可以响应一个请求。
我们对Acceptor接受提案做出如下约束:
P1a:一个Acceptor只要尚未响应过任何编号大于M的Prepare请求,那么它就可以接受这个编号为M的提案。
如果Acceptor收到一个编号为M的Prepare请求,在此之前它已经响应过编号大于M的Prepare请求。根据P1a,该Acceptor不可能接受编号为M的提案。因此该Acceptor可以忽略编号为M的Prepare请求。
因此一个Acceptor需要保存两个信息:
- 已接受的编号最大的提案
- 已响应的请求的最大编号
Basic Paxos算法描述
-
Basics
- 提案编号(n)=(轮次round number,服务器ID)
- T:在领导者选举算法中使用的固定超时时间
- α:Multi-Paxos中的并发限制
1.1 领导选举算法
- 每隔T毫秒向其他服务器发送空的心跳消息。
- 如果一个服务器在最近的2T毫秒内没有收到某一具有更高ID的服务器的心跳消息,则它充当领导者。
上面的几条有些内容我们可能还没涉及到,这里面都是Multi-Paxos的内容。下一篇文章我对Multi-Paxos做个推导记录。
-
Basic Paxos
2.1 每台服务器的持久状态
- minProposal:此服务器将接受的最小提案的编号,如果从未收到Prepare请求,则为0
- acceptedProposal:服务器已接受的最后一个提议的编号,如果从未接受任何提案,则为0
- acceptedValue:服务器已接受的最新提案中的值,如果从未接受提案,则为null
- maxRound:服务器已知的最大轮数
2.2 Messages
2.2.1 Prepare阶段
请求字段:
- n:新的提案编号
Acceptor在收到Prepare请求后, 如果n>=minproposal,Acceptor将minProposal设置为n。该响应构成了拒绝将来提案编号小于n的Accept消息的承诺,即承诺不会再接受一个提案编号小于n的请求。
响应字段:
- acceptedProposal:Acceptor的acceptedProposal(当前接受的具有最高编号的提案的编号)
- acceptedValue:Acceptor的acceptedValue(当前接受的具有最高编号的提案的值)
2.2.2 Accept
请求字段:
- n:与Prepare阶段中使用的提案编号相同
- v:一个值,可以是Prepare阶段所有响应中编号最大的值,如果没有,则来自客户端请求
Acceptor收到Accept请求后,如果n>=minProposal,则:
- 设置acceptedProposal=n
- 设置acceptedValue=v
- 设置minProposal=n
响应字段:
- n:Acceptor的minProposal
2.3 Proposer提议者算法
- 设n为新的提案编号(增加并持久化maxRound)
- 广播Prepare(n)请求给所有Acceptor
- 当收到来自大多数Acceptor的Prepare响应(reply.acceptedProposal,reply.acceptedValue):
- 按如下方式设置v:如果这些reponse中的最大reply.acceptedProposal不为0,则使用其对应的reply.acceptedValue。否则使用inputValue
- 广播Accept(n,v)请求
- 当收到Accept的response(reply.n):
- 如果reply.n>n,则设置maxRound从n开始,并从步骤1重新执行
- 等待直到接收到来自大多数接受者的n的响应
- 最终选定v
如果保证Basic Paxos算法的Liveness
竞争提案时可能会导致死锁
假设服务器S1成功接收到请求,并处于Prepare阶段(P 3.1)。在接受值X之前(A 3.1X),另外一个服务器S5正处于它的Prepare阶段(P 3.5),这会阻止前序值的接受(A 3.1X)。然后S1会重新选择提案编号并再次开始提案过程(P 4.1),假设它正进入了第二轮的Prepare阶段,在接受值之前,服务器S5正试图完成接受值的选定Y(A 3.5 Y),不过此时因为(P 4.1)的编号高于(A 3.5 Y),所以它阻止了(A 3.5 Y)的Accept,这样S5的提议就失败了,然后S5又重新开始下一轮的提案,如此往复,这个过程无限循环就造成了死锁。
为了不发生死锁,Basic Paxos需要通过某种补充机制来保证它可以正确运行。一个简单的方式是让服务器等待一个random time,如果发生接受失败的情况,必须返回重新开始。在重新开始之前等待一会,让提案有机会完成。话说这个机制真的很像Raft leader election阶段的选举失败再Re-election的机制,都是随机等待一段时间。可以让集群下服务器随机的延迟,从而避免所有服务器都处于相同的等待时间下。在Multi-Paxos下,会有些不同,会采取一种leader election的机制,保证在同一时间下只有一个Proposer在工作。