paxos笔记
22 Mar 2014 by LelouchHe
问题
分布式系统中必不可少的是replica,利用数据冗余来提高可用性,避免数据丢失.但数据冗余带来了一致性问题,即多个replica可能会由于网络和机器故障等原因造成内部数据的差别.对于一些一致性要求不严格的服务,这也许不是什么问题,比如最后我们可以通过重新dump一遍数据,更新所有的replica即可.但对于其他服务,比如分布式DB,一致性是必须的
如何保证一致性,在这里有了简单的描述,本文主要是介绍其中一个解决方法,即paxos算法
适用前提
paxos适用于非拜占庭将军的异步系统,即:
- 系统中的消息传递可以延迟,重复甚至丢失,但是不能在网络传输中被修改(corrupted)
- 系统中机器是各自独立的,可能随时crash或者重启
其实可以看到,这个场景其实是绝大部分分布式系统的常见场景
paxos服务在系统中的位置
一开始我的理解是paxos服务就是提供数据的servers,clients直接向paxos服务提交请求,然后由paxos进行一致性处理,然后在内部进行具体的数据处理
但也可以将真正的数据servers和paxos服务划分出来,如下图
[][]
整个服务的步骤如下:
- clients向数据servers并行的提交改变数据servers状态的请求(如果都是不改变的请求,就成了只读服务器了,没必要搞一致性)
- 数据servers将请求编号,并向paxos提交请求
- paxos服务使用paxos算法,从多个并行请求中选择一种执行序列,并返回数据servers该序列
- 数据servers获取该执行序列,并按照该序列对数据servers内部的状态进行修改,然后返回clents
- clients可以根据返回的状态,判断是执行成功还是失败,是否需要回滚或者重新提交请求
当然,执行序列只是一个一般的说法,真正的数据servers可以单个单个的串行提交,或者一批一批的batch提交,又或者异步并行的提交请求,这些都不会影响paxos的处理
将数据servers和paxos服务分割开来,主要的目的是为了paxos的复用,即一个paxos服务可以被多个不同的要求数据一致性的数据servers使用.paxos处理的应该是服务内部抽象的请求编号,而不是真正的直接请求
可以说,相对于paxos服务而言,数据servers就是它的clients,提交的请求就是对应的请求编号
角色划分
针对于paxos服务而言,clients(即上面的数据servers)提交的是一些value(这个value是对应的数据servers抽象的请求编号),paxos做的只是决定哪些value被接受了,哪些value被驳回了,至于被驳回的这个value最后如何,这就得看clients对于不同返回状态的处理了,和paxos无关
而在paxos内部,各个机器有这不同的角色划分:
- proposer 同clients交互,并代clients提交请求的机器.注意,这个不是clients,这个是paxos内部的机器,是paxos服务的一部分
- acceptor 接受不同请求,并决议接受哪些请求的机器.注意,这个不和clients打交道,对应的是paxos内部的proposers
- learner paxos服务内部绝对接受哪些请求之后,需要让整个paxos内部进行一致,就是通过learner来learn接受的value
这些角色的划分,并不是物理的划分,而是逻辑的划分.同一个机器可以有多个角色,但我感觉,如果将一个机器的角色安排的过多,有时候不是特别的好,比如一台机器fail之后,多个角色都fail了.当然,这需要各种trade off了,就是现实的问题了
instance的概念
最简单的情景就是从多个value中选取一个value,然后驳回其他所有的value.为了能从多个value中得到一个一致性的序列,可能需要迭代的执行多次上述最简单的过程,每次使value数少1.
上面说的最简单的情景,就是一个instance,后面进行的迭代,就是多个instance的迭代.当然,这里不是说instance只能串行,instance可以像请求一样,并行的来操作
但是要记住一点,instance是各自独立的,相互之间并不影响,每个instance只是决定接受单一的某个value,而这些instance的组合才会决定所有value的最后一致性的安排
instance执行的演变
instance的执行是paxos的核心(synod),即如何从多个value中选择唯一value.这个的算法不是一蹴而就的,而且同后续多instance的执行也有些联系.所以此处,我们选择沿着解决问题的思路来讲解synod的演变,而不是像上面一样,直接给出结论(结论就简单的几句话,但理解需要整个过程来看)
总体原则
paxos的总体判断原则是,如果一个value被acceptor中的majority接受,那么这个value就算被整体接受了
paxos保证的是,只要确实被majority接受,就肯定会被所有的接受.此处的majority不一定指的是数量,如果每个机器的权值不一样的话,还代表权值和的majority.不过我们此处讨论的只是数量,为了简便起见
paxos的算法,就是要通过精心设计的算法步骤,来保证这一点.细分来看,就是要保证下面3点必然能成功:
- 有value可以被接受
- 有value能被majority接受
- 被majority接受的value,最后被全体接受
下面通过讨论这些性质的充分条件,来对算法的性质进一步的约束,从而最后得到算法的具体步骤
可以被接受
单纯考虑这个性质,只要acceptor可以接受请求的value即可.
还需要什么特殊的条件么?
假设paxos服务只有一个proposor和一个acceptor(比如其他角色全部挂掉了),那么此时的acceptor并不能对value有特别的要求,只能是proposor提出什么请求,就接受哪个value,如果非要等待某个特殊性质的value的话,会导致服务陷入无限等待中
而且,instance只要决定选择某个value即可,并不要求value是什么,那么此时最优的选择,肯定是第一个value(因为后面的value存在与否,能否到来都是不可知的)
所以性质1的充分条件,目前可以总结为:
- C1 acceptor只接受第一个请求的value
当然,这只是能够满足条件的一个充分条件而已,在满足要求的前提下,自然是怎么方便怎么处理,比如,我们自然也可以把性质1的充分条件总结为:
- C1a acceptor接受最新请求的value
可以被majority接受
一旦提到majority,必然有多于1个的proposer和acceptor,假设存在P1和P2(分别请求v1和v2),A1和A2.此时的majority就是A1+A2.
按照C1的性质,我们来考虑最理想的情况,P1向A1和A2提出请求,A1/A2都接受了(因为是第一个请求),此时形成了majority,P2再向A1/A2请求时,就直接被拒绝了.此时,就得到了v1作为本次instance选取的最后value.P1成功,而P2失败
但是,还有种非理想的情况,即P1向A1提出了请求并被接受,P2向A2提出了请求并被接受(请求时机和网络之类的因素,很容易产生类似情况),此时,P1/P2的新的请求肯定不会被A2/A1接受(根据C1),或者无法达成一致(根据C1a,P1/P2一直不停的向A1/A2提交请求),这样就无法形成majority,此次就只能hang在这里了.
从这一点来看,C1/C1a的性质是可疑的,即我们无法同时满足C1和majority.majority性质是必须的性质,就说明C1/C1a这个充分条件是不合适的,我们需要重新寻找新的充分条件来满足性质1
- 首先,acceptor是必须能接受多个请求的,否则就是C1带来的问题,一人一半,永远无法达成一致
- 其次,接受的多个请求不能是任意的请求,否则就成了C1a,无限的循环下去,也永远无法达成一致
- 最后,可以看到,我们需要的只是在请求的性质上做限制即可,使得acceptor可以接受满足特殊性质的请求,就能打破hang的局面
所谓特殊性质,简单说来,就是能从请求之间的相对关系来进行选择,最通俗的说法,就是让请求有序,即让各个请求之间存在一定的顺序关系,否则,我们无法在多个类似请求之间进行选择(随即选择倒也是选择,但无法保证一定一致).同时,这个”有序”不能是时间上的顺序,否则就成了C1a,无限的循环下去了.
那么第一步,是个所有的请求进行编号,要求是所有的编号不能重复而且有序.此处,我们简单的假设P1的请求号为n1,P2的请求号为n2,且n1<n2(有序的一种性质),同时,将C1修改为:
- C1b acceptor只接受请求号最大的value
这个性质就可以很好的解决刚才的非理想情况(A1接受P1,A2接受P2),此时P2向A1发送请求,A1比较下请求号,自然就接受了P2的请求,但P1向A2的请求,则被拒绝.此时,P2的请求得到了majority的接受,v2成了majority值.证明此种条件下,有可以被majority接受的value
有没有什么更奇葩的情况呢?P1会不会主动的增加请求号,来争取被接受呢?这是个问题,由于网络原因(接受请求的应答延迟或丢失了),或者被拒绝(有更大的请求号),P1有可能提高请求号来促使自己的请求被接受
但是,这也不是个问题.失败重发机制是非常常见的,我们不能禁止proposer重复发送请求,比如proposer不知道请求接受了没有,还是丢失在网络中,所以没有在一定时间内没有显式应答,肯定需要重发的.
在C1a的情况下,此时的请求肯定就是新的请求了(又回到C1a的无限循环了).当然,我们可以说,”把请求真正产生的时间记录下来不就可以了么?”.自然,如果请求的真正产生时间可以用来比较请求的先后,那么这个就类似于请求编号了,而且,分布式系统中,各个机器的物理时间是最不可靠的,我们不能依赖于近似与各自独立的元素来达成分布式的一致
在C1b的情况下,我们可以避免这种竞争行为,因为重发并不会改变请求的编号,只有在显式得到”被拒绝”的的应答,才会考虑增加请求编号.不过我们为什么要增加呢?本请求被拒绝,说明有别的请求被接受,那么本instance的执行有了选择,就已经完成了执行的本意,所以我们自然可以控制不去重发类似的请求
综上所述,独立不重复编号+C1b+不重发新增编号的请求确实可以保证有value可以被majority接受
可以被全体接受
还是假设如上的情况,A1接受P1,A2接受P2,然后A1接受了P2,A2拒绝了P1,此时,P2成了majority,同时也是全体acceptor,此时P2自然可以宣布说v2是最终的选择,然后让learner学习v2
这算是一种理想情况了,假设如下的情景,A1接受P1,A2也接受了P1(P2没来得及发送,或者被延迟,或者被丢失了),此时P1能否宣布v1被选择了呢?自然不行,假如P2的请求正在路上,紧跟着被A1/A2接受了,那么接受v1肯定就带来了不一致.
此处解释下,为什么A1/A2要接受P2.按照C1b,确实应该接受编号更大的请求.如果我们试图修改C1b为下面的条件:
- 伪C1c acceptor只在没有majority的情况下,接受编号较大的请求
C1c貌似能解决我们的问题,但必须思考的是,A1/A2是独立在分布式系统中的,我们通过什么让A1/A2知道自己接受的value已经是majority呢?让他们之间通信么?进行广播么?自然,这些手段在这里都是不可靠的,而且不能阻止acceptor接受P2.那么能否组织P2进行提交呢?自然也不能,P2的请求可能已经在路上了
我们需要保证的是,当P1的v1被接受之后,不会再有新的value被提交了.通过majority的广播之类是无法解决的(上面已经分析了),我们只能从另一个侧面来解决
问题的根源在于,既然已经有一个value(即v1)被majority接受了,我们为什么还要接受另一个呢?本instance的目的不就是选择一个value么?既然有了,我们自然应该直接进行选择就好,干嘛多此一举再来呢?
既然不能广播majority,那么让proposer提交对应的majority的value也行.现在我们就面对这样一个情况,将请求和value分开提交.先通过一个阶段学习当前可能的majority的value,然后再根据情况进行处理即可
我们引入2阶段的处理:
- 预提交阶段 proposer提交请求号,让acceptor判断是否接受,并返回majority的value
- 真提交阶段 proposer提交请求号,和从上一个阶段得到的value,acceptor进行真正的接受
预提交阶段
所谓”预提交阶段”,就是相互竞争majority阶段,根据C1b,各个请求之间竞争是以请求编号为key的,最大的请求编号最后会被acceptor接受,编号较小的请求会被拒绝.
接受预提交请求后,acceptor应该返回什么value呢?自然,如果没有接受过真提交请求的话,自然是没有value返回,直接返回空.
如果有value被接受过,怎么办?我们可以想象一下,有value是什么情况.既然有value了,证明之前有proposer确认自己的预提交请求被majority接受了,那么它真提交的value在理想状态下应该就是majority的value了,所以我们此处直接返回这个value,告知其他的proposer即可
上面理想的情景假设的是预提交和真提交之间是没有间隔的.假如在真提交全部接受之前,即对应的value真正成为majority之前,有更大编号的预提交请求达到了,怎么办?
一个简单的办法是拒绝,但这就又回到上面的问题,单个的acceptor无法判断是否为majority,如果acceptor有已经接受的value时,该acceptor可以做出判断,但如果没有value,但靠acceptor是无法判断这个的,所以acceptor只能按照简单的C1b来进行判断,而把问题交给proposer来判断
真提交阶段
proposer在上一阶段收到的每个acceptor应答可能有3种:
- 被拒绝 如果acceptor接受过其他编号更大的预提交请求,那么它只能对本proposer进行拒绝了
- 被接受,而且没有value返回 证明返回应答的这个acceptor还没有接受任何真提交请求,所以此时是请求竞争的阶段
- 被接受,且有value返回 证明返回应答的acceptor接受了某真提交请求,value是可能的majority
只有接到majority的成功应答,proposer才会进入真提交阶段,此时应该提交什么value呢?
假设如下的情景,P1向A1/A2预提交成功,接着P2向A2/A3预提交成功,二者都认为自己是majority,而且acceptor都没有返回value,二者都要进行真提交.P1向A1提交成功,P2向A2提交成功.如果此时,又有P3预提交给A3,A3没有办法只能接受,然后P3分别向A1/A2预提交,二者也只能接受并返回对应的v1/v2.此时P3全被接受了,成为majority,P1/P2的真提交会被A2/A3拒绝.
这个时候,P3提交哪个value呢?v1?v2?v3?我们假设几个可能的选择:
- 按照多数原则选择 这个可能是一个通常的选择.返回2个v1,1个v2,按照这个原则,我们选择v1.但此时有新的问题,P3只经过majority的确认,不是所有成功的确认都返回了value,而且,在majority中选择majority,这样的结果很难代表真正的majority
- 按照编号最大选择 这也是通常的选择.因为acceptor就是根据此原则进行接受的,可以想象,小编号的真提交请求肯定会被它自认为majority中的某个拒绝(此例中是A2).这一点比较tricky,但仔细思考一下,会有2个majority同时接受2个编号的预请求么?肯定不会,假设存在这样的接受,2个majority必然大于全集了(a>1/2&&b>1/2&&a/b无交集=>a+b>1),所以小编号的majority之中肯定有acceptor已经接受了大编号的预请求(比如A2)从而会拒绝掉之后所有的小编号请求,我们自然可以安全的忽略小编号的值.
那我们是选择v2,还是P3的新v3呢?这个还需要斟酌.假设P3的预提交比较晚,P2的真提交已经被接受,成为了majority的值,此时v2已经被选择出来了,是否还有必要提交v3呢?个人感觉没有必要,这一方面带来新一轮的迭代,而且如果proposer不停的加入进来,这个过程就永远没有终止的时候了;就算P2还没有成为majority,但P2是当前返回的最大编号请求,不做任何处理,也会在将来获取majority的地位,它代表的value肯定是majority的value,我们提交v3只能提前终止这个情况,并让算法继续迭代下去,得不偿失
记住,我们的目标是选出一个value,不在乎这个value是谁的,只要有一个即可
所以,在提交value时,我们的选择限制在acceptor返回的请求编号和value中,从里面选择最大编号的value来进行提交
此时的acceptor该如何应对呢?有如下2种情况:
- 编号小于acceptor已经接受的请求编号,直接拒绝
- 否则,接受该真提交请求,并更新对应的值
这样,我们可以保证某个value会被全体接受
重复发送请求
在”可以被majority接受”一节中,我们阐述了一个观点,即被拒绝的proposer不再发出新的请求了.这对于当时讨论的单阶段请求来讲,是合理的,但对于目前改进后的2阶段提交有着比较大的冲突
设想这样的情景,P2向A1/A2预提交请求,都被接受,P2挂了.P1向A1/A2预提交请求,被拒绝.采用刚才的策略,P1就不会在发请求了.这样,整个算法就因为P2的挂机hang在这里,而此时,我们还没有可以使用的value
去掉不重复发送的约束,就可以解决这个问题,比如让P1再被拒绝之后,提高请求编号,重新发送预提交请求,此时的A1/A2就可以接受P1的请求,从而继续算法过程不至于hang掉.至于P2哪天又重新恢复,继续真提交请求其value,由于编号太小,也会被acceptor集体忽略的.
这又带来一个问题,P2此时需要重复提交么?因为P2并没有足够的信息了解当时的情况,所以它的情景和P1当时被拒绝是类似的,所以P2肯定是要重复提交的.如果此时A1/A2已经接受了P1的真提交请求,倒也无妨了,P2最多就是重复提交P1的v1而已.但如果P1还没有完成majority的真提交请求,P2就开始重复发送,导致P1无法完成相应的流程.P1就只好继续重复发送请求.(可以看到,在2阶段的任意阶段被拒绝,都是要增大请求编号,并重新发送请求的)
如此往复,算法就又hang住了.不要小看这种情况,在大规模分布式系统里,任何小概率事件都会发生的.
此时怎么办?
一个思路是我们只让一个proposer可以增加请求编号,就是所谓的leader.leader是proposer之一,负责所有的proposer的提交,当被acceptor拒绝后,会主动提高请求编号,来迫使acceptor接受请求.其他proposer不能提交请求,只能将收到的请求转发给leader
谁是leader
leader是怎样选出来的?
可能的做法是把proposer机器进行编号,当前存活机器中编号最高的就是leader.这样看来,leader需要定时的向proposers发送存活信息,来确保自己的leader地位.
但有可能这样的信息延迟或者丢失,导致某些proposer认为leader已挂.我们能选择次高的么?应该不行,最大编号的机器可能没挂,只是存活信息丢失了,或者即使挂了,我们怎么确保次高的没挂呢?难道系统中的每个机器都要不停的广播子集的存活信息,然后确保所有机器都接收到这样的信息么?
显然不可能.
我们需要一种可以选择出leader的算法.我们手边的paxos就可以.
当proposer认为leader已挂时,就开始提出自荐leader的请求.这个请求和普通的请求一样,只是我们不会主动的增加请求编号了.只要被拒绝,就认为leader仍然存活或已经选出,否则,该proposer就成为了新的leader,并对此进行广播
也就是说,在接收到拒绝应答后,proposer因其状态的不同,有不同的反应:
- 普通的proposer: 停止提交(其实它就不该受到拒绝,它应该转发请求)
- leader: 增加请求号,重新提交
- leader候选人: 停止提交,并成为普通proposer
有了这样的策略,就不用担心两个proposer不停的增加请求编号的竞争局面了.在大部分时间里,都只有leader一个proposer进行提交,没有任何冲突,instance执行的很快.只有在leader的存活信息失效或丢失时,才会出现多个proposer竞选的局面,但paxos保证了只有一个proposer会被接受,从而选择出leader,又恢复到原来的情况了
细节提醒
有几点是需要清楚的了解的:
- 请求和value是分离的.proposer虽然代表着client提交请求,但算法的根本目的是选择一个value,而不是请求.所以为了这一点,有的时候某个proposer会提交非自己value的请求,这是为了保证算法的结束.
- 真提交阶段value的来源有2种,要不是还没有proposer提交过,所以自己随便提交(自己的value即可),或者已经有人真提交value了,从中选取最大编号的value即可.
- majority之间必有一个元素.acceptor依赖这个性质把已接受的value放心的返回给proposer,而proposer放心的从返回的majority应答中,挑选最大编号的value进行提交
- 被acceptor接受不代表某value被选择了.只有在value被majority都接受之后,才被成为”选择”.此时根据第2点的限制,保证了之后提交请求的value都是这个value
- 最重要的一点,本instance的执行只是为了选择某个value,并没有说哪个value,所以只要有一个value即可,并不对value有特别的要求
学习阶段
proposer真提交阶段后,受到majority的成功应答,就表明自己提交的value(可能不是自己原本的value)被选择了.此时需要做的,就是通知paxos服务内部的所有learner,让它们学习这个value,作为本次instance的最后结果.
由于是majority接受了,很有可能的情况是,剩下的minority的value还是别的值.但是,我们不关心.因为只有learner学习到的才是真正有用的,acceptor的值在下一次的instance中就无效了,所以我们根本不关系它们的值是多少
由此,我们让成功的proposer通知所有的leaner,并得到成功应答后,就可以顺利的返回该value,告知服务的调用方,新的value是什么了
几个不一样的情况
上面描述的算法和我看到的别的地方的讲解有些不一样的地方,下面列出一些我知道的不同,和我对此的解释:
- acceptor只能为奇数: 这个是在这里里看到的,但我觉得这是错误的.偶数的acceptor同样有majority,并不影响paxos的执行,选择奇数更可能是实现上的原因,而不是算法本质的问题
- 2阶段的本质: 我这里没有采用通常的说法,而是自己感觉出来的结论.我觉得2阶段的本质就是请求和value的分离,以便于proposer判断出真正的majority,从而避免错误的提交.acceptor不关心value是什么,相反,它只关心接受最大编号的请求;proposer也不关心value是谁的,只要提交自己已知的最大编号的value即可(甚至这个value都不是最大编号自己的,也是通过类似途径提交的).二者的正确性由集合的majority必然有交集和majority只接受一个value的限制来保证
- 学习阶段由acceptor发起: 在我看来,算法一直避免的是让acceptor来判断majority,因为这个不可能.相对的是,由于proposer必须获取到majority的成功应答,那么此处才是majority判断的合理位置.学习阶段必须在majority接受真提交请求的value之后,而这一点,只能proposer保证,所以我觉得,这个应该是proposer发起的通知,通知对象是所有的learner