learn-tech/专栏/周志明的架构课/31_分布式共识(上):想用好分布式框架,先学会Paxos算法吧.md
2024-10-16 06:37:41 +08:00

23 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        31 _ 分布式共识想用好分布式框架先学会Paxos算法吧
                        你好,我是周志明。从这节课起,我会用两讲带你学习分布式共识算法。

可靠与可用、共识与一致

在正式开始探讨分布式环境面临的各种技术问题和解决方案之前,我们先把目光从工业界转到学术界,学习两三种具有代表性的分布式共识算法,为后续分布式环境中操作共享数据打好理论基础。

我们先从一个最简单、最常见的场景开始:如果你有一份很重要的数据,要确保它长期存储在电脑上不会丢失,你会怎么做?

这不是什么脑筋急转弯的古怪问题,答案就是去买几块磁盘,把数据在不同磁盘上多备份几个副本。

假设一块磁盘每年损坏的概率是5%那把文件复制到另一块磁盘上备份后数据丢失的概率就变成了0.25%两块磁盘同时损坏才会导致数据丢失。以此类推使用三块磁盘存储数据丢失的概率就是0.00125%使用四块则是0.0000625%。换句话说使用四块磁盘来保存同一份数据就已经保证了这份数据在一年内有超过99.9999%的概率是安全可靠的。

那对应到软件系统里,保障系统可靠性的方法,与拿几个磁盘备份并没有什么本质区别。

单个节点的系统宕机导致无法访问数据的原因可能有很多比如程序运行出错、硬件损坏、网络分区、电源故障等等一年中出现系统宕机的概率也许比5%还要大。这就决定了软件系统也必须有多台机器能够拥有一致的数据副本,才有可能对外提供可靠的服务。

但是,在软件系统里,要保障系统的可用性,面临的困难与磁盘备份却又有着本质的区别。

其中的原因也很好理解:磁盘之间是孤立的不需要互相通讯,备份数据是静态的,初始化后状态就不会发生改变,由人工进行的文件复制操作,很容易就能保证数据在各个备份盘中是一致的。但是,到了分布式系统里面,我们就必须考虑动态的数据如何在不可靠的网络通讯条件下,依然能在各个节点之间正确复制的问题。

现在,我们来修改下要讨论的场景:如果你有一份会随时变动的数据,要确保它能正确地存储在网络中几台不同的机器上,你会怎么做?

这时,你最容易想到的答案一定是“数据同步”:每当数据有变化,就把变化情况在各个节点间的复制看成是一种事务性的操作,只有系统里的每一台机器都反馈成功地完成磁盘写入后,数据的变化才能宣布成功。

我们在第13讲学过的2PC、3PC就可以实现这种同步操作。同步的一种真实应用场景是数据库的主从全同步复制Fully Synchronous Replication。比如MySQL Cluster进行全同步复制时所有Slave节点的Binlog都完成写入后Master的事务才会进行提交。

不过这里有一个显而易见的缺陷尽管可以确保Master和Slave中的数据是绝对一致的但任何一个Slave节点、因为任何原因未响应都会阻塞整个事务。也就是说每增加一个Slave节点整个系统的可用性风险都会增加一分。

以同步为代表的数据复制方法叫做状态转移State Transfer。这类方法属于比较符合人类思维的可靠性保障手段但通常要以牺牲可用性为代价。

但是我们在建设分布式系统的时候往往不能承受这样的代价。对于一些关键系统来说在必须保障数据正确可靠的前提下对可用性的要求也非常苛刻。比如系统要保证数据要达到99.999999%可靠性同时也要达到99.999%可用的程度。

这就引出了第三个问题:如果你有一份会随时变动的数据,要确保它正确地存储于网络中的几台不同机器之上,并且要尽可能保证数据是随时可用的,你会怎么做?

系统高可用和高可靠之间的矛盾是由于增加机器数量反而降低了可用性带来的。为缓解这个矛盾在分布式系统里主流的数据复制方法是以操作转移Operation Transfer为基础的。我们想要改变数据的状态除了直接将目标状态赋予它之外还有另一种常用的方法就是通过某种操作把源状态转换为目标状态。

能够使用确定的操作促使状态间产生确定的转移结果的计算模型在计算机科学中被称为状态机State Machine

状态机有一个特性:任何初始状态一样的状态机,如果执行的命令序列一样,那么最终达到的状态也一样。在这里我们可以这么理解这个特性,要让多台机器的最终状态一致,只要确保它们的初始状态和接收到的操作指令都是完全一致的就可以。

无论这个操作指令是新增、修改、删除或者其他任何可能的程序行为,都可以理解为要将一连串的操作日志正确地广播给各个分布式节点。

广播指令与指令执行期间允许系统内部状态存在不一致的情况也就是不要求所有节点的每一条指令都是同时开始、同步完成的只要求在此期间的内部状态不能被外部观察到且当操作指令序列执行完成的时候所有节点的最终的状态是一致的。这种模型就是状态机复制State Machine Replication

在分布式环境下,考虑到网络分区现象是不可能消除的,而且可以不必去追求系统内所有节点在任何情况下的数据状态都一致,所以采用的是“少数服从多数”的原则。

也就是说一旦系统中超过半数的节点完成了状态的转换就可以认为数据的变化已经被正确地存储在了系统当中。这样就可以容忍少数通常是不超过半数的节点失联使得增加机器数量可以用来提升系统整体的可用性。在分布式中这种思想被叫做Quorum机制。

根据这些讨论,我们需要设计出一种算法,能够让分布式系统内部可以暂时容忍存在不同的状态,但最终能够保证大多数节点的状态能够达成一致;同时,能够让分布式系统在外部看来,始终表现出整体一致的结果。

这个让系统各节点不受局部的网络分区、机器崩溃、执行性能或者其他因素影响能最终表现出整体一致的过程就是各个节点的协商共识Consensus

这里需要注意的是共识Consensus与一致性Consistency是有区别的一致性指的是数据不同副本之间的差异而共识是指达成一致性的方法与过程。

由于翻译的关系很多中文资料把Consensus同样翻译为一致性导致网络上大量的“二手中文资料”都混淆了这两个概念。以后你再看到“分布式一致性算法”的时候应该就知道它指的其实是“Distributed Consensus Algorithm”了。

好了我们继续来学习分布式中的共识算法。说到这里我们就不得不提Paxos算法了。

Paxos算法

Paxos算法是由Leslie Lamport就是大名鼎鼎的LaTeX中的“La”提出的一种基于消息传递的协商共识算法。现在Paxos算法已经成了分布式系统最重要的理论基础几乎就是“共识”这两字的代名词了。

这个极高的评价来自提出Raft算法的论文“In Search of an Understandable Consensus Algorithm”更是显得分量十足。

关于Paxos在分布式共识算法中的地位还有这么一种说法

There is only one consensus protocol, and thats “Paxos” — all other approaches are just broken versions of Paxos.- 世界上只有一种共识协议就是Paxos其他所有共识算法都是Paxos的退化版本。- —— Mike BurrowsInventor of Google Chubby

虽然我认为“世界上只有Paxos一种分布式共识算法”的说法有些夸张但是如果没有Paxos那后续的Raft、ZAB等算法ZooKeeper、etcd这些分布式协调框架Hadoop、Consul这些在此基础上的各类分布式应用都很可能会延后好几年面世。

但Paxos算法从被第一次提出到成为分布式系统最重要的理论基础可谓是经历了一番波折。我们来具体看看。

Paxos算法的诞生之路

为了解释清楚Paxos算法Lamport虚构了一个叫做“Paxos”的希腊城邦这个城邦按照民主制度制定法律却又不存在一个中心化的专职立法机构而是靠着“兼职议会”Part-Time Parliament来完成立法。这就无法保证所有城邦居民都能够及时地了解新的法律提案也无法保证居民会及时为提案投票。

Paxos算法的目标就是让城邦能够在每一位居民都不承诺一定会及时参与的情况下依然可以按照少数服从多数的原则最终达成一致意见。但是Paxos算法并不考虑拜占庭将军问题也就是假设信息可能丢失也可能延迟但不会被错误传递。

Lamport在1990年首次发表了Paxos算法选的论文题目就是“The Part-Time Parliament”。由于这个算法本身非常复杂希腊城邦的比喻使得描述更为晦涩难懂。所以这篇论文的三个审稿人一致要求Lamport删掉希腊城邦的故事。这就让Lamport非常不爽然后干脆就撤稿不发了所以Paxos刚刚被提出的时候并没有引起什么反响。

到了八年之后也就是1998年Lamport把这篇论文重新整理后投到了“ACM Transactions on Computer Systems”。这次论文成功发表Lamport的名气也确实吸引了一些人去研究但是并没有多少人能看懂他到底在说什么。

时间又过去了三年来到了2001年Lamport认为前两次没有引起什么反响是因为同行们无法理解他以“希腊城邦”来讲故事的幽默感。所以他第三次以“Paxos Made Simple”为题在“SIGACT News”杂志上发表这篇论文的时候终于放弃了“希腊城邦”的比喻尽可能用他认为简单直接、可读性较强的方式去介绍Paxos算法。这次的情况虽然比前两次要好一些但以Paxos本应获得的重视程度来说依然只能算是应者寥寥。

这一段听起来跟网络段子一般的经历被Lamport以自嘲的形式放到了他的个人网站上。尽管我们作为后辈应该尊重Lamport老爷子但是当我翻开“Paxos Made Simple”这篇论文见到只有“The Paxos algorithm, when presented in plain English, is very simple.”这一句话的“摘要”时还是忍不住在心里怀疑Lamport这样写论文是不是在恶搞审稿人和读者在嘲讽“你们这些愚蠢的人类”。

虽然Lamport本人连发三篇文章都没能让大多数同行理解Paxos但当时间来到了2006年Google的Chubby、Megastore和Spanner等分布式系统都使用Paxos解决了分布式共识的问题并将其整理成正式的论文发表。

之后得益于Google的行业影响力再加上Chubby的作者Mike Burrows那略显夸张但足够吸引眼球的评价推波助澜Paxos算法一夜间成为计算机科学分布式这条分支中最炙手可热网红概念开始被学术界众人争相研究。

2013年因为对分布式系统的杰出理论贡献Lamport获得了2013年的图灵奖。随后才有了Paxos在区块链、分布式系统、云计算等多个领域大放异彩的故事。其实这样充分说明了在技术圈里即使再有本事也还是需要好好包装一下。

讲完段子吃过西瓜希望你没有被这些对Paxos的“困难”做的铺垫所吓倒反正又不让你马上去实现它。

其实说难不难如果放弃些许严谨性并简化掉最繁琐的分支细节和特殊情况的话Paxos是完全可以去通俗地理解的。毕竟Lamport在论文中只用两段话就描述“清楚”了Paxos的工作流程。

下面我们来正式学习Paxos算法在本小节中Paxos指的都是最早的Basic Paxos算法

Paxos算法的工作流程

Paxos算法将分布式系统中的节点分为提案节点、决策节点和记录节点三类。

提案节点称为Proposer提出对某个值进行设置操作的节点设置值这个行为就是提案Proposal。值一旦设置成功就是不会丢失也不可变的。

需要注意的是Paxos是典型的基于操作转移模型而非状态转移模型来设计的算法所以这里的“设置值”不要类比成程序中变量的赋值操作而应该类比成日志记录操作。因此我在后面介绍Raft算法时就索性直接把“提案”叫做“日志”了。

决策节点称为Acceptor是应答提案的节点决定该提案是否可被投票、是否可被接受。提案一旦得到过半数决策节点的接受就意味着这个提案被批准Accept。提案被批准就意味着该值不能再被更改也不会丢失且最终所有节点都会接受它。

记录节点被称为Learner不参与提案也不参与决策只是单纯地从提案、决策节点中学习已经达成共识的提案。比如少数派节点从网络分区中恢复时将会进入这种状态。

在使用Paxos算法的分布式系统里所有的节点都是平等的它们都可以承担以上某一种或者多种角色。

不过为了便于确保有明确的多数派决策节点的数量应该被设定为奇数个且在系统初始化时网络中每个节点都知道整个网络所有决策节点的数量、地址等信息。另外在分布式环境下如果说各个节点“就某个值提案达成一致”代表的意思就是“不存在某个时刻有一个值为A另一个时刻这个值又为B的情景”。

而如果要解决这个问题的复杂度,主要会受到下面两个因素的共同影响:

系统内部各个节点间的通讯是不可靠的。不论对于系统中企图设置数据的提案节点,抑或决定是否批准设置操作的决策节点来说,它们发出、收到的信息可能延迟送达、也可能会丢失,但不去考虑消息有传递错误的情况。 系统外部各个用户访问是可并发的。如果系统只会有一个用户或者每次只对系统进行串行访问那单纯地应用Quorum机制少数节点服从多数节点就已经足以保证值被正确地读写了。

第一点“系统内部各个节点间的通讯是不可靠的”,是网络通讯中客观存在的现象,也是所有共识算法都要重点解决的问题。所以我们重点看下第二点“系统外部各个用户访问是可并发的”,即“分布式环境下并发操作的共享数据”问题。

为了方便理解,我们可以先不考虑是不是在分布式的环境下,只考虑并发操作。

假设有一个变量i当前在系统中存储的数值为2同时有外部请求A、B分别对系统发送操作指令“把i的值加1”和“把i的值乘3”。如果不加任何并发控制的话将可能得到“(2+1)×3=9”和“2×3+1=7”这两种结果。因此对同一个变量的并发修改必须先加锁后操作不能让A、B的请求被交替处理。这可以说是程序设计的基本常识了。

但是,在分布式的环境下,还要同时考虑到分布式系统内,可能在任何时刻出现的通讯故障。如果一个节点在取得锁之后、在释放锁之前发生崩溃失联,就会导致整个操作被无限期的等待所阻塞。因此,算法中的加锁,就不完全等同于并发控制中以互斥量来实现的加锁,还必须提供一个其他节点能抢占锁的机制,以避免因通讯问题而出现死锁的问题。

我们继续看Paxos算法是怎么解决并发操作带来的竞争的。

Paxos算法包括“准备Prepare”和“批准Accept”两个阶段。

第一阶段“准备”Prepare就相当于抢占锁的过程。如果某个提案节点准备发起提案必须先向所有的决策节点广播一个许可申请称为Prepare请求。提案节点的Prepare请求中会附带一个全局唯一的数字n作为提案ID决策节点收到后会给提案节点两个承诺和一个应答。

其中两个承诺是指承诺不会再接受提案ID小于或等于n的Prepare请求承诺不会再接受提案ID小于n的Accept请求。

一个应答是指在不违背以前作出的承诺的前提下回复已经批准过的提案中ID最大的那个提案所设定的值和提案ID如果该值从来没有被任何提案设定过则返回空值。如果违反此前做出的承诺也就是说收到的提案ID并不是决策节点收到过的最大的那就可以直接不理会这个Prepare请求。

当提案节点收到了多数派决策节点的应答称为Promise应答可以开始第二阶段“批准”Accept过程。这时有两种可能的结果

如果提案节点发现所有响应的决策节点此前都没有批准过这个值即为空就说明它是第一个设置值的节点可以随意地决定要设定的值并将自己选定的值与提案ID构成一个二元组(id, value)再次广播给全部的决策节点称为Accept请求。 如果提案节点发现响应的决策节点中已经有至少一个节点的应答中包含有值了那它就不能够随意取值了必须无条件地从应答中找出提案ID最大的那个值并接受构成一个二元组(id, maxAcceptValue)然后再次广播给全部的决策节点称为Accept请求

当每一个决策节点收到Accept请求时都会在不违背以前作出的承诺的前提下接收并持久化当前提案ID和提案附带的值。如果违反此前做出的承诺即收到的提案ID并不是决策节点收到过的最大的那允许直接对此Accept请求不予理会。

当提案节点收到了多数派决策节点的应答称为Accepted应答协商结束共识决议形成将形成的决议发送给所有记录节点进行学习。整个过程的时序图如下所示

到这里整个Paxos算法的工作流程就结束了。

如果你之前没有专门学习过分布式的知识那学到这里你的感受很可能是操作过程中的每一步都能看懂但还是不能对Paxos算法究竟是如何解决协商共识这个问题形成具体的认知。

所以接下来我就不局限于抽象的算法步骤以一个更具体例子来讲解Paxos。这个例子以及其中使用的图片都来源于“Implementing Replicated Logs with Paxos”。

借助一个例子来理解Paxos算法

在这个例子中,我们只讨论正常通讯的场景,不会涉及网络分区。

假设一个分布式系统有五个节点分别是S1、S2、S3、S4和S5全部节点都同时扮演着提案节点和决策节点的角色。此时有两个并发的请求希望将同一个值分别设定为X由S1作为提案节点提出和Y由S5作为提案节点提出我们用P代表准备阶段、用A代表批准阶段这时候可能发生下面四种情况。

情况一比如S1选定的提案ID是3.1全局唯一ID加上节点编号先取得了多数派决策节点的Promise和Accepted应答此时S5选定的提案ID是4.5发起Prepare请求收到的多数派应答中至少会包含1个此前应答过S1的决策节点假设是S3。

那么S3提供的Promise中必将包含S1已设定好的值XS5就必须无条件地用X代替Y作为自己提案的值。由此整个系统对“取值为X”这个事实达成了一致。如下图所示

情况二事实上对于情况一X被选定为最终值是必然结果。但从图中可以看出X被选定为最终值并不是一定要多数派的共同批准而只取决于S5提案时Promise应答中是否已经包含了批准过X的决策节点。

比如下图所示S5发起提案的Prepare请求时X并未获得多数派批准但由于S3已经批准的关系最终共识的结果仍然是X。

情况三当然另外一种可能的结果是S5提案时Promise应答中并未包含批准过X的决策节点。

比如应答S5提案时节点S1已经批准了X节点S2、S3未批准但返回了Promise应答此时S5以更大的提案ID获得了S3、S4和S5的Promise。这三个节点均未批准过任何值那么S3将不会再接受来自S1的Accept请求因为它的提案ID已经不是最大的了。所以这三个节点将批准Y的取值整个系统最终会对“取值为Y”达成一致。

情况四从情况三可以推导出另一种极端的情况如果两个提案节点交替使用更大的提案ID使得准备阶段成功但是批准阶段失败的话这个过程理论上可以无限持续下去形成活锁Live Lock。在算法实现中会引入随机超时时间来避免活锁的产生。

到这里我们就又通过一个例子算是通俗地学习了一遍Paxos算法。

虽然Paxos是以复杂著称的算法但我们上面学习的是基于Basic Paxos、以正常流程未出现网络分区等异常、以通俗的方式讲解的Paxos算法并没有涉及到严谨的逻辑和数学原理也没有讨论Paxos的推导证明过程。所以这对于大多数不从事算法研究的技术人员来说理解起来应该也不会太过困难。

Basic Paxos的价值在于开拓了分布式共识算法的发展思路但因为它有如下缺陷一般不会直接用于实践Basic Paxos只能对单个值形成决议并且决议的形成至少需要两次网络请求和应答准备和批准阶段各一次高并发情况下将产生较大的网络开销极端情况下甚至可能形成活锁。

总之Basic Paxos是一种很学术化、对工业化并不友好的算法现在几乎只用来做理论研究。实际的应用都是基于Multi Paxos和Fast Paxos算法的在下一讲我们就会了解Multi Paxos以及和它理论等价的几个算法比如Raft、ZAB等算法。

小结

今天这节课,我们从分布式系统的高可靠与高可用的矛盾开始,首先学习了分布式共识算法的含义,以及为什么需要这种算法。我们也明确了“共识”这个词,在这个上下文中所指含义,就是“各个分布式节点针对于某个取值达成一致”。

其次我们了解了Basic Paxos算法发表的一些背景历史以及这种算法的主要工作流程。尽管我们很少有机会去研究或者实现分布式共识算法但理解它的基本原理是我们日后理解和使用etcd、ZooKeeper等分布式框架的重要基础。

一课一思

今天这节课我们一起学习了Paxos算法在正常情况下的工作流程。你认为Paxos在工程上需要考虑哪些异常情况呢实现的难度在哪里呢

欢迎在留言区分享你的思考和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。