paxos

https://www.zhihu.com/question/19787937
作者:朱一聪
链接:https://www.zhihu.com/question/19787937/answer/82340987
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

作为一个因为毕设和这个密切相关从而有了解的人表示,paxos本身并不复杂,在<<paxos made simple>> Lamport用两段话就描述清楚了它的流程。他老人家也说paxos其实是个简单的算法。但是是我在工程领域见过最为精妙的算法。我想论述Paxos为什么难以理解会比描述Paxos的流程长的多的多。我最初学习Paxos是从《从Paxos到Zookeeper:分布式一致性原理与实践》,看的时候看了两遍头略晕,似懂非懂。然后某天晚上我在床上睡不着,凭着印象自己尝试推了一遍basic-Paxos推通了,那时我感觉我想明白了。后来我决定把Zookeeper的类似文件系统的上层数据模型换成更为直接的Java对象,下层用Raft协议维护一致性日志,重写个类似的系统,作为我的毕设题目。于是我看了蛮多的相关论文,特别得提一下的是Diego Ongaro那篇300页的博士论文《CONSENSUS: BRIDGING THEORY AND PRACTICE》,真是写不动了就翻翻的良作;也看了Zookeeper的源代码。最后自己YY的系统写的能跑的,性能还不错,靠不靠谱吗我觉得是不靠谱的,论文也码完。现在想想当时我觉得我懂了,简直是扯淡。期间无数次我都有一种我懂了的感觉,然后又被打脸。现在回过头来看那本《从Paxos到Zookeeper:分布式一致性原理与实践》,关于ZAB协议和Paxos介绍的章节,就真心不敢恭维了,作者只是翻译了下<<paxos made simple>>和ZAB的两篇论文的关键章节,完全没有用自己的理解和语言来组织内容。不仅没有讲清ZAB两篇论文之间的关系,也搞混了paxos和ZAB之间的关系。在我所见过的资料里,现在看来没有任何地方关于Paxos的描述比<<paxos made simple>> Lamport的那两段话更为精确和简要。 任何号称浅显易懂的解说Paxos算法的描述,最多只是让你更好的入门,要更深的理解Paxos以及所有等同于它的一致性协议,ZAB,Raft,ViewStamp,你需要的是直接阅读相关论文,比较,质疑,与人探讨,最后实现它。

好吧下面试图用尽量简短的话描述下paxos和它的原理(为何它是正确的),而为了描述为何它是正确的,我们尝试自己导出paxos=/=。当我想到Paxos其实可以假装这么推导出的时候,我以为推导过程是简短的,因为关键点就这么几个,但是当我写完之后我发现好长啊,然而我已经改不动了。

让我们考虑如下的场景:有一组进程p1,p2.....pn,一个变量v。所谓的一致性问题就是:如何让这组进程就变量v的值达成一致。为了解释何谓达成一致,考虑如下可能,p1令v=a,p2令v=b,那么显然进程p1和p2就v的值没有达成一致。如果p2令v=a,那么认为p1,p2就v的值达成一致。显然让各个进程对变量v的值达成一致需要两个过程:(1)给变量v选择一个值,假设是c(2)决定v的值为c,即让各个进程都认为v=c。

首先我们介绍一个性质,称之为法定集合性质。我们将一个超过半数的集合称之为法定集合,比如数字1,2,3,4,5,共5个元素,{1,2,3}有三个元素就是法定集合。 法定集合性质:任意两个法定集合,必定存在一个公共的成员。
接着我们直接给出v的值被决定的定义: 当法定集合的进程 令v为某个相同的值,比如都是c,那么称v的值被决定为c 。一旦变量v的值被决定了,那么变量的值不可更改,按照之前达成一致的定义,最终所有的进程都会认为v=c,这个性质称之为安全性。


然后我们举例说明这个定义可能导致的问题,假设进程全集为p1,p2,p3。p1和p2都认为v=c,那么我们认为v的值被决定为c。如果此时p3令v=d,那么这组进程显然没有就v的值达成一致。因此p3只能令v=c。我们可以导出一个结论, 一旦变量v的值被决定为c,尽管一个进程pi尚未给v赋值,但是pi已经没有了给变量v随意赋值的自由,pi只能选择值c赋予给变量v

pi如何得知v已经被决定了或者如何选择值c赋予给变量v呢,一个最傻的办法是pi向其它 所有进程询问你是不是已经给v赋值了,值是多少。收集到所有结果后,pi自然可以判定v的值是否被决定并且得知v的值被决定为c。问题在于,进程是可能崩溃的,例如进程pj崩溃了,那么pi还要不要等待pj的回复呢,不等待,当整个集合只有三个进程pi,pj,pk,且pk和pj记录了v=c时,pi由于无法判定pj是否记录了v=c,因此 无法直接判定v的值是否被决定;等待,pj崩溃后如果没有恢复,pi就会一直等待。

我们假设v的值已经被决定为c,根据变量v的值被决定的定义,存在一个法定集合Q1,Q1中每个进程都记录了v=c。 我们让进程pi向一个法定集合Q2中的每个进程询问v的值,这样我们就可以允许少数的进程崩溃。根据法定集合性质 ,Q1和Q2必然至少有一个公共的进程p。p记录了v=c,p告知pi v=c,存在这样的情形:Q2中有另一个进程pk告知pi v=d, 关键的问题是为何pi要相信p,而不是相信pk,从而挑选p记录的值赋予给变量v。我们称这个问题称之如何选值。
上面我们已经明确了这样一步: 一个进程pi需要先向一个法定集合中的每个进程询问它们v的值,这一步称为询问,再从中挑选某个进程v的值作为自己的v的值。考虑v的值还未被决定时的情形,比如初始时,这个时候进程pi需要有自由赋予变量v任何值的权利,因此我们规定, 当pi完成询问后,如果法定集合中的每个进程告知pi它们都未给v赋予值,那么pi有自由赋值的权利。随之产生的问题是 如果有两个进程pi,pj同时询问,并同时获得了自由赋值的权利,如何保证安全性。考虑如下的情形,当获得自由赋值的权利后,pi令v=c,pj令v=d,pi试图向一个法定集合Q3写入v=c,pj试图另一个法定集合Q4写入v=d,使得v的值被决定,同时Q3和Q4必然至少有一个公共成员q2。为了安全性,我们必须保证 pi和pj中只有一个的写入企图能够彻底实现如果作为公共成员的进程q2能够只接受一个进程的写入,拒绝掉另一个进程,那么我们就可以保证这一点。 这个问题称之为如何拒绝。


为了满足安全性,上面遗留了两个问题,如何选值和如何拒绝。
为了让描述更清晰,我们赋予进程两种角色,提议者和接受者。提议者负责处理如何选值,接受者负责处理如何拒绝,显然一个进程可以同时有两种职能,顺便我们总结下两者分别负责的所有事情。
提议者:提议者首先向接受者进程进行 询问,得到一个法定集合的进程的回复后,如果法定集合的进程均未给v赋予值,那么提议者拥有自由赋值的权利,不然提议者从回复中选择一个值赋予给v。
当询问结束后,提议者选择或者自由的给v赋予某个值,例如c后,提议者尝试将v=c写入到一个法定集合的进程,从而令v=c被决定。不防令接受者角色负责写入v的值。 我们将这种提议者进程尝试令接受者写入自身v的值的行为叫做提议,这个过程中提议者发送给接受者的消息称之为提案,显然提案中包括了提议者自身的v的值。
接受者:接受者负责处理提议者的询问和提议。上面我们已经导出接受者必须有能力拒绝提议者的提议才能保证安全性。

我们先考虑如何拒绝。重新考虑这个场景:提议者pi,pj在询问之后,都获得了自由赋值的权利,对于变量v,pi尝试让法定集合Q3的接受者们接受提案pq1(令v的值为c),pj尝试向法定集合Q4的接受者接受它的提案pq2(令v的值为d)。
Q3与Q4至少存在一个公共的接受者q2,上面我们已经得出关键之处在于进程 q2必须能够拒绝掉其中一个提议者的提案。目前我们的流程并没有提供任何信息让q2能够做到这一点,提案中只包括了提议者建议的v的值。如果本身存在一个 唯一的数字来标识这个提案,不妨称为proposer-id,那么q2可以根据提案proposer-id的大小从pq1和pq2当中选择一个接受,不妨选择proposer-id大的议案来接受。我们令接受者q2用变量 a-proposer-id记录它接受过的提案的proposer-id如果一个议案pq的proposer-id>a-proposer-id,那么接受者q2接受这个提案,并更新a-proposer-id=pq.proposer-id。不防假设pq1.proposer-id>pq2.proposer-id,有两种情形:(1)q2先收到提案pq1。(2)q2先收到提案pq2。情形一,显然q2将拒绝后到的提案pq2;但是情形二,q2接受pq2后也会接受后到的提案pq1,这违背了我们的目标。,根据我们目前为止的规则,看起来似乎没有办法解决这个问题,情形二中q2无法拒绝pq1,但是如果q2能够拒绝pq2呢?这也符合我们的目标。q2如何拒绝pq2呢?如果q2收到提案pq2时,此时接受者q2.a-proposer-id=pq1.proposer-id,那么就能做到这一点。然而q2并未收到议案pq1, 如何能令q2.a-proposer-id=pq1.proposer-id?显然只有提案pq1的提议者p1能够预先知道pq1的proposer-id,而p1在提议之前,还有一个询问阶段,只要在询问阶段提议者p1告知接受者q2它在提议阶段将提出的议案的proposer-id,q2记录下这个proposer-id,那么就可以做到这一点。 我们将询问阶段提议者发送给接受者的消息称之为预提案,预提案包含了即将发送的提案的proposer-id,它的作用就是告知接受者在下一阶段该提议者的提案的proposer-id。

整理一下上面说述, 接受者在收到提议者的包含它的proposer-id的预提案后,会设置它的a-proposer-id=proposer-id。如果接受者每接受一个预提案,就更新它的a-proposer-id,那么对于上面的例子中的情形二,无法保证q2.a-proposer-id=pq1.proposer-id。例如接受者q2先收到提议者pi的预提案ppq1,之后收到另一个提议者pj的预提案ppq2,再收到pj的提案pq2,此时q2.a-proposer-id=ppq2.proposer-id=pq2.proposer-id。我们的目的是令提议者pi和pj同时提出预提案ppq1和ppq2时,无论ppq1和ppq2的到达顺序如何,最终q2.a-proposer-id=ppq1.proposer-id=pq1.proposer-id。我们已知ppq1.proposer-id>ppq2.proposer-id,因此自然的我们只要加一个约束, 对于一个预提案ppq,当接受者q.a-proposer-id<ppq.proposer-id时,才更新q.a-proposer-id=ppq.proposer-id。这样如果q2后收到pj的预提案ppq2,此时q2.a-proposer-id=q1.proposer-id>q2.proposer-id,将拒绝预提案ppq2。考虑如下的情况,提议者发送预提案ppq给接受者q,q尚未接受任何预提案和提案,故q.a-proposer-id=ppq.proposer-id,系统中不存在其它提议者,因此pq受到法定集合的接受者回复发送提案pq给接受者q,由于此时pq.proposer-id=ppq.proposer-id=q.a-proposer-id, 我们之前的约束要求pq.proposer-id>q.a-proposer-id,这会导致q拒绝提案pq,只存在一个提议者的系统,提议者的提案竟然无法被接受,这显然不合理,因此修改约束为pq.proposer-id>=q.a-proposer-id。上面我们已经彻底保证如果提议者pi和pj在询问之后都获得了自由赋值的权利,无论接受者q2以如何的顺序接受到它们的预提案和提案,q2只会接受它们中一个的提案。显然p1和p2可以是任意两个进程,c和d可以时任意两个值,因此目前为止协议已经保证如下的一点: 任意时刻,就算存在多个提议者在询问之后提出了不同的值的提案,最终只有其中一个提议者的提案中的值将会被法定集合的接受者接受,即只有一个值能够被决定。

回顾下目前为止我们的协议对于如何决定变量v的值的流程:
提议者:
询问:发送包含自身proposer-id的预提案给接受者,得到一个法定集合的接受者的回复后,如果法定集合的接受者均未给v赋予值,那么提议者拥有自由赋值的权利,不然提议者从中选择一个值赋予给v。假定自由赋予或者选择的值为c。
提议:发送包含c和proposer-id的提案给接受者。
接受者:
处理询问:如果收到的预提案ppq.proposer-id>a-proposer-id,那么更新a-proposer-id=ppq.proposer-id,接受这个预提案,不然则拒绝这个预提案。
处理提议:如果收到的提案的pq.proposer-id>a-proposer-id,那么更新a-proposer-id=pq.proposer-id,接受这个提案,记录v的值为提案中的值,即v=pq.c。

提议者在询问阶段需要得到一个法定集合的接受者的回复后才进行选值,因此接受在处理询问时,如果接受预提案,就会回复一个消息给提议者,称这个消息为 诺言,诺言中包含了接受者记录的v的值

似乎我们只剩下了最后一个问题如何选值。上面最初我们已经论证了当变量v的值被决定为c时,提议者pi在询问阶段收到的法定集合Q2的接受者的回复中,必然存在一个接受者q2回复了它记录了v=c。选值的关键在于 我们从什么地方来判断q2记录的v的值才是v被决定了的值。接受者必须向提议者 额外的信息来使得接受者有能力做出这种判断。我们重新回到v的值被决定的定义,v的值被决定为c,代表存在一个法定集合Q的接受者,记录了q.v=c。由于我们上面已经丰富了我们的协议,并引入了许多关键的概念,所以此时接受者还记录了自身a-proposer-id。我们假设是议案pq最先另q.v=c,由提议者p在提议阶段提出。 那么v被决定为值c后,Q中每一个接受者:q.v=c,并且可以论证q.a-proposer-id >=p.proposer-id。论证如下:当q接受议案pq时,q.a-proposer-id=pq.proposer-id=p。而q.a-proposer-id更新的条件是pq.proposer-id>=q.a-proposer-id和ppq.proposer-id>q.a-proposer-id,故q.a-proposer-id单调递增,故结论成立。
假设pi收到的法定集合Q2的接受者的诺言中,qj回复的诺言中v=d。我们要相信q2记录的值才是v被决定的值而不是qj。不妨假设提出议案pqj令qj.v=d的提议者是pj,pqj向法定集合Q3发送提案或预提案的接受者。我们已经引入了预提案的阶段,一个提案pq被提出,代表存在一个集合Q, 对于Q中的任意一个进程q.a-proposer-id>=pq.proposer-id。这一点论证非常容易,预提案阶段会使得如果q.a-proposer-id<ppq.proposer-id,那么q.a-propposer-id=ppq.proposer-id,同时只有收到的提案或预提案的proposer-id大于q.a-proposer-id,q.a-proposer-id才会更新,即q.a-proposer-id是单调递增的,故得证。根据这个结论,我们可以得知Q3 ,Q3中任意一个接受者q,q.a-proposer-id>=pqj.proposer-id,又知存在一个法定集合Q,Q中任意一个接受者q都接受了pq, 而Q3和Q至少存在一个公共接受者qk,qk接受了提案pq,又收到了预提案ppqj。我们假设pqj.proposer-id=ppqj.proposer-id>pq.proposer-id。 如果qk先收到预提案ppqj,那么代表qk收到pq时,qk.a-proposer-id>=ppqj.proposer-id>pq.proposer-id,qk就会拒绝pqj,与Q中每个q都接受了pq矛盾。如果qk先接受了提案pq,再接受预提案ppqj,那么qk会回复一个包含v=qp.v=c的诺言给qj,即提案pqj的提出在v的值被决定后。 根据归纳原理,假设我们的选值策略有效,那么我们的策略会使得qj选择值c作为提案pq的值,即pq.v=pqj.v=c,与假设qpj.v=d!=c违背。此时可以认定假设pqf.proposer-id>qpj.proposer-id必然不成立, 这样我们确信pqj.proposer-id<pqj.proposer-id。注意我们已经得知了一个非常关键的信息:pi收到的法定集合Q2的接受者的诺言中, q2回复的诺言的值对应的提案的proposer-id大于其它v的值不是c的诺言对应的提案的proposer-id。因此我们只需要从收到的诺言中挑选对应的提案的proposer-id最大的诺言,假设是pro-k,必然pro-k.v=c。诺言由接受者在处理询问阶段回复给提议者,因此接受者在处理提议阶段记录v为收到的提案中的值外,还要记录这个提案的proposer-id,事实上就是把整个提案记录下来。现在我们又保证了 当v的值被决定为c后,之后任意的提议者提出的提案的值也是c。

目前整个协议已经完整了,我们用目前为止导出和定义的所有约束和概念重新描述这个协议,同时我们改称接受者处理询问的阶段为承诺,处理提议的阶段为接受。对于变量v整个一致性协议如下:

提议者:
询问:发送包含自身proposer-id的预提案给接受者,得到一个法定集合的接受者的承诺后,从中挑选出承诺对应的提案的proposer-id最大的承诺,选择这个承诺的值作为v的值,如果这些诺言没有对应的提案(表明接受者尚未在接受阶段记录任何提案),就自由赋值给v。假定自由赋予或者选择的值为c。
提议:发送包含c和proposer-id的提案给接受者。
接受者:
承诺:如果收到的预提案ppq.proposer-id>a-proposer-id,那么更新a-proposer-id=ppq.proposer-id,接受这个预提案,回复包含在接受阶段记录的提案的诺言给发出预提案的提议者。不然则拒绝这个预提案,直接。
接受:如果收到的提案的pq.proposer-id>a-proposer-id,那么更新a-proposer-id=pq.proposer-id,接受这个提案并且记录这个提案。假定用a-pq来记录提案,即令a-pq=pq。当接受者没有接受过任何提案时,a-pq=null。

上述的协议有了如下的两个保证:
(1)任意时刻,就算存在多个提议者在询问之后提出了不同的值的提案,最终只有其中一个提议者的提案中的值将会被法定集合的接受者接受,即只有一个值能够被决定。
(2)当v的值被决定为后,假设被决定为c,之后任意的提议者提出的提案的值也是c


保证(2)可以得出多个提议者提出不同值的议案只有在v的值未被决定时,当v的值未被决定时,保证(1)又保证了v的值会被决定为一个唯一的值,假设是c。而v的值被决定为c后,(2)又保证了之后的提案的值都是c。因此最终所有的进程都会得知v的是c,这就达成了一致,保证了协议安全性。

上面这个协议就叫做Paxos=、=。描述和<<Pa本一致了。上面的导出协议过程的先验就是本来就理解Paxos=、=,假装从头推导出协议只是为了更好的理解Paxos。

题外话:
(1)上面的论证是不严谨的,不严谨的地方在于并没有清晰的定义例如当v的值被确定后,这种先后关系。实际上判断v的值被确定后还是确定前的严格意义上的分界线并不是法定集合的接受者接受v的同一个值这个事件的先后点。同时提出议案的先后严格意义上的分界线也并不是提议者提出以后这个时刻点。这两件事的先后关系的严格定义要更加微妙一点=.=。如果要严格的证明可以参照原作者大神的论文<<Paxos made simple>>和<<Fast Paxos>>中对基本Paxos的回顾。

(2)上面的算法是存在无法终止的可能性的,想象下多个提议者同时提出议案,每个议案中的值被决定前后又一个提议者以更大的proposer-id提出议案。实际上FLP结论已经指出,哪怕容忍一个进程的崩溃,在一个异步的系统中任何一致性算法都存在无法终止的可能性,就不要怪Paxos在容忍了少数派进程崩溃的前提下,无法真正保证活性。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值