标签:
在学习分布式相关知识时,很早之前就断断续续了解过Paxos算法,搜到的资料大抵如Paxos Made Simple中译版,互相转载、翻译、互相注释。在粗览版面后,发现了一些奇怪的东西,诸如“提案”、“选举”、“议员”、“决议”。心里不禁骂娘,这都是些神马玩意啊,和分布式容错有五毛钱关系?作为一个长期生活在社会主义国家的单纯码农,瞬间被这些词汇带歪了。硬着头皮往后读,发现遇到疑惑的地方又冷不丁一个“显而易见”,真真有苦无处诉。
这还算好,如果你看到其他作者拿出三国时期的五虎上将来举例,生生编出一个故事,洋洋洒洒数千字翻页翻到手抽筋的时候,而你的脑海里还要把这些和上面的名词再映射一番时,酒仙太白也醉了吧?
当时的想法是,要是有一个形式化的严谨描述也不至于看得如此痛苦。通篇读下来,不知它要证明什么,要达到什么目的,解决了什么问题。既然读起来不是这么容易让人懂,必然会有大牛们写出各种注来,于是再次带上关键字“彻底读懂”、“深入分析”搜索相关文章,果然找到一篇:Paxos算法深入分析。
读完这篇文章时,我几乎已经快要明白了,这时候再反过来看看Paxos Made Simple,加上点自己的思考,也终于算是不会被绕晕了。
然而,眼看着大师们把显而易见的地方省略所造成的菜鸟的困惑却又将一代一代传承下去,想到这里,不禁奋笔疾书,誓要改变此状况,因此有了这一篇。读罢此文,若你还不明白什么是Paxos或者Paxos能做什么,我就宣布退出江湖了。
其实paxos算法本身的并不复杂,力求让读者也同意这个观点,这是本文的主题之一。然而这并不复杂的想法后面的逻辑又是如何?它到底是怎么形成的呢?这是本文要着力探讨另外一个主题了。虽然文字有点多,希望给大家带来一次轻松愉悦的阅读体验。
事实上,Paxos Made Simple一文中算法描述的整体思路,就是从算法要到达的目的开始,逐步反推出算法需要满足的约束,最后根据各阶段的动作及其约束做出最终总结的一个过程。
在倒推的过程中,类似玩一个思维的游戏,你大可放开想象勇往直前,而毋须担忧逻辑失严。通常地,你会发现当前的行动缺失必要的提前约定,此时游戏重置,加强约定再重新出发,如此反复直至达成预期。是的,节操掉了还能捡回来,当时就是这样。
鉴于Paxos Made Simple一文的思考细节缺失,本文接下来将加入自己的理解,着重重述这一过程。然而被别人牵着鼻子走却不知道为什么的时候,心里会很难受。我建议在参考别人的思考之前先自己思考,两相对照,直至从心底里认同,或者提出有力反驳为止。
算法的目的是在一个具有容错能力的分布式系统中,达成读写一致性。“一致性”显然是核心诉求,简单的说,就是把这个分布式系统当成一个读写串行的单一节点,这样写x成功后必然能读到x,写y成功后必然能读到y,而不至于部分节点读到x,部分节点又读到y;另一方面,又必须具有容错能力,具体到这个算法,只要过半数的节点存活,那么系统就能正常工作。
容错能力即高可用,高可用通常通过冗余副本实现,而冗余又意味着没有绝对的一致;单一副本则天然一致,然而一出故障就不可用。高可用和一致性之间能鱼与熊掌兼得吗?答案是肯定的。有一个专门的定理更全面描述了这方面的问题,即CAP定理,这个定理当然又迷惑了万千少男少女,也许本段文字就漏洞百出,然而它的相关阐述留待下回分解,此处不要追究过多。
更具体一点,从实现的角度看,要达成的目的:
实现的时候采用一组固定数目Server,每个Serve都随时可以接受Client的请求。当多个Client分别将自己的请求值(Value_i)发给其中的一个Server处理,最终每个Server给Client的回复都是一个一致的值(Chosen_Value),这个值是Value_i之一,并且只要过半数的Server存活,任意时刻都可以再次请求任意Server获知该值(Chose_Value)。
现在你知道了,本文讨论的核心是basic paxos算法,也就是一旦写入完成,后续都将只能写入同样的值,即等价于只能写一次。至于怎么支持写多次(更新为其他值),此算法当然是基础,限于篇幅,又没人给稿费,是否在本文阐述multi-paxos完全看心情了。
如果你知道CAP定理,就该知道,CA与P不可兼得,上述能返回正常响应的Server都必须是和其他Server互连互通的,否则认为该Server非存活。
能解决上述问题的Paxos算法,是基于Server组之间消息传递最终达成一致的算法。消息的传输允许丢失,重复,乱序,但是不考虑消息被攥改的情况。
为什么可以假设为消息内容不会被篡改的非拜占庭模型?事实上消息在传输、存储的过程中不但可能被人为篡改,还可能因为纳米级别芯片工艺的进一步细化而导致的硬件对环境的敏感度增强,最终造成位翻转等难于察觉的悲惨事故。
如果要考虑消息可能被篡改,则又是另外一篇文章的主题,即拜占庭将军问题。也就是说保证一个稳定安全的消息传输通道,和这里讨论的一致性算法可以独立并行,各自描述。
这里也简单提一下如何解决消息篡改的问题。首先人为的问题先忽略,可以假设算法运行在一个比较安全的内网环境。而在解决非人为的数据错乱的方案里面,校验和是最经济也最有效的方案,TCP协议就是如此,然而它永远只能减小出错概率,而不能100%杜绝。据称Google就曾经发生过TCP协议下位翻转的事情,此后,他们的RPC协议在TCP之上又增加了一层应用层校验机制。
Client发出请求,要写入Value。Client可能同时有m个:Client A说要写Value1,ClientB说要写Value2,……。
Server节点(后续简称节点)接收到该请求后怎么办呢?如何从众Value当中选择(Choose)出一个Value(请注意`选择这个术语,很重要),使得一旦该Value被选择,那么该时刻后的任意时间点,只要过半数的节点存活(容灾),写请求只能写同样的Value(写一致性)才返回成功,其他Value的写被拒绝;对任意的读请求都能读到该Value(读一致性)。这个被选择的Value,有一个专门的名词,叫Chosen_Value。
如果只有一个节点,要做到这样是很简单的。有无数种串行化请求的机制,任择其一,然后使用简单的先到先选择策略即可。假设Client A的请求先接收到,那么Server节点选择Value1即可,后续的Value如果不是Value1就拒绝,Value1此时成为Chosen_Value。一旦Chosen_Value产生,在此后的任意时刻,Client的读请求都可以读到Value1,写请求都只有在写Value1的情况下才能成功。
但是,一旦这唯一的节点出现故障,就无法提供服务了,甚至导致数据永久丢失,没有满足“容灾”的特性。所以,节点必然是多个的。
对数据进行容灾的唯一出路就是多副本,朴素、有效,paxos亦不能免俗。
那么,问题来了,需要在多少个节点上选择出Chosen_Value(Chosen_Value需要在多少个节点上存在)呢?
我们知道Paxos算法的目的是要在仅有过半数节点存活的情况下,仍能提供读写服务。那好,我们就假设集群(节点的集合)是第一次运行,然后除了这恰好过半数的节点存活外,其他节点统统在还是一张白纸的情况下失联了。这时,m个Client开始同时发起写请求,而我们目前并不了解的神奇的paxos算法开始在节点间运作,经过一段时间后,算法终止,尘埃落定,Chosen_Value诞生。
所谓无巧不成书,在Chosen_Value刚刚诞生后,失联的那批节点又鬼魅一般复活了,然而上帝又和之前存活的过半数开了个玩笑,将他们统统挂掉,只留一个(准确的说:奇数节点的集群留1个,偶数节点的集群留2个),这时复活的白纸节点加运行了paxos算法的唯一(或唯二)幸存节点一起又组成了另一个过半数。此时,读写服务应该不受影响。很明显,保持一致性的重担和白纸节点们没有半毛钱关系,于是这唯一幸存的节点上面必然要存留有Chosen_Value的痕迹。由于这个幸存的节点是在存活的过半数当中任意选取的,所以任意的存活节点都必须有承担这个重担的觉悟,也就是Chosen_Value必须满足以下两个约束:
a. Chosen_Value一旦被选择(后面你会看到,Chosen_Value被选择并不需要某个节点去认同,只要客观上的条件一达到,Chosen_Value就诞生了),就至少需要存在一个过半数节点的集合,集合中的每一个节点上都有线索可以找出Chosen_Value。(容灾性)
b. Chosen_Value有且只有一个。(一致性)
如何让过半数的节点对同一个Value做出这个选择的决定,而其他的节点要么没有做出选择,要么选择了同样的Value呢?
这是很难做到的,因为选择意味着“认定”了这个值为Chosen_Value。过半数节点上同时认定更是没可能的事,如果挨个去认定,就面临发起认定请求的节点+已经认定的节点挂掉后,后续存活的过半数无法获知之前认定的Chosen_Value的事实,于是不一致性的怪物又出现了。
换一个思路,如果能保证在“认定”的节点没有过半数(客观上,并不需要某个节点去问询和统计)之前,这个认定的值允许不同,而在认定同一个Value恰好过半数(客观上存在,你挨个去认定,然后一旦客观上达成了过半数)的那一刻起,后续都只能认定同一个Value,那么即使在那一刻起挂掉了发起认定请求的节点和已经认定的节点的一部分(肯定不能是全部,因为认定的节点已经过半数了,如果挂了全部,剩下的节点就不是过半数,paxos失效也是理所当然了),也无关紧要了,因为不管你承认不承认,这个Value就是Chosen_Value了,它已经在过半数的节点上留下了足够的证据。
有必要给认定一个新的术语——“批准”。
它是如此的重要,以至于再怎么强调都不过分,于是我从另外一个角度解读一下上面的结论。一个节点批准(认定)某个Value,和过半数的节点批准了某个Value,然后挂掉只剩下一个,现在在存活的节点当中两者都只被一个节点批准了,那这两者现在有什么区别呢?有本质的区别:
其一,过半数可以保证在过半数存活的情况下,至少有一个节点知道当时的真相——批准了谁,从而有机会保证数据的一致性。如果是前者,唯一批准的那个节点挂掉后,就没有人再知道那个Value了
其二,后者出现过过半数,这个客观事实很重要,直接触达paxos的心脏。请继续看下文到底如何利用这个客观事实。
由于有的Value被批准后,可能注定不能成为Chosen_Value,批准过这种Value的节点如果不再接受其他Value的批准,那么就很可能形不成某一个Value被大多数节点批准的客观事实(不同Value分别被不同的节点批准)。
于是很容易得出一个结论,一个节点必然要批准多个Value,同时意味着节点批准Value需要一个批准策略。
当节点提出的Value被批准后,这个节点可能放羊去了,它的Value就在被废弃之列。为了使得算法继续进行下去,就必须要不断节点再次提出Value,一个很显而易见的策略就是批准最新的Value。谁新谁旧,因此Value还必须附带有一个类似时间的标识,能区分出谁新谁旧,一个很容易想到的方案,就是给每个Value编号。提出待批准的请求后续称之为accept请求,accept请求的内容即要求节点批准某个Value——称之为“议案”。因此议案包含{编号,Value}。 批准accept请求的节点,此刻扮演的角色称之为acceptor。
Tips: 议案为{编号, Value}对,编号为议案的唯一标识,不同议案的Value可能相同。
当所有节点都开始批准最新的Value时,会出现两个不同的Value分别被过半数批准:Value1先发送给过半数的节点批准,然后Value2(更新的Value)又来了,很显然,也能获得过半数的批准。
如何解决这个矛盾?很容易想到,如果我们能保证Value1和Value2相等,那么一致性仍然得意保持。即—即一旦一个议案被过半数的节点批准的客观事实成立,那么后续发起的议案的Value也必须与之保持一致。
这个如何做到呢?请继续看下文。
提出议案的节点很显然就是接收到Client请求节点,如何能保证两个不同节点提出一个同样Value的议案呢?统一意味着协调,提出议案的节点之间必须先协调。
只有某个议案被过半数批准的客观事实出现后,后续的议案才需要保持Value一致,因此如果在议案被提出前先发起一个请求去问一下过半数的节点——此请求称之为prepare请求:每个节点当前最近(编号最大)被批准的议案是什么,似乎我们看到曙光了。
从提出议案的时刻起,如果此时已经有过半数的节点批准了某个议案(请开启上帝视角去观察,它客观存在),那么prepare请求从过半数那里收到的结果中必然能获知它(又是抽屉原理,接收到prepare请求的过半数acceptor节点和批准accept请求的过半数acceptor节点必有交集),我们应该选择它作为新议案的Value。那么它是谁?怎么辨别?我们目前收到了一堆被批准的议案,议案的内容为{编号,Value},选哪个编号的Value呢?谁才是曾经被过半数批准的议案呢?
另外一种情况,从提出议案的时刻起,如果此时没有任何议案被半数批准(请继续开启上帝视角去观察),那么prepare请求从过半数那里收到的结果中仍然可能返回一堆被批准的议案,议案的内容为{编号,Value},此时我们又该选择谁?
等等,我想静静,也别问我静静是谁。
既然有上面的两种情况,导致返回的被批准的议案中可能没有被过半数批准也可能有被过半数批准,那么我们就需要制定一个规则,从一堆议案中找出一个议案,它要么是曾经被过半数批准的议案,要么是将来可能被过半数批准的议案,面对{编号,Value},显然只能从编号上下手,那么规则也就明朗了:编号最大的那个。
有人可能会说,为什么不去问全部节点呢?然后返回的被批准的议案中如果有一个议案重复了过半数,那不就是它吗?然而天有不测风云,任何节点都可能随时挂掉,如果被批准的过半数的节点中任意挂掉一个,上面的过半数就不可能满足了。
在prepare请求的过半数回复中,我们选择被批准的议案中编号最大的议案的Value作为新议案(如果没有被批准的就选择自己Client请求中的Value),同时新议案必须有一个编号(编号必须递增,并且每个节点之间不重复,一个很简单的方案就是每个节点之间的编号分段,各自独立递增)。
那么问题解决了吗?如何保证编号最大的那个议案被过半数批准而其他编号的议案不被批准呢?想象一种情况,某个节点提出了议案,发现没有任何议案被批准,于是它选择自己的值Value1,议案编号为13,随后另一个节点又提出了一个议案,prepare问一圈后仍然没有议案被批准,于是它也选择自己的值Value2,议案编号250。这时议案{13,Value1}被过半数节点批准了,此时Choose_Value诞生。然后因为节点要批准最新的议案,于是议案{250,Value2}显然也能被过半数批准,卧槽!又出现两个Value被过半数批准了!
问题出在哪?议案13和议案250提出的时候,还没有批准任何议案,然而随后这2个议案都能被批准,这是不能接受的,因此要加强约束。只能允许一个议案被过半数批准,根据规则很显然就是我们之前约定的250(目前为止最大编号的议案,开启上帝视角就能看到了)。因此在议案250提出prepare请求时,就应该埋下议案13(实际上是比250小的任何议案)不能被过半数批准的伏笔,即prepare请求提出后,accepotr节点不能只返回最新批准的议案,还要给出一个承诺,不再批准比250(prepare的议案编号)小的议案。
由于pepare也是发给过半数,而议案13要被批准也需要发给过半数,它们之间必有交集,交集的节点因为承诺,就不会通过议案13的批准,于是议案13在议案250提出的那一刻起就注定不能被过半数批准了,不一致性被生生扼杀在摇篮中,这也是整个算法最精妙的地方。
简单来说,算法过程如下:
阶段1.
收到Client请求的节点的角色称之为Propser(因为马上要提出议案),选择一个新的议案编号,向Acceptor们发起prepare请求(带上即将要发起的议案的编号),询问最新被批准的议案。Acceptor在收到prepare请求后,返回最新被批准的议案(编号+Value),同时承诺不再批准编号小于prepare请求中议案的编号。
阶段2.
Proposer收到acceptor过半数的回复后,就可以开始阶段2Propser选择阶段1所有回复中编号最大的议案的Value(如果没有,选择Client请求中的Value)作为当前新议案的Value,向Acceptor(包括它自己)们发起accept请求,询问自己的议案能否被批准。
Acceptor收到accept请求后,先检查自己承诺过的编号,如果当前议案的编号比承诺的编号小,那么拒绝批准议案,否则通过(还有一个约束是只批准更新的议案,事实上已经包括在承诺里面了,即如果一个小的(更早)编号的议案出现了,意味着更大(更新的)的议案已经诞生了,更大的诞生获得的承诺保证了更小的不会被过半数通过,那么即使这里通过了小编号的议案也对大局无影响)。
剩下的事情就是Proposer检查Acceptor的回复,如果收到了过半数的回复说议案通过了,那么就可以确人这个议案的Value就是Chosen_Value。
即使Proposer在没有收到过半数的回复之前就挂掉了,而收到accept请求的Acceptor仍然兢兢业业在批准议案,一旦客观上批准达到了过半数,Proposer自身的挂掉就不值一提了。 因为我们可以在任意时刻,再发起一轮这样的过程,必然可以再度获知到这个被批准的Value。这就是paxos强一致性的所在了。节点随便挂,只要过半数活着就ok。
其实还有一个Learner的过程,模拟Propser提出议案,如果返回有被批准的议案,那么就发起accept请求,如果收到了过半数的回复,说明Chosen_Value已经诞生,就是它发起的议案的Value,于是这个值就可以广为传播了,同时这一轮basic paxos就可以宣告结束了,可以再接收下一轮的更新。从basic paxos到multi paxos过渡的过程,基本上就差不多了。(本段纯属“想得太多,读书太少”的产物,本人还没有仔细去研究,不过想来应八九不离十了。)
另外再扯一句,要实现paxos,还是有很多需要考虑和优化的地方,比如之前讨论的,如果不断有更高编号的议案提出,那就可能没有任何议案能被过半数节点批准,这样paxos算法就不会终止,读到这里,我想你已经能自己去研究剩下的事情了。
最后,本文描述极有可能有错乱的地方,请大家自行对照其他相关文献去伪存菁,本文实乃抛砖引玉,不免贻笑方家了~。
标签:
原文地址:http://www.cnblogs.com/nofree/p/4271588.html