分布式算法Paxos探究

139 阅读14分钟

Paxos

背景:

Paxos算法作者Lamport描述了一个名为Paxos的希腊城邦,这个城邦是按照民主的议会制度来进行选举的,所有的居民进行提议和投票来选出决议。但是居民们不想花时间一直在选举上,大家都不定时地来提议、了解提议、投票、看进展等等,而Paxos算法的目标就是通过少数服从多数的方式来达成最终的一致意见。

在常见的分布式系统中,总会发生诸如机器宕机网络异常(包括消息的延迟、丢失、重复、乱序,还有网络分区)等情况。Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。但是Paxos算法不解决拜占庭将军问题。

注:这里某个数据的值并不只是狭义上的某个数,它可以是一条日志,也可以是一条命令(command)。。。根据应用场景不同,某个数据的值有不同的含义。

image-20220620162733569.png

相关概念

在Paxos算法中包含三种角色:

  • Proposer:提案者。也就是在选举中提出提案的人,放到分布式系统里,就是接收到客户端写操作的人。一切行为都由Proposer提出提案开始,Paxos会将提案想要进行的操作,抽象为一个“value”,去在多台机器中传递,最后被所有机器接受。
  • Acceptor:批准者。Acceptor从含义上来说就是除了当前Proposer以外的其他机器,他们之间完全平等和独立,Proposer需要争取超过半数(N/2+1)的Acceptor批准后,其提案才能通过,它倡导的“value”操作才能被所有机器所接受。
  • Learners:学习者这个角色,该角色是在达成决议时,对结论的学习者,也即是从其他节点“学习”最终提案内容,比较简单,不参与选举。

算法过程

一个简单的提案:

先描述最简单的情况,假设现在有四台机器,其中一台收到了来自客户端的写操作请求,需要同步给其他机器。此时这台收到请求的机器,我们称它为proposer,因为它将要开始将收到的请求,作为一个提案,提给其他的机器。这里为了方便,我们假设这个请求是要将一个密码设置为“123”,此时,其他的acceptor都闲着呢(null),也没其他人找,所以当它们收到proposer的提案时,就直接投票了,说可以可以,我是空的,赞成提案(同意提议):那么如下图所示:

提议准备阶段.jpg

到这里,就还是一个简单的同步的案例,但需要注意的是,这里proposer实际上是经历了两步的。

在这个简单的提案过程中,proposer其实也经历了两个阶段

Prepare阶段:proposer告诉所有其他机器,我这里有一个提案(操作),想要你们投投票支持一下,想听听大家的意见。acceptor看自己是null,也就是目前还没有接受过其他的提案,就说我肯定支持。

Accept阶段:proposer收到其他机器的回复,说他们都是空的,也就是都可以支持接受proposer的提案(操作),同时支持的数量超过半数,于是正式通知大家这个提案被集体通过了,可以生效了,操作就会被同步到所有机器正式生效。

两个提案并发进行

现在考虑一个更复杂的场景,因为我们处于一个分布式的场景,每台机器都可能会收到请求,那如果有两台机器同时收到了两个客户端的不同请求,该怎么处理呢?大家听谁的呢?最后的共识以谁的为准呢?如下图所示:

image-20220620145532840.png

在这种情况下,由于网络传输的时间问题,两个Proposer的提案到达各个机器,是会存在先后顺序的。假设Proposer 1的提案先达到了Acceptor 1和 Acceptor 2,而Proposer 2的提案先达到了Acceptor 3,其达到Acceptor 1和Acceptor 2时,由于机器已经投票给Proposer 1了,所以Proposer 2的提案遭到拒绝,Proposer 1达到Acceptor 3的时候同样被拒,时序图如下图所示:

image-20220620152834202.png

Proposer 1发现超过半数的Acceptor都接受了自己,所以放心大胆地发起要求,让所有Acceptor都按照自己的值来操作。而Proposer 2发现只有不到半数的Acceptor支持自己,而有超过半数是支持Proposer 1的值的,因此只能拒绝Client 2,并将自己也改为Proposer 1的操作,如下图所示:!

image-20220620151026843.png 到此为止,看起来没有问题,但是,这是因为恰好Acceptor的数量是单数,可以选出“大多数”,但是因为同时成为Proposer的机器数量是不确定的,因此是无法保证Acceptor的数量一定是单数的,如下面这种情况就无法选出“大多数”了:

image-20220620153443306.png

这时,两个Proposer有可能总是先抢到一个Acceptor的支持,然后在另一个Acceptor处折戟沉沙,算法就一直循环死锁下去了。为了解决这种情况,Paxos给提案加了一个编号

给提案加上编号

之前我们Proposer的提案都是只有操作内容的,现在我们给他加一个编号,即:

Proposer 1的提案为:[n1,v1]

Proposer 2的提案为:[n2,v2]

假设Proposer 1接到Client1的消息稍微早一点,那么它的编号就是1,Proposer 2的编号就是2,那么他们的提案实际就是:

Proposer 1的提案为:[1,{ Set password=123}]

Proposer 2的提案为:[2,{ Set password=456}]

此时,Paxos加上一条规则:

Acceptor如果还没有正式通过提案(即还没有Accept使操作生效),就可以接受编号更大的Prepare请求

所以,回到上面的困境

当Proposer 1想要向Acceptor 2寻求支持时,Acceptor 2一看你的编号(1)比我已经支持的编号(2)要小,拒绝拒绝。此时Proposer 1由于没有得到过半数的支持,会重新寻求支持。

而当Proposer 2想要向Acceptor 1寻求支持时,Acceptor 1一看你的编号(2)比我已经支持的编号(1)要大,好的你是老大我听你的。此时Proposer 2已经得到了超过半数的支持,可以进入正式生效的Accept阶段了。

image-20220620160934294.png

这里需要补充一下,Proposer 1这里支持提案失败,他是怎么让自己也接受Proposer 2的提案的呢?

所以这里的后续会发生的事情是:

Proposer 2发现得到了过半数的支持,开始向所有Acceptor发送Accept请求。

所有Acceptor接收到Accept请求后,按照之前Prepare时收到的信息与承诺,去生效Proposer 2的提案内容(即Set password=456的操作)。

Proposer 1之前已经收到了所有Acceptor的回复,发现没有得到过半数的支持,直接回复Client 1请求失败,并变成一个Acceptor(或者说Learner),接受Proposer 2的Accept请求。

这里再想多一点,考虑另一种场景:假设Proposer 2的Accept请求先达到了Acceptor 2,然后Proposer 1向Acceptor 2发送的Prepare请求才到达 Acceptor 2,会发生什么呢?

最直观的处理是,Acceptor 2直接拒绝,然后Proposer 1走上面的流程,但Paxos为了效率,又增加了另一条规则:

如果一个Prepare请求,到达Acceptor时,发现该Acceptor已经接受生效了另一个提案,那么它除了回复提案被拒绝外,还会带上Acceptor已经通过的编号最大的那个提案的内容回到Proposer。Proposer收到带内容的拒绝后,需要修改自己的提案为返回的内容

此时会发生的事情就变成了:

此时Acceptor 2除了会拒绝它的请求,还会告诉Proposer 1,说我已经通过并生效了另一个编号为2的提案,内容是Set password=456。

然后Proposer 1查看回复时,发现已经有Acceptor生效提案了,于是就修改自己的提案,也改为Set password=456,并告知Client 1你的请求失败了。

接着Proposer 1开始充当Proposer 2的小帮手,帮他一起传播 Proposer 2的提案,加快达成共识的过程。

PS:这里需要注意, 编号是需要保证全局唯一的,而且是全局递增的 ,否则在比较编号大小的时候就会出现问题,怎么保证编号唯一且递增有很多方法,比如都向一个统一的编号生成器请求新编号;又比如每个机器的编号用机器ID拼接一个数字,该数字按一个比总机器数更大的数字间隔递增。

一些异常情况

异常情况一:假设现在有三个Proposer同时收到客户端的请求,那么他们会生成全局唯一的不同编号,带着各自接收到的请求提案,去寻求Acceptor的支持。但假设他们都分别争取到了一个Acceptor的支持,此时由于Prepare阶段只会接受编号更大的提案,所以正常情况下只有Proposer 3的提案会得到所有Acceptor的支持。但假设这时候Proposer 3机器挂了,无法进行下一步的Accept了,怎么办呢?那么所有Acceptor就会陷入持续的等待,而其他的Proposer也会一直重试然后一直失败。

image-20220620161928473.png

为了解决这个问题,Paxos决定,允许Proposer在提案遭到过半数的拒绝时,更新自己的提案编号,用新的更大的提案编号,去发起新的Prepare请求

那么此时Proposer 1和Proposer 2就会更新自己的编号,从【1】与【2】,改为比如【4】和【5】,重新尝试提案。这时即使Proposer 3机器挂了,没有完成Accept,Acceptor也会由于接收到了编号更大的提案,从而覆盖掉Proposer 3的提案,进入新的投票支持阶段。

异常情况二虽然更新编号是解* 决了上面的问题,但却又引入了活锁的问题*。由于可以更新编号,那么有概率出现这种情况,即每个Proposer都在被拒绝时,增大自己的编号,然后每个Proposer在新的编号下又争取到了小于半数的Acceptor,都无法进入Accept,又重新加大编号发起提案,一直这样往复循环,就成了活锁(和死锁的区别是,他们的状态一直在变化,尝试解锁,但还是被锁住了)。

要解决活锁的问题,有几种常见的方法:

当Proposer接收到回复,发现支持它的Acceptor小于半数时,可以不立即更新编号重试,而是随机延迟一小段时间,来错开彼此的冲突。

可以设置一个Proposer的Leader,全部由它来进行提案,这即使共识算法的常见套路,选择一个Leader。这需要进行Leader的选举,以及解决存活性检查以及换届的问题。实际上就已经演变成Multi-Paxos了。

异常情况三:由于在提案时,Proposer都是根据是否得到超过半数的Acceptor的支持,来作为是否进入Accept阶段的依据,那如果在算法进行中新增或下线了机器呢?如果此时一些Proposer知道机器数变了,一些Proposer不知道,那么大家对半数的判断就会不一致,导致算法出错。

因此在实际运行中,机器节点数的变动,也需要作为一条要达成共识的请求提案,通过Paxos算法本身,传达到所有机器节点上。

为了使Paxos运行得更稳定,不需要时刻担心是否有节点数变化,可以固定一个周期,要求只有在达到固定周期时才允许变更节点数,比如只有在经过十次客户端请求的提案与接受后,才处理一次机器节点数变化的提案。

那如果这个间隔设置地相对过久,导致现在想要修改节点数时,一直要苦等提案数,怎么办呢?毕竟有时候机器坏了是等不了的。那么可以支持主动填充空的提案数,来让节点变更的提案尽早生效。

Paxos总结:分为两个阶段

Prepare准备阶段

在该阶段,Proposer会尝试告诉所有的其他机器,我现在有一个提案(操作),请告诉我你们是否支持(是否能接受)。其他机器会看看自己是否已经支持其他提案了(是否接受过其他操作请求),并回复给Proposer(如果曾经接受过其他值,就告诉Proposer接受过什么值/操作);

Acceptor如果已经支持了编号N的提案,那么不会再支持编号小于N的提案,但可以支持编号更大的提案;

Acceptor如果生效了编号为N的提案,那么不会再接受编号小于N的提案,且会在回复时告知当前已生效的提案编号与内容。

Accept提交阶段

在该阶段,Proposer根据上一阶段接收到的回复,来决定行为;

如果上一阶段超过半数的机器回复说接受提案,那么Proposer就正式通知所有机器去生效这个操作;

如果上一阶段超过半数的机器回复说他们已经先接受了其他编号更大的提案,那么Proposer会更新一个更大的编号去重试(随机延时);

如果上一阶段的机器回复说他们已经生效了其他编号的提案,那么Proposer就也只能接受这个其他人的提案,并告知所有机器直接接受这个新的提案;

如果上一阶段都没收到半数的机器回复,那么提案取消;

PS:接受其他提案,以及提案取消的情况下,Proposer就要直接告诉客户端该次请求失败了,等待客户端重试即可。

这里可以看到,超过半数以上的机器是个很重要的决定结果走向的条件。至此,已经描述完了针对一次达成共识的过程,这被称为Basic-Paxos

那如果有多个值需要达成共识呢?

Multi-Paxos

如果有多个值要不断地去针对一次次请求达成共识,使用Basic-Paxos也是可以的,无非就是一遍遍地执行算法取得共识并生效嘛,但在分布式系统下,容易由于多次的通信协程造成响应过慢的问题,何况还有活锁问题存在。因此Lamport给出的解法是:

先选择一个Leader来担当Proposer的角色,取消多Proposer,只有一个Leader来提交提案,这样就没有了竞争(也没有了活锁)。同时,由于无需协商判断,有了Leader后就可以取消Prepare阶段,两阶段变一阶段,提高效率。

对于每一次要确定的值/操作,使用唯一的一个标识来区分,保证其单调递增即可。

对于选择Leader的过程,简单的做法很多,复杂的也只需要进行一次Basic-Paxos即可。选出Leader后,直到Leader挂掉或者到期,都可以保持由它来进行简化的Paxos协议。

如果有多个机器节点都由于某些问题自认为自己是Leader,从而都提交了提案,也没关系,可以令其退化成Basic-Paxos,也可以在发现后再次选择Leader即可。