序
参考:
https://www.cnblogs.com/linbingdong/p/6253479.html
https://zhuanlan.zhihu.com/p/31780743
https://zhuanlan.zhihu.com/p/58405630
《Paxos Made Simple》
https://www.microsoft.com/en-us/research/uploads/prod/2016/12/paxos-simple-Copy.pdf
https://www.zhihu.com/question/19787937
附件:
<<paxos-simple-Copy.pdf>>
前言
害,Paxos从大学的时候第一次看到接触,直到现在也没能弄清楚里面每一步的进化史和解决的痛点。
故,重新进行梳理,参考各类blog、wiki、论文写下这篇自己能够理解的文章。
这篇不仅包含了理论,还包含了很多推导过程,造成整个体积比较庞大。
Paxos是什么
Paxos算法是基于消息传递,且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题的最有效算法之一。
除了基于消息传递,还有基于共享内存的。
这个算法收到了很多人的青睐,Google Chubby的作者 Mike Burrows认为只有一种一致性算法,就是Paxos,其他都是残次品。虽然有点夸张,但是说明了Paxos的地位。但Paxos好是好,算法却是晦涩难懂,不去钻研很难读透其每一步的执行过程和解决问题。
背景
Lamport宗师提出一种基于消息传递的分布式一致性算法,使其获得2013年的图灵奖。在1998年中的 《The Part-Time Parliament》论文中首次公开了Paxos,最初的描述使用了希腊小岛Paxos进行比喻,描述了小岛汇总通过决议的流程,并且以此为算法命名,在后续重新发表了更简单的版本《Paxos Made Simple》。(已经在上面参考,在线和离线都有)
强大
基本Paxos问世以来,就持续垄断了分布式一致性算法,Paxos就等同于分布式一致性,使用到的有Chubby、Megastore、Spanner、ZooKeeper、MySQL的主从复制等等都使用该算法进行分布式一致性问题的解决。
特点
生涩难解,更难实现。
Paxos利用了大多数(Majority)机制,保证了2N+1的容错能力,也就说允许集群中2N+1个节点中有N个节点同时出现故障。
问题产生背景
在常见的分布式系统中,总会发生很多机器宕机、网络波动、消息阻塞丢失等等。Paxos需要解决的就是在上述可能发生的所有情况内,怎么快速且正确的达成在集群内部对某个数据的值达成一致性,并且保证无论发生什么异常,都不会破坏整个系统的一致性。
也就是说Paxos算法允许运行在宕机故障的异步系统中,不要求消息的可靠传递,甚至消息丢失、延迟、乱序、重复等等情况都能够使用。
某个数据的值可以是一个值,可以是一个文件,一条message,一句命令等等。
基础概念
在Paxos算法中有三个角色:Proposer、Acceptor、Learners
一个提案 Proposal,最终达成一致性的值就在这个提案里面。
在具体的实现中,一个进程可能会扮演多个角色。(在多副本状态机中,每个副本同时具有Proposer、Acceptor、Learner三种角色)
暂且认为提案就是value,即提案质保函value。在我们接下来的推导过程中会发现如果提案质包含value会出现一些问题。
暂且认为 Proposer可以直接提出提案。在我们接下来的推导过程中,会发现如果Proposer直接提出提案会有问题,需要增加一个学习提案过程。
Proposer 提出propose; Acceptor可以接受accept提案;如果某个提案被选定chosen,那么这个提案中的value就被选定了。Learner只负责最终提案的学习。
所以 对某个数据的值达成一致 就是指的是Proposer、Acceptor、Learner都认为同一个value被选定chosen。那么他们选定某个value的条件如下:
Proposer:只要Proposer发的提案被Acceptor接受(刚开始先认为只需要一个Acceptor接受即可,在推导过程中发现需要半数以上的Acceptor统一才行),Proposer就认为该提案中的value被选定。
Acceptor:只要Acceptor接受了某个提案,Acceptor就任务该提案里的value被选定了。
Learner:Acceptor告诉Learner 哪个value被选定,那么Learner就认为哪个value被选定。不参与决策
问题描述
假设有一组可以 Propose value 的进程集合。一个一致性算法需要保证提出的这么多value中,只有一个value被选定。如果没有value被提出,那么value就不会被选定;如果有一个value被选定,那么所有进程都应该 learn到这个被选定的value。对于一致性算法,安全性 safety要求如下:
只有被提出的value才能被选定。
只有一个value被选定
如果某个进程认为某个value被选定了,那么这个value必须是真的被选定的。
我们不去精确定义其 活性liveness 的要求,我们的目标就是保证最终只有一个提出的value被选定。当一个value被选定后,进程最终也能学早这个value。 这个就是Paxos的目标。
假设不同角色之间可以通过发送消息来进行通讯,那么:
A 每个角色以任意的速度执行,可能因出错而停止、重启、断开服务等。一个value被选定后,所有的角色可能失败然后重启,除非那些失败重启的角色能记录某些信息,否则等他们重启后无法确定被选定的值。
B 消息在传递过程中可能出现任意时长的延迟、重复、缺失。但保证消息不被破坏、篡改等问题(拜占庭将军问题)。
推导过程
假设:Acceptor只有一个
假设只有一个Acceptor 多个Proposer,只要Acceptor接受到它的第一提案,则该提案被选定,该提案中的value就是被选定的value,这样只有这个value被选定。
但是如果这个Acceptor出现宕机,那么整个系统就无法发进行工作。
所以为了满足条件A,必须存在多个Acceptor
假设:Acceprot有多个
多个Acceptor接受value值,假设只有一个Proposer提出了value,那么这个value会被所有Acceptor接受。
那么得到第一个约束:
P1:一个Acceptor必须接受它收到的第一个提案。
这个时候,如果有多个Proposer分别提出不同的value,给了不同的Acceptor。根据P1规则,Acceptor分贝接受自己收到的value,那么就会导致不同的value在各个Acceptor手中。
那么上述时候到底以哪一个Acceptor的值作为最后的输出方案呢?这个时候就需要引入一个规定:
规定:一个提案被选定,必须要被超过半数以上的Acceptor所接受
这样就避免了一个集群出现多个value值的情况,如果有多个值,则认为value并没有被最终选定。那么当出现多个value的时候,怎么进行推导向最终值的目的呢?
【一个Acceptor必须能够接受不止一个提案】
如果一个Acceptor能够接受多个提案,那么他就不能仅仅保存value的形式,我还需要一个编号,来表达这个提案的版本号,也就是说 提案的形式必须发生更改:
提案=value+版本号
版本号怎么进行选择?后续的提案编号都是用M进行代替,value使用V进行代替。M必须是递增的!
版本号可以考虑为 时间戳+ip(本机/线程标示符)
选定后的更新
再次回顾一下,如上图,3个propose被三个acceptor接受,如果每个acceptor不能接受多个提案,那么就无法进行最终value一致性的选择。如果 提案仅仅保存value,那么就无法管控提案的版本号,可能造成循环(如a到b,b到c,c到a不停覆盖提案)。总结上述后继续推导。
当前虽然允许多个提案被选定,但必须保证所有被选定的提案都具有相同的value值,那么如何在Acceptor接收多个值的时候进行选择保留一个value呢?新增约束:
P2:如果某个value为v的提案被选定了,那么只有更高编号M的value才能够被选定。
一个提案被Acceptor接收才可能被选定,因此可以写成:
P2A:如果某个value为v的提案被Acceptor选定,那么只有更高编号M的value才能够被Acceptor接受。
考虑下一下情况:
假设总共有5个Acceptor/Proposer。 Proposer2提出[M1,V1]的提案,Acceptor有半数以上接受了这个提案(假定2435),于是对于Acceptor2435来说,[M1,V1]就是最终的选定。Acceptor1刚刚从宕机中恢复过来,此前并没有接受到任何提案,Proposer1向Acceptor1发送了[M2,V2]的提案(V2!=V1&&M2>M1),对于Acceptor1来讲,这个就是接受到的第一个提案,根据P1,必须接受这个提案。
那么就会存在Acceptor1接受V2,而Acceptor2345接受V1,出现了不一致;并且P2A也不能约束到这种情况。
因此我们需要对P2A进行约束泛化,将强化转移到Proposer身上:
P2B:如果某个为value的提案被选定了,那么之后任何Proposer提出的编号更高的提案的value也必须是value。
那么,如何确保在某个value提案被选定后,Proposer提出的编号更高的提案都是value呢:
P2C:对于任意的M和V,如果提案【M,V】被提出,那么存在一个半数以上的Acceptor组成的集合S,满足一下两个条件中的任意一个:
S中每个Acceptor都没有接受过编号小于M的提案。(这个M是最小的提案,根本不能够影响集合S)
S中Acceptor接受过的最大编号提案的value。(这个M是是S集合中版本最高的,且V都是value)
Proposer生成提案
刚刚讲了四个P2都是为了如果已经选定value,那么进行更新的时候必须的约束条件(约束Acceptor、Proposer)。
对于P2B来说,Proposer为了满足这个条件,就必须去查询已经被选定或者可能被选定的value,然后以这个value作为自己的提案来进行提出。如果value并没有被选定,那么Proposer才可以指定只记得value,这样才能慢慢归一化。
这个查询学习过程,是通过Prepare请求实现。
可以提炼出提案的生成算法:
- Proposer选择一个新的提案编号M,然后向某个Acceptor集合S(半数以上)发送请求,请求集合S中每个Acceptor对这个【M,V】做出如下的响应reponse:
- 向Proposer承诺,不接受任何小于M编号的提案。
- 如果Acceptor接受过提案,那么就向Proposer响应已经接受过的编号小于M的最大编号提案和Value。
我们将这个称之为 编号为M的Prepare请求。
- 如果Proposer收到了半数以上的Acceptor的响应,那么他就可以生成编号为【M,V】天,这里的V是所有响应中编号最大的提案的value。如果半数集合S并没有进行value值的响应,那么Proposer就可以自己进行选择。
生成提案后,Proposer会将提案发送给半数以上的Acceptor(可以并不是集合S),并期望这次集合中的Acceptor能够接受这个提案。
我们将这个称之为Accept请求。
Acceptor接受提案
由于以前说过的网络故障、丢包、错误等等,Acceptor可以忽略任何请求(上面提到的prepare请求&Accept请求),并且不用担心任何的安全性影响。那么我们就仅仅讨论什么时候Acceptor进行响应就行。
我们对Acceptor接受提案给出以下约束:
P1A:一个Acceptor只要尚未响应过任何编号大于M的Prepare请求,那么他就可以接受这个编号为N的提案。
如果Acceptor收到一个编号为M的Prepare请求,在这个之前已经响应过编号大于M的其他Prepare请求。则根据P1A,Acceptor就不能接受M的提案。因此Acceptor可以直接忽视,或者返回一个ERROR(返回已经接受过的编号小于M的提案与Value)。
所以在Acceptor的世界里,只需要记住:
- 已经接受的编号最大的提案。
- 已经响应的最大请求编号。
我接受过比你编号M更小的编号的提案,但我能够响应你,并且答应你忽视小于等于M的Prepare请求和小于M的Propose请求(这里后者没有等于,因为我要接收你的Propose请求),并且告诉你我已经接收到过的更小编号的提案的【M,V】。
Paxos算法描述
上述都是对约束进行的推导,现在总结下Paxos的算法流程。Paxos分为两个阶段:
阶段一:
Proposer选择一个提案编号M,然后向半数以上的Acceptor发送编号为M的Prepare请求。
如果Acceptor收到了一个编号为M到的Prepare请求,且M大于Acceptor已经响应过的所有的Prepare请求编号,那么它就会将他自己已经接受过的最大提案进行返回(如果没有就返回空),并承诺不再接受任何小于M的提案。
阶段二:
如果Proposer收到半数以上的Acceptor对其发送的编号为M的Prepare请求的响应,那么他就会发送一个针对【M,V】提案的Accept请求给半数以上的Acceptor(并不一定是刚刚的集合)。注意:V为阶段一Acceptor响应编号中最大的提案的value,如果都返回的为null,则V由Proposer自己决定。
如果Acceptor收到了一个针对编号为M提案的Accept请求,只要该Acceptor没有对编号大于M的Prepare请求作出过响应,他就接受这个提案。(V呢?)
Learner学习被选定的value
Learner学习被选定的value有如下三种方案:
第一种为广播式:
发送给所有Learner进行学习。那么会造成Acceptor*Learner次网络信息量,信息量过大。
第二种为Leader式:
发送给主Learner,主Learner发送给其他。这样就会形成单节点故障(解决参考Multi-Paxos)
第三种为ISR式:
小集合学习后通知大集群。
如何保证Paxos算法的活性
假设有两个Proposer依次提出编号递增的提案,最终会陷入死循环,没有value被选定。
Proposer1 发出 M1 prepare请求,收到过半的响应,完成阶段一。
同时Proposer2发出编号为M2的请求,也收到过半响应,完成阶段一。
Proposer1发出Accept请求,没有半数接受,重新发起M3 prepare请求。
Proposer2发出Accept请求,没有半数接受,重新发起M4 prepare请求。
……
两个Proposers交替的Prepare请求成功,导致Acceptor无法接受,造成Accept失败,形成活锁(Livelock)。
通过选取主Proposer,就可以保证Paxos算法的活性。
至此,我们能够得到一个既能保证安全,又能保证活性的分布式一致性算法。
伪代码实现
Multi-Paxos
在 Basic Paxos 中,只能对一个值进行决议,决议的形成至少需要两次网络的来回。在高并发的场景下会需要更多的网络来回,甚至在上面的Proposer交替Prepare成功还会造成活锁的形成。
那么在实际躬工程中,需要确定多个值,需要更高的效率进行,所以产生了Multi-Paxos算法,对Paxos算法进行了两点改进:
Instance ID 标识
针对每一个要确定的值,运行一次Paxos算法实例(Instance),形成决议。每一个Paxos实例使用唯一的Instance ID标识。
Leader
在所有Proposers中选举一个Leader,由Leader唯一地提交Proposal给Acceptors进行表决,这样没有Proposer竞争,快速高效解决活锁问题。甚至由于只有一个Leader Proposer,那么prepare阶段都可以跳过,直接进行提交。
过程
也就是说Multi-Paxos首先需要进行Leader的选举,Leader的选举也可以认为是一次决议的Value的形成,直接使用Basic-Paxos进行选举。如果宕机重新进行Basic-Paxos的选举。
容错
多个Leader会有问题么?多个Leader其实如果还是二阶段提交提案,那么就退化成为Basci Paxos而已,不影响其安全性。
总结
Paxos分为三个阶段:
Prepare阶段:
Proposer们向Acceptor发送Prepare请求,Acceptors针对收到的Prepare请求进行Promise承诺。
Accept阶段:
Proposer们根据接收到的Promise承诺后,考虑是否进行Propose请求,Acceptor针对收到的Propose请求进行Accept处理并返回结果。Proposer接收到结果后知道自己是否成功。
Learn阶段:
决议成功后,Acceptor会将最终提案Value发送给所有Learner
在Proposer中:
想要发出提案必须先进行一次学习,发送Prepare请求,试探后根据返回V进行Propose请求。
在Acceptor中:
接受一个提案需要承诺别人一些东西,Prepare请求则不再接受小于等于这个prepare的请求,并且不接受小于Propose请求。
提问
Paxos算法中,有几个角色,分别是什么作用。
Paxos算法中,有几个阶段,每个阶段的作用。
Paxos的缺陷是什么,怎么进行弥补。
你能写一个Paxos的伪代码么?
后记
终于写完,也重新梳理完所有,其实在各个博客中也能搜索到很多相关文章,但读起来还是不太行,自己写个,也不太行,后续可以写个精简版,进行整理。
Raft也是分布式协议,keep studying。