说到分布式一致性算法,那么必然不可避免谈到Paxos,Paxos算法在分布式领域地位非常重要,接下来简单记录下Paxos算法的原理。由于个人水平有限,如有错误还请谅解,本文参考书籍《从Paxos到ZooKeeper》与一些网络博客。
在分布式系统中,经常会发生例如网络异常、服务宕机等情况,为了解决出现问题时数据不一致而产生了Paxos算法,可以保证无论在任何情况下都不会破坏数据的一致性。
Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。
每次在对数据进行更新时,由Proposer角色进行本次操作提案的发起,由Acceptor角色进行提案的同意表决,最终完成数据的更新。
提案的内容:提案编号和提案数据值Value,姑且暂时只关注Value(修改数据的值 = Value),后面会对提案的内容进行详细介绍。
在Paxos算法中,将每个进程分配一种或多种角色。分别为:
如何保证当一个Proposer集合同时提出一个提案时,最终只有一个提案被选定,并且所有进程都能够学习到这个提案值,如果没有提案被提出就不会有提案被选定。
Paxos的目标:保证最终有一个value会被选定,当value被选定后,所有进程最终也能获取到被选定的value。
如果在只有一个Acceptor角色的情况下,那么问题非常容易解决,只要Acceptor同意收到的第一个提案,该提案就被最终选定。
如下图所示,Acceptor只接受一个发起的提案,并最终选定提案1
虽然这种方式非常简单,但是会出现单点问题,当Acceptor节点宕机后,整个系统就无法工作了。为了避免单点问题,必须采用多Acceptor节点来进行选举提案。
如下图所示,如果多个Acceptor来接受提案,那么最终如何选定一个唯一的提案呢?
推导:
1、首先,我们要保证,当只有一个提案提出时,也能够被选定,那么得出
P1:一个Acceptor必须接受它收到的第一个提案。
2、为了解决上图的问题,提案1被Acceptor1接受,提案2被Acceptor2接受导致最终选定值不唯一,从而得出
一个提案被最终选定需要被半数以上的Acceptor同意选定
3、如果要满足第二点,那么也就意味着一个Acceptor能够接受多个提案,否则提案将无法被选定。在最开始我们认为一个提案的内容只包含被更新的数据value,这样看来只有value无法满足我们的要求。
这时,通过给提案加一个提案ID也叫提案编号来区分是哪一个提案,其中提案ID值按照提案的发起顺序单调递增,这里不解释具体如何生成提案ID。
提案内容由Value变更为【提案ID,Value】
4、由于允许了多个提案被选定,那么问题又来了,如果两个不同值的提案被选定,岂不是无法保证数据的一致性了吗?于是得出所有被选定的提案都有相同的value值。
P2:如果某个value为v的提案被选定了,那么每个编号更高的被选定提案的value必须也是v。
由于提案的选定由Acceptor来进行接受选定,那么可以将P2改为对Acceptor的约束P2a
P2a:如果某个value为v的提案被选定了,那么每个编号更高的被Acceptor接受的提案的value必须也是v。
只要满足P2a必然满足P2, 可得出P2a -> P2
5、此时感觉一切都很好,但是由于分布式系统中,网络波动和机器的宕机情况的发生,可能会出现数据不一致的情况。如下图所示:
其中Proposer1发生了网络异常或者宕机,此时Proposer2进行了提案【M1,V1】,此时系统中的超过半数的Acceptor选定了该提案,
然后Proposer1恢复了,将提案发给了Acceptor1,由于Acceptor没有接受过任何提案,根据之前 P1:一个Acceptor必须接受它收到的第一个提案,此时V2被选定,出现了数据不一致。并且违反了我们P2a的规定,此时V2 != V1。
6、为解决上面出现的问题,从而对P2a约束进一步进行强化,P2a是对Acceptor的约束,而P2b是对Proposer的约束
P2b:如果某个value为v的提案被选定了,那么之后任何Proposer提出的编号更高的提案的value必须也是v。
7、最终证明
P2c:如果一个提案【Mn,Vn】被提出,那么肯定存在一个超过半数已上的Acceptor集合S,满足以下两个条件中的任何一个。
至此,只需要保证P2c,我们就可以保证P2b,从而保证P2和P1,最终保证数据的一致性。
通过上面的推理,我们确定了,当一个新的提案被提出时,提案的value一定为当前被选定提案的Value值。那么Proposer如何能够满足这一点呢?
步骤一(Prepare):
Proposer生成一个新的提案编号M,然后向半数已上的Acceptor集合发送请求,要求每个Acceptor做出ACK响应。
步骤二(Accept):
在收到所有Acceptor的响应后,生成一个提案【M,V】,其中V值由响应结果决定,如果没有Acceptor接受过提案,那么V值自己决定,如果有Acceptor接受过提案,则更新为所有返回提案中编号最大的V。
Acceptor可以忽略任何请求,包括prepare和accept。
Acceptor接受一个提案的条件为:
P1a:一个Acceptor只要尚未响应过任何编号大于M的Prepare请求,那么他就可以接受这个编号为M的提案。
最终可以得出,Acceptor只需要关心目前接受的最大编号的提案,小于该编号的不予以响应,另外关心当前已经响应的提案编号,保证不接受任何小于已经响应提案编号的提案。
当最终提案被确定后,提案的值要被所有的Learners学习同步数据。
那么如何进行数据同步呢?
方案一
当Acceptor确定一个提案后,发送消息给所有的Learners,这样Learners可以第一时间获取到最新的值,但是需要所有Acceptor和Learners进行通信,通信次数为二者个数的乘积。
方案二
当Acceptor确定一个提案后,发送消息给Learners集合中的主Leader,然后由Leader和所有Learners通信,这样可减少通信次数,但是会出现Leader单点问题。
方案三
当Acceptor确定一个提案后,发送消息给Learners集合中的一个子集,然后由子集进行通信,这样是方案一与方案二的折中,既不会出现单点问题,也不会出现通信次数过多,子集的个数越多可靠性越好。但是网络通信复杂度较高
至此,推导过程已经全部完成。我们来简单回顾一下:
1、一个提案的内容,需要包含提案编号M和值V
2、一个提案被选定需要超过半数的Acceptor集合接受
3、Proposer生成提案前先确定当前已经被接受的最大提案值
4、Acceptor不接受小于当前已经接受或响应的提案编号的提案
5、Learners数据同步
通过上面的推导,应该已经了解了Paxos的执行过程。
接下来对Paxos算法流程简单描述一下,整个算法分为两个阶段
1、首先Proposer生成唯一的递增提案编号M,然后向半数已上的Acceptor发送Prepare请求,等待响应。
2、此时Acceptor判断,如果已经想响应过比M大的提案,则对当前提案不予以响应,否则返回已经响应过的提案中编号最大的那个,同时该Acceptor承诺不再接受任何编号小于M的提案。
3、如果Acceptor未超过半数响应,则重新发起提案,如果Acceptor响应内容含有提案,则选定所有返回提案中编号最大的那个,如果没有响应提案,则自己决定提案值V。
1、向半数已上的Acceptor发送Accept请求,期望Acceptor接受提案。
2、如果Acceptor没有对编号大于M的其他提案做出响应,则接受该提案。
如果一个提案超过半数已上的Acceptor接受,那么该提案被最终选定,并且Acceptor向Learners发送选定提案的消息,Learners进行数据同步。
至此,Paxos算法已经全部讲完了,其中可能有人发现了一个问题,那就是该算法活性无法保证,如果有两个提案依次进行提案请求,那么该算法将陷入死循环,无法最终选定一个Value,如图所示:
两个proposer依次提交prepare请求,都会执行响应成功,但是在提交accept请求时,都会被忽略,这样就会造成死循环,无法保证算法活性。
所以为避免这种问题,采用选举一个Leader Proposer来进行提案的提交,这样总有一个Proposer提交提案,就不会出现问题,当Leader Proposer挂掉时,再进行重新选举推选一个新的Leader避免单点问题。
本文已经详细的总结了Paxos算法的推导过程和运行的时序调用流程。可能还有很多细节没有说清楚,请原谅水平有限。
本文中的图为个人所画,如果有引用,请麻烦标注参考本文地址。
参考资料
【1】《Paxos Made Simple》
【2】《从Paxos到ZooKeeper》
网络中部分博客。