好久不写Blog,趁着期末要交论文,写一篇算法学习笔记。Paxos算法的论文是这一年里少有的让我看了之后感到兴奋、orz膜拜的论文(上一次看这样的论文是Google的MapReduce,也促成了我的本科毕业设计论文顺利完成),可能是因为我孤陋寡闻,平时看的论文很少@.@
这篇文章我写了3天,所有文字都是原创手敲,可能里面有很多内容比较啰嗦,因为我在学习Paxos的时候晕了很长一段时间,当时我在做一个介绍Paxos算法的ppt,里面设计了一个实例(就是后面说到的如何决定操作系统课程的考试形式),这个实例我也是反复改了又改,因为每一天都在发现自己对算法理解有误,姿势不够优雅╮(╯_╰)╭
所以我把这篇笔记尽可能写得详细,希望能分享给对分布式感兴趣的朋友,我非常希望有人能耐心地读完这篇笔记,对本文中错误的观点提出批评和指正。
OK~Let’s see how can Paxos be so coooooooooooooooool!
在分布式系统中,数据需要备份,有备份就牵涉到数据同步,数据一致性,特别是一些要求强一致性的系统。假设我们的教务系统有多个服务器节点,课程考试信息被备份到多个副本节点上,每次学生查询考试信息的时候,查询请求会被发到任意一个节点上,一旦有人对考试信息进行了修改,必须保证所有节点都收到了这个修改指令,这样才能避免一些学生查询到开卷考试,一些学生查询到闭卷考试。如果有多个人同时对考试信息进行修改,则很容易造成混乱。
一个比较直观的解决方案是,设置一个master节点,比如李老师是一个master,然后有3位学生,我、帆帆、田田,针对操作系统这门课程的考试形式分别发起了互不相同的修改提案,我希望开卷考试,帆帆希望闭卷,田田希望半开卷,李老师收到请求之后只会批准其中一个,比如开卷,然后由他统一把所有节点上的考试信息改为开卷考试。但如果某一天李老师突然出差了,系统就会陷入瘫痪。
既然单master会有单点故障的风险,我们自然希望有多位老师(master)来对提案进行选举表决,而一旦引入多master,系统就会变得相当复杂,为了方便描述,我们先定义一下系统里的3种角色,分别是:
Proposer,相当于学生,负责发起各自提案(比如开卷或者闭卷考试)。
Acceptor,相当于老师,负责接收提案,并对提案进行选举表决。
Learner,老师和学生都可以是Learner,当选举结果出来之后,Learner会获知选举结果,这样其它同学想知道操作系统到底是什么考试形式的话,可以查询任意一个Learner,得出的结果一定是准确一致的。
现在我们有3名学生(proposer)同时发起提案,有多位老师(acceptor)接收提案并对提案进行表决,所以这个时候就不是李老师一个人说了算了,必须要有多数派的老师同时批准一个提案,这份提案才算是被选举通过,什么叫做多数派?简单的说就是大于一半的人数,比如有n位老师,那多数派至少需要n/2+1位老师。这种规则在我们日常生活中的选举是很常见的,只要获得超过半数的选票就算竞选成功。
先作个铺垫吧,多数派这个词看似浅显,实际上是Paxos的核心思想之一,多数派具有一个非常重要的性质:任意两个多数派,一定存在交集,也就是说至少有1位老师同时存在于这两个多数派中,只要我们保证任意时刻,一位老师只支持一种提案,那就可以保证选举结果是单一的。所以奥巴马和罗姆尼肯定不会同时当选总统啦:P
于是我们肯定希望这个多master系统具有以下几个功能特点,这也是一个初步的模型框架:
- 多名学生可以同时发起提案,可以发给任意的老师。
- 老师批准一项提案后,可以转而批准另一项不同的提案,但之前批准的就作废了,也就是任意时刻,一位老师只能支持一项提案。
- 一项提案只有被多数派的老师同时批准后,才能作为最终决议,从而决定操作系统的考试形式。
- 一旦这个决议被选出(比如开卷考试被选出),就不能再改变。
- 整个系统运作的过程中,学生可以旷课去玩游戏,老师可以出差,学生发出的提案可能中途丢失、重复、阻塞,但不会被篡改(也就是说我已经发出去的开卷提案不会被帆帆恶意篡改成闭卷)。
好吧,其实这几个功能特点都是在我已经了解了Paxos算法后才提出的,剧透太多了。但是即使有了框架,依然有很多问题需要解决,比如怎么保证第4条?
接下来就该Paxos登场了,作者Lamport为了使上面所描述的多master系统正常运作,首先提出了下面3条要求(safety requirements):
- Only a value that has been proposed may be chosen
- Only a single value is chosen
- A process never learns that a value has been chosen unless it actually has been
这三条要求初看就像三句废话,但理解paxos算法的思想和流程之后,再回来品味,就别有一番滋味,我们用中文的描述来预习一下这三条要求,首先看第一条。
第一条要求中有三个关键词:value,proposed,chosen。value是指一项提案的内容,比如我的提案是开卷考试,那么开卷就是我这个提案的value。proposed是指发起一项提案,chosen则是指这项提案作为最终决议被选举出来,要特别注意chosen和论文后面出现的accept是不同的意思,accept是指某一位老师自己批准了一项提案,而chosen是指“多数派”的老师同时批准了同一项提案,此时这项提案就会成为选举的最终决议,所以第一条要求翻译成中文的话,应该是:一项提案的内容只有在提案被发起后才有可能成为最终决议的内容。
第二条要求看似简单,却是整个算法费尽心思想要达成的一个重要约束,我们仔细地体会一下这句话,Only a single value is chosen,前面说过了,chosen是指选举出的最终决议,决议本身也是一项提案,提案里包含了内容(value),假设某一次选举,多数派的老师都批准了我的开卷提案,我收到了多数派老师的回复,知道我的开卷提案已经成为最终决议,我很开心地告诉了我的室友操作系统是开卷考试,而由于消息传递的不可靠,老师之间的通信出现故障,他们并不知道知道多数派老师对我的提议达成一致这一事实,那么选举无法结束,蒙在鼓里的老师们还会对新的提案进行表决,他们有可能又会对帆帆的闭卷提案达成一致,然后帆帆又把这个新的表决内容告知了其他同学,这样就造成了混乱。所以第二条要求实际可以延伸一下:某项提案作为最终决议被选举出来以后,即使选举继续进行,新选出来的决议内容也要和最初的决议内容一致。对应到操作系统考试这个例子中,一旦多数派老师同时批准我的开卷提案,即使选举继续进行,即使多数派老师又同时批准了帆帆的提案,那么这个时候必须设计一个算法,要么想办法使老师不批准帆帆的闭卷提案,要么就强迫帆帆把自己的提案内容从闭卷改成开卷,这样才能和之前的决议内容(开卷)保持一致。
第三条要求则是说,只有当一项提案成为最终决议时,才能被大家学习(learn)到,这里的learn也可以理解为消息内容持久化,即开卷考试被选举出后就成为板上钉钉的事情,不能再修改,并且当一项提案成为最终决议后,提案的内容最终都会被大家所获知,可能我最先获知开卷提案通过,然后我告知室友,室友又告知其它的同学,在这个决议信息传递过程中,决议的内容不会改变。
就这三条简短的要求,我已经展开了这么一大堆内容,可能是因为我已经学习过paxos,有点先入为主的感觉了,实际上如果只有这3条要求,还无法设计出一个可行的算法,比如具体怎么选举一个single value。那么我们首先来看看如何选出一个value,作者Lamport为了保证在选出value的过程中不打破“Only a single value is chosen”的要求,提出了一系列约束条件。
我们肯定希望即使只有一个学生发起了提案,这项提案也能作为最终决议通过,所以Lamport提出了约束条件P1:
P1. An acceptor must accept the first proposal that is receives.
翻译成中文就是acceptor一定会批准他所收到的第一份提案。这里第一次出现了accept这个词,需要再强调一次,accept是指单一的acceptor批准某一项提案,并不代表这项提案成为最终决议(chosen),只有当多数派的acceptor同时accept同一项提案时,决议才会产生。
P1并没有说acceptor如何处理他收到的第二份第三份提案,如果只允许他批准第一份提案,会导致很多问题,比如我和帆帆同时发起不同的提案,由于消息传递的延时,2位老师批准了我的开卷,另外2位老师批准了帆帆的闭卷,还有一位老师不在场,这样就无法出现多数派达成一致的情况,无法产生决议。
所以应该允许老师批准新收到的提案,为了避免提案之间产生混淆,需要给每一份提案一个独立的编号,不同的proposer不能共享提案编号,对于某个单一proposer,它可能在一次选举中多次提出提案,那么每次他提出新提案的时候,需要使用“更高的”编号,也就是说,对于同一个proposer提出的多个提案,编号越高,提案就越新。编号的方法其实很简单,用类似取模的思想就OK,比如我,帆帆,田田分别编号为1,2,3,我的第一份提案是1号提案,以后继续发提案就依次采用4,7,11等编号,帆帆就用的是2,5,8,田田用3,6,9,大家的编号集合互不相交,并且依次递增。
现在我们允许acceptor批准多项提案了,但必须注意,批准多项提案并不是说这些提案同时处于被这个acceptor批准的状态,比如李老师首先批准了我的开卷提案,之后他收到帆帆的闭卷提案并且又批准了闭卷,那么他对我的开卷提案的批准就此作废,因为最终选举出的提案内容只有一种情况,所以任何时候,acceptor只需要知道自己最近批准的提案信息(包括编号和内容)即可。
到这里,大家如果倒回去看一下我对“Only a single value is chosen”这条要求的解释,可以发现,一旦acceptor可以多次批准不同的提案,就可能会造成某项提案被chosen之后,选举没有结束,然后又选举出一项新的决议,决议的内容却和之前不同,违背了“Only a single value is chosen”的要求。
因此,Lamport提出了第二个约束条件P2:
P2. If a proposal with value v is chosen, then every higher-numbered proposal that is chosen has value v.
Paxos的论文从P2这里开始,就让人感到晕乎乎了。我们还是先用中文翻译一下P2:一旦某项提案作为最终决议被选举出来,并且这项提案的内容为v(比如开卷考试),编号为n,那么任何编号大于n并且被选举为最终决议的提案,其内容也一定为v(也同样为开卷考试)。
中文Wiki上的翻译是“之后”选举出的提案内容也为v,这个“之后”可能和原文的“higher-number”(更高编号)有所出入,但本质是一样的,学习完paxos之后就会知道“之后”选举出的提案的编号肯定是大于n的,好吧,这里说的有点超前了。
P2所想表达的核心思想就是一旦决议产生,即使“之后”又选举出了新的决议,决议的内容是不会变的。很明显,不管你选出多少份决议,决议的提案编号可以变,但内容绝对不变,这样就能保证“Only a single value is chosen”了。但是P2显得非常的突兀,一上来就谈选举结果,我们连怎么选出来的都不清楚,而且新人很容易对这个“higher-numbered”产生疑惑:由于消息延迟的存在,有可能更低编号的提案“之后”才会传到acceptor手上,难道就不能选举“lower-numbered”的提案为新的决议吗?
带着一头雾水,我们来看看Lamport怎么自圆其说吧,哈哈。
P2是对选举结果加以限制,由于选举出一项决议肯定离不开acceptor批准某一项提案,所以Lamport从acceptor入手,提出了P2的一个充分条件P2a,满足P2a就能满足P2
P2a. If a proposal with value v is chosen, then every higher-numbered proposal accepted by any acceptor has value v.
翻译成中文:一旦某项提案作为最终决议被选举出来,并且这项提案的内容为v(比如开卷考试),编号为n,那么任何acceptor所批准的任何编号大于n的提案的内容也一定为v(也同样为开卷考试)。
P2a作为P2的充分条件是很明显的,就不证明了。但P2a有一个局限,假设某次选举已经选出了决议内容为v,然后某一个acceptor和一个proposer之前一直在睡眠没有参与选举,这个时候他们突然醒来,proposer发送了一个更高编号的内容为w的提案给这个acceptor,那根据之前P1的约束,acceptor必须批准第一次收到的提案,而这份提案的内容却是w,这样就打破了P2a的约束。
所以为了满足P2a的同时不打破P1的约束,Lamport又对P2a进一步加强,再狠一点,直接限制proposer,因为一项提案要被批准首先需要被proposer发起这项提案,于是有了P2a的充分条件P2b:
P2b. If a proposal with value v is chosen, then every higher-numbered proposal issued by any proposer has value v.
翻译成中文:一旦某项提案作为最终决议被选举出来,并且这项提案的内容为v(比如开卷考试),编号为n,那么任何proposer所发起的任何编号大于n的提案的内容也一定为v(也同样为开卷考试)。
P2b确实太狠了,直接从提案的源头进行限制,比如我的开卷提案已经成为最终决议,然后帆帆又提了一个更高编号的提案,本来他是想提闭卷的,但为了满足P2b,他不得不把自己的提案改成开卷和我一样了。
但P2b这个约束本身难以被保证,他并没有说明帆帆是如何得知自己需要修改提案的,而且经常会出现这样的情况:一个提案被多数派同时批准了,决议选出来了,但大家都还不知道这个事实,所以proposer很难去获知“a proposal with value v is chosen”这条信息,也就无法保证他及时修改自己提案的value,一旦他没有修改,发起了不同内容的提案,P2b就被打破了。
因此,Lamport再次大显神威,进一步加强P2b,提出了P2b的一个充分条件P2c,P2c是一个非常详细的约束条件:
P2c. For any vand n, if a proposal with value vand number nis issued, then there is a set Sconsisting of a majority of acceptors such that either
(a) no acceptor in Shas accepted any proposal numbered less than n, or
(b) vis the value of the highest-numbered proposal among all proposals numbered less than naccepted by the acceptors in S.
P2c是Paxos算法最难理解的一部分,我们接下来需要慢慢梳理两件事:
- P2c到底在讲什么?
- 如何证明P2c是P2b的充分条件?
我们首先来看看P2c在讲什么,按照国际惯例,还是用中文翻译一下P2c吧:
假设某一项提案的编号为n,内容为v,在这项提案被proposer发送出去时,必须满足下面2个条件中的一个:
条件a,存在一个多数派的集合S,这个集合里的任意一个acceptor都没有批准过编号比n小的提案;
条件b,存在一个多数派的集合S,集合中的某些acceptor曾经批准过编号小于n的提案,而所有的这些提案中编号最大的那个提案的内容与v相同。
看到这里,很多人已经晕得不知东南西北了,我最初也是如此。在P2c里,“多数派”这个词多次出现,我在之前写到过“多数派”是paxos的核心思想之一,接下来我们先沉住气,看看Lamport如何利用多数派的性质来证明P2c是P2b的充分条件。
假设P2c成立,那么我们需要证明P2b也随之成立。由于P2b说的是决议被选出来之后的情况,那么我们就假设此时一份提案(编号为m,内容为v)被多数派的集合S批准,成为决议,并且接下来的所有更高编号的提案都满足P2c,然后看看它们是否也同时满足P2b(也就是证明这些提案的内容也为v)。
假设这个“更高的”编号为n,我们用顺推的数学归纳法,从n=m+1开始证明。
根据P2b,编号为n的提案内容也应该为v,我们假设编号为n的提案内容是w,由于这份提案满足P2c,我们看看w是个虾米情况。
如果提案n满足的是P2c的条件(a),那很明显这是不可能的,因为任意一个多数派都会和S有重叠,里面至少有一个人是批准了提案m的,所以(a)不满足,那只有满足P2c的条件(b)了。对于条件(b),同样的,任意一个多数派中至少有一个人批准了提案m,而n=m+1,所以m一定是编号小于n的提案中编号最高的,所以根据条件(b),w必须等于v。
接下来证明n=m+2就很容易了,还是采用相同的假设,则提案n肯定无法满足条件(a),对于条件(b),同样的,任意一个多数派中至少有一个人批准了提案m,而n=m+2,所以这个多数派集合中批准过的编号小于n的提案中编号最高的提案要么是m号提案,要么就是m+1号提案,而m+1号提案之前已经证明过,它的内容也是v,所以n=m+2的时候,w也必须是v,以此顺推下去,无论n取多大,它的提案内容都一定是v。
至此,我们证明了P2c是P2b的充分条件。
这个证明充分利用了多数派的核心思想,可能看了一遍还是不太清晰,自己多思考一下肯定能想通了,但即使想通了也还是会晕得不知东南西北,因为我们只是证明了P2c是P2b的充分条件,作者能直接提出P2c,真乃神人也。
证明完毕后,我们来看看具体怎么实现才能满足P2c。首先,如果proposer想发起编号为n的提案,那么他必须先在acceptor里面任意找一个多数派,查询一下这个多数派里已经和【将要】批准的编号小于n的提案中编号最高的内容是什么,注意这里我强调了“将要”一词,不得不佩服Lamport考虑得如此周全,我举个例子,比如某一时刻还未选举任何一个决议,此时我查询了一个多数派,得到已经批准的最高编号提案的内容为v,于是我把自己编号为n的提案内容也改为v,发送出去,但是消息传递是有延时的,在我的提案传递过程中,有一个acceptor批准了编号小于n,内容为w的提案,而他恰好使得批准这项提案的acceptor人数达到多数派要求,于是w作为决议被选出,而我却已经提出了内容为v的提案,这样就打破了P2b。
所以“将要批准”就是指我的提案从发出到acceptor接收的这段时间内,acceptor所批准的提案,但是很明显,预测未来是非常困难的,我只能询问到acceptor已经批准的提案,而不可能知道acceptor将会批准什么样的提案。因此,当我向acceptor询问他批准过的提案时,我需要要求acceptor在回复我的时候附加一个承诺:他不会再批准编号小于n的任何提案,这样他对我的回复就是绝对有效,不会出错的。在上面那个“将要”的例子中,acceptor一旦回复过我,他已经作出了承诺,就会直接拒绝那一份内容为w的提案。
认真思考的人会提出一个问题,如果“将要”批准提案w的那个acceptor,并没有向我作出过任何承诺,怎么办?这是一个很值得思考的问题,实际上,在这种情况,我所发出的提案内容v一定和w相同,所以就无所谓承诺不承诺了,参考P2c推出P2b的证明方法,利用多数派的性质,这个证明留给读者自己思考。
所以现在proposer就不能随便乱发提案了,每次发提案之前,他都要先向多数派的acceptor发起一次“询问”,把自己提案的编号n发送给这些acceptor,acceptor需要回复他已经批准过的编号最高的提案信息,并且向proposer承诺不再批准任何编号小于n的提案。当然,如果acceptor已经批准过编号大于n的提案了,那么这个编号为n的“询问”就可以直接被拒绝,因为acceptor绝对不会再批准编号为n的提案了。
现在再回去看P1的话就会发现P1是不完备的,很有可能acceptor无条件批准的第一个提案编号恰好是他承诺过不会批准的编号范围。所以要对P1进行加强,于是有了P1的充分条件P1a:
P1a: An acceptor can accept a proposal numbered n if it has not responded to a prepare request having a number greater than n.
翻译成中文:acceptor只要没有收到过编号大于n的询问,那他就可以批准编号为n的提案。这里的prepare request我翻译成了询问,我觉得这样最容易理解,就是说学生发起提案前,要先向老师咨询一下现在是个什么情况,再根据老师的回复来决定自己提案的实际内容,不能随便乱发提案。
大家可能有疑问,如果P1a是P1的充分条件,怎么保证acceptor收到的第一个提案的编号不会是他承诺过不批准的编号范围?原因就是proposer正式发起提案前,会预先询问,由于acceptor已经承诺过不会批准编号小于n的提案了,那么对于编号小于n的“询问”,就会直接无视或者拒绝proposer发提案骚扰他,当然就不会再有这样的提案发给这个acceptor了。
至此,P1和P2两个约束条件被逐步加强,我们可以根据P1a和P2c设计出详细的算法流程了。整个算法分为2个阶段,第一阶段为前面所说的“询问”阶段,第二阶段则为正式发起提案阶段,每个阶段里,proposer和acceptor都有自己的职责。具体过程如下:
阶段1a:
Proposer将自己提案的编号n发送给多数派的acceptor,这条消息被称为prepare request(我把它翻译为询问);发完询问之后需要等待acceptor的回复,如果确定收不到多数派的OK回复,则应该提升自己的提案编号,重新开始阶段1a的工作。
阶段1b:
Acceptor收到编号为n的询问,如果自己之前没有收到过编号大于n的询问,那么就回复OK,并附带自己已经批准的提案信息(包括编号和内容),没有批准过提案则附带Null,如果之前有收到过编号大于等于n的询问,则无视当前请求或者回复reject (为了满足P1a)
阶段2a:
Proposer编号为n的询问得到多数派的OK回复,则正式发起编号为n的提案,提案的内容必须和这些回复里编号最高的提案内容相同,没有则可以取任意值作为提案的内容(为了满足P2c);发完提案之后需要等待acceptor的回复,如果确定收不到多数派的OK回复,则应该提升自己的提案编号,重新开始阶段1a的工作。
阶段2b:
Acceptor收到编号为n的提案,检查自己承诺过的最高编号的询问m,如果m>n,则拒绝这个提案,否则回复OK,并记录下这个编号为n的提案(为了满足P1a)
这里需要补充说明一些细节。
在阶段1a,proposer只需要发送自己的提案编号,内容是不用发的,因为它只是询问,有可能接下来还需要修改自己的提案内容。
在阶段1b,“无视当前请求或者回复reject”这句话我说得有点模糊,实际上可以分两种情况:
如果acceptor只是收到过编号大于n的询问,那它可以直接无视当前这个编号为n的询问,proposer收不到回复可以补发询问给其它acceptor,只要凑齐多数派的ok回复即可。
而如果acceptor已经批准过编号大于n的提案(比如m)了,那他应该回复一个明确的reject给这个发编号n询问的proposer,告诉他不要再做无用功,也不要再发消息给其他acceptor了,因为很明显,此时肯定有多数派的acceptor已经收到过编号m的询问,承诺了绝对不会批准编号小于m的提案,所以编号n不可能凑得齐多数派的OK回复,这里再次利用了多数派的性质。
在阶段2b,acceptor记录下编号为n的提案时,会直接覆盖他上次记录的提案,也就是说,acceptor永远只需要记录他最近一次批准的提案,并且这个记录应该是持久化的(比如保存到硬盘),即使acceptor宕机重启了,记录也还保存着,这样acceptor才能继续加入选举。
这个时候我们再回去看一下P2,我提到过一个问题,lower-number哪里去了?有了上面的算法流程,我们可以很容易得出结论,一旦决议产生,“之后”得到多数派acceptor批准的提案编号一定是更高的,lower-number的提案则会被多数派拒绝,这个就不用再详细证明了。
至此,Paxos算法的主体部分已经介绍完毕,不过上面这一大堆内容都在讲怎么选出一个single value,工作都是交给proposer和acceptor的,当决议选出之后,大家如何获知选举结果?接下来就该一直坐冷板凳的Learner上场了,我们来看看如何Learn a value。
多个acceptor选举过程中,一个很大的问题就是,当多数派的acceptor批准了同一份提案时,作为局外的围观者,我们知道决议已经被选出了,但参与选举的acceptor可能由于相互之间的消息传递出现bug,导致大家都不知道决议产生这一事实,使得选举继续进行,Paxos通过前面的约束规则保证了即使选举继续进行,决议内容不会改变,确保了系统一直是安全的。
因此Learner的职责就是围观这场选举,每次acceptor批准一份提案,都会把这个提案信息发送给Learner,当Learner发现同一份提案被多数派acceptor批准后,他就知道选举结果已经出来了,就会把这个信息广播给参与选举的所有人,让大家得知这个结果,然后关闭选举。
Learner肯定不止一个,否则又会有单点故障,通常会有多个Learner,任意一个Learner得知选举结果后即可广播这个信息。在实际的系统中,通常一个节点可以身兼多职,比如proposer同时也可以是Learner,因为他在阶段2发出提案后需要接收acceptor的反馈,如果收到多数派的OK反馈,那他就可以通知大家选举结果,并且关闭本轮选举了。
以上便是Paxos的基本内容,还有很多扩展内容没有讲到,比如Paxos的活锁问题,Fast Paxos,Multi-Paxos等变种和改进的算法,这个等我好好学习一下再写下一篇笔记吧。另外在学习Paxos的时候,我们完全可以把它和其他一致性算法做一个对比,比如两阶段提交,这里就不展开了。分布式是一个大坑,里面的问题也非常有趣,希望能好好学习天天向上T-T
参考文献:
《Paxos Made Simple》作者Lamport的论文,我反复看了很多遍,每一次阅读都提升一点点理解的姿势-_-!
《paxos和分布式系统》by 知行学社 百度大牛的视频讲解,从分布式系统设计的角度来讲解Paxos,值得一看
求是大肥羊
2012.12.28
浙公网安备 33010602011771号