Paxos杂谈

152 阅读15分钟

前言

Paxos作为第一个被证明的分布式一致性协议,在google、微软成功的被应用到核心工程项目中,在分布式系统领域做出了很大的贡献。但由于其性能以及实现的难度,使得该技术无法普及开来。本文将尽可能采用通俗一些的表述,围绕 Paxos协议介绍,算法过程,怎么证明,应用场景,如何工程化5个角度来进行讨论。希望能对读者理解Paxos提供帮助。

在此之前,大家不妨一读《Paxos Made Simple》。作者在论文中渐进式地,中间用数学归纳法进行了证明,从零开始推导出了Paxos协议。

Paxos协议介绍

Paxos协议的目标就是在异步通信环境中,经过轮投票确定出一个一致的内容(论文称为value)。执行多轮paxos协议便可以确定多个内容。

分布式环境

Paxos一致性协议是在特定的环境下才需要的,这个特定的环境称为异步通信环境。而恰恰,几乎所有的分布式环境都是异步通信环境。

异步通信环境指的是消息在网络传输过程中,可能发生丢失,延迟,乱序现象。消息乱序是一个非常恶劣的问题,这个问题导致大部分协议在分布式环境下都无法保证一致性,而导致这个问题的根本原因是网络包无法控制超时,一个网络包可以在网络的各种设备交换机等停留数天,甚至数周之久,而在这段时间内任意发出的一个包,都会跟之前发出的包产生乱序现象。无法控制超时的原因更多是因为时钟的关系,各种设备以及交换机时钟都有可能错乱,无法判断一个包的真正到达时间。

异步通信环境并非只有paxos能解决一致性问题,经典的两阶段提交也能达到同样的效果,但是分布式环境里面,除了消息网络传输的恶劣环境,还有另外一个严重问题,就是机器的宕机,甚至永久失联。在这种情况下,两阶段提交将无法完成一个一致性的写入,而paxos,能容忍在只有多数派机器存活的情况下,仍然能完成写入,并保证一致性。

至此,总结一下paxos就是一个在异步通信环境,并容忍在只有多数派机器存活的情况下,仍然能完成一个一致性写入的协议。

三个约束

Paxos三个约束.png

上图取自《The part-time parliarment》中的三个约束。其中:

β:一轮协议中所有的投票集合

B:β中的某一次投票

Bbal:投票的标识符(ballot number)

Bqrm:参与投票的节点中的多数派,比如有ABC三个节点参与投票,那么Bqrm可以是(A,B),

也可以是(B,C), (A,C)。

Bdec:本次投票的内容

MaxVote(Bbal,Bqrm,β):β中的一次投票,含义为Bqrm中的每个节点参与过的投票中,比Bbal小但bal最大的那一轮(也可能是空,比如Bqrm从未参与过任意投票)。

基于上述含义,三条约束可以理解为:

B1:在一轮投票所构成的集合中,每次进行的投票拥有自己的独立编号。

B2:如果任意两次投票都存在多数派,则多数派的交集不为空。

B3:在投票集合中,若存在X=MaxVote(Bbal,Bqrm,β),则Bdec=Xdec。若X为空,则Bdec可为任意值

Paxos算法实现

Paxos本身只是一个协议,并不为计算机所认识,故而Lamport提出了相应的算法,即广为大家所识的paxos算法。Paxos算法由两个角色构成,proposer和acceptor,两者在每一轮协议的若干次投票中通过遵守上述的三条约束,最终选出一个一致性的内容。

值确定过程

Proposal Value: 提议的值

Proposal Number: 提议编号,要求提议编号不能冲突

Proposal: 提议 = 提议的值 + 提议编号

Proposer: 提议发起者

Acceptor: 提议接受者

Learner: 提议学习者

协议中Proposer有两个行为,一个是向Acceptor发Prepare请求,另一个是向Acceptor发Accept请求;Acceptor则根据协议规则,对Proposer的请求作出应答;最后Learner可以根据Acceptor的状态,学习最终被确定的值。

方便讨论,记{n,v}为提议编号为n,提议的值为v的提议,记(m,{n,v})为承诺了Prepare(m)请求,并接受了提议{n,v}。

第一阶段A:

Proposer选择一个提议编号n,向所有的Acceptor广播Prepare(n)请求。

第一阶段B:

Acceptor接收到Prepare(n)请求,若提议编号n比之前接受的Prepare请求都要大,则承诺将不会接受提议编号比n小的提议,并且带上之前Accept的提议中编号小于n的最大的提议,否则不予理会。

第二阶段A:

proposer得到了多数Acceptor的承诺后,如果没有发现有一个Acceptor接受过一个值,那么向所有的Acceptor发起自己的值和提议编号n,否则,从所有接受过的值中选择对应的提议编号最大的,作为提议的值,提议编号仍然为n。

第二阶段B:

Acceptor接收到提议后,如果该提议编号不违反自己做过的承诺,则接受该提议。需要注意的是,Proposer发出Prepare(n)请求后,得到多数派的应答,然后可以随便再选择一个多数派广播Accept请求,而不一定要将Accept请求发给有应答的Acceptor,这是常见的Paxos理解误区。

假如P1广播了Prepare请求,但是给A3的丢失,不过A1、A2成功返回了,即该Prepare请求得到多数派的应答。然后它可以广播Accept请求,但是此时给A1的又丢了,不过A2,A3成功接受了这个提议。因为这个提议被多数派(A2,A3形成多数派)接受,我们称被多数派接受的提议对应的值被Chosen。

如果Acceptor之前都没有接受过Accept请求,所以不用返回接受过的提议,但是如果接受过提议,则根据第一阶段B,要带上之前Accept的提议中编号小于n的最大的提议。Proposer广播Prepare请求之后,收到了A1和A2的应答,应答中携带了它们之前接受过的{n1, v1}和{n2, v2},Proposer则根据n1,n2的大小关系,选择较大的那个提议对应的值,比如n1 >n2,那么就选择v1作为提议的值,最后它向Acceptor广播提议{n, v1}。

当一个提议被多数派接受后,这个提议对应的值被Chosen(选定),一旦有一个值被Chosen,那么只要按照协议的规则继续交互,后续被Chosen的值都是同一个值,也就是这个Chosen值的一致性问题。

学习过程

如果一个提议被多数Acceptor接受,则这个提议对应的值被选定。一个简单直接的学习方法就是,获取所有Acceptor接受过的提议,然后看哪个提议被多数的Acceptor接受,那么该提议对应的值就是被选定的。

另外也可以把Learner看作一个Proposer,根据协议流程,发起一个正常的提议,然后看这个提议是否被多数Acceptor接受。

Paxos算法证明

以上就是基本Paxos算法的全部内容,以下用数学语言表达,进而用严谨的数学语言加以证明。

Paxos原命题:

如果一个提议{n0,v0}被大多数Acceptor接受,那么不存在提议{n1,v1}被大多数Acceptor接受,其中n0 < n1,v0 != v1。

Paxos原命题加强:

如果一个提议{n0,v0}被大多数Acceptor接受,那么不存在Acceptor接受提议{n1,v1},其中n0 < n1,v0 != v1。

Paxos原命题进一步加强:

如果一个提议{n0,v0}被大多数Acceptor接受,那么不存在Proposer发出提议{n1,v1},其中n0 < n1,v0 != v1。

如果“Paxos原命题进一步加强”成立,那么“Paxos原命题”显然成立。下面我们通过证明“Paxos原命题进一步加强”,从而证明“Paxos原命题”。论文中是使用数学归纳法进行证明的,这里用比较紧凑的语言重新表述证明过程。

归纳法证明

假设提议{m,v}(简称提议m)被多数派接受,那么提议m到n(如果存在)对应的值都为v,其中n不小于m。

这里对n进行归纳假设,当n=m时,结论显然成立。

设n=k时结论成立,即如果提议{m,v}被多数派接受,那么提议m到k对应的值都为v,其中k不小于m。当n=k+1时,若提议k+1不存在,那么结论成立。

若提议k+1存在,对应的值为v1,因为提议m已经被多数派接受,又k+1的Prepare被多数派承诺并返回结果。基于两个多数派必有交集,易知提议k+1的第一阶段B有带提议回来。

那么v1是从返回的提议中选出来的,不妨设这个值是选自提议{t,v1}。根据第二阶段A,因为t是返回的提议中编号最大,所以t >= m。又由第一阶段B,知道t < n。所以根据假设t对应的值为v。即有v1 = v。所以由n = k结论成立可以推出n = k+1成立。

于是对于任意的提议编号不小于m的提议n,对应的值都为v。所以命题成立。

应用场景

我们一直都在说paxos是一个一致性协议,但如果从工程界的角度看,我觉得下面的说法会比较好理解:

Paxos的节点们最终为集群仲裁出了一个请求的序列。通过它,我们可以保证集群内每个节点都以同样的顺序串行执行同样的请求。而执行请求的程序,我们一般称之为状态机StatusMachine(SM)。

怎么理解这段话呢,我们一步一步来:

确定一个值

假设有三台机器,每台机器上运行这Acceptor来遵守paxos协议,每台机器的Acceptor为自己的一份Data数据服,可以有任意多个Proposer。当paxos协议宣称一个值被确定(Chosen)后,那么Data数据就会被确定,并且永远不会被改变。

确定多个值

对我们来说,确定一个值,并且当一个值确定后是永远不能被修改的,很明显这个应用价值是很低的。虽然我都甚至还不知道确定一个值能用来干嘛,但如果我们能有办法能确定很多个值,那肯定会比一个值有用得多。我们先来看下怎么去确定多个值。

上文提到一个三个Acceptor和Proposer各自遵守paxos协议,协同工作最终完成一个值的确定。这里先定义一个概念,Proposer,各个Acceptor,所服务的Data共同构成了一个大的集合,这个集合所运行的paxos算法最终目标是确定一个值,我们这里称这个集合为一个paxosinstance,即一个paxos实例。

一个实例可以确定一个值,那么多个实例自然可以确定多个值,很简单的模型就可以构建出来,只要我们同时运行着多个实例,那么我们就能完成确定多个值的目标。

这里强调一点,每个实例必须是完全独立,互不干涉的。意思就是说Acceptor不能去修改其他实例的Data数据,Proposer同样也不能跨越实例去与其他实例的Acceptor交互。

有序确定多个值

如何利用paxos有序的确定多个值?上文我们知道可以通过运行多个实例来完成确定多个值,但为了达到顺序的效果,需要加强一下约束。

首先给实例一个编号,定义为i,i从0开始,只增不减,由本机器生成,不依赖网络。

其次,我们保证一台机器任一时刻只能有一个实例在工作,这时候Proposer往该机器的写请求都会被当前工作的实例受理。

最后,当编号为i的实例获知已经确定好一个值之后,这个实例将会被销毁,进而产生一个编号为i+1的实例。

基于这三个约束,每台机器的多个实例都是一个连续递增编号的有序系列,而基于paxos的保证,同一个编号的实例,确定的值都是一致的,那么多台机器都获得了一个有序的多个值。

实例对齐

假设有机器A B C,其中C由于宕机或者网络问题,其进度落后于A,B(AB两台机器为多数派,可确定一个值),要如何追上进度呢? 上文说到每个实例里面都有一个Acceptor的角色,这里再增加一个角色称之为Learner,顾名思义就是找别人学习,她回去询问别的机器的相同编号的实例,如果这个实例已经被销毁了,那说明值已经确定好了,直接把这个值拉回来写到当前实例里面,然后编号增长跳到下一个实例再继续询问,如此反复,直到当前实例编号增长到与其他机器一致。

由于约束里面保证仅当一个实例获知到一个确定的值之后,才能编号增长开始新的实例,那么换句话说,只要编号比当前工作实例小的实例(已销毁的),他的值都是已经确定好的。所以这些值并不需要再通过paxos来确定了,而是直接由Learner直接学习得到即可。

状态机

我们利用paxos确定有序的多个值这个特点,再加上这里引入的一个状态机的概念,结合起来实现一个真正有工程意义的系统。

状态机这个名词大家都不陌生,一个状态机必然涉及到一个状态转移,而paxos的每个实例,就是状态转移的输入,由于每台机器的实例编号都是连续有序增长的,而每个实例确定的值是一样的,那么可以保证的是,各台机器的状态机输入是完全一致的。根据状态机的理论,只要初始状态一致,输入一致,那么引出的最终状态也是一致的。而这个状态,则可以用来实现非常多的东西。

比如说:大部分存储系统,都是以AppendLog的形式,确定一个操作系列,而后需要恢复存储的时候都可以通过这个操作系列来恢复,而这个操作系列,正是确定之后就永远不会被修改的。到这已经很豁然开朗了,只要我们通过paxos完成一个多机一致的有序的操作系列,那么通过这个操作系列的演进,则可以实现了一个具有多机一致的存储系统。

paxos状态机.png

如上图,一个请求发给Proposer,Proposer与相同实例编号为x的Acceptor协同工作,共同完成一值的确定,之后将这个值作为状态机的输入,产生状态转移,最终返回状态转移结果给发起请求者。

工程化

严格落盘

Paxos协议的运作工程需要做出很多保证(Promise),这个意思是我保证了在相同的条件下我一定会做出相同的处理,如何能完成这些保证?众所周知,在计算机里面,一个线程,进程,甚至机器都可能随时挂掉,而当他再次启动的时候,磁盘是他恢复记忆的方法,在paxos协议运作里面也一样,磁盘是她记录下这些保证条目的介质。

而一般的磁盘写入是有缓冲区的,当机器当机,这些缓冲区仍然未刷到磁盘,那么就会丢失部分数据,导致保证失效,所以在paxos做出这些保证的时候,落盘一定要非常严格,严格的意思是当操作系统告诉我写盘成功,那么无论任何情况都不会丢失。这个我们一般使用fsync来解决问题,也就是每次进行写盘都要附加一个fsync进行保证。

Fsync是一个非常重的操作,也因为这个,paxos最大的瓶颈也是在写盘上,在工程上,我们需要尽量通过各种手段,去减少paxos算法所需要的写盘次数。

万一磁盘fsync之后,仍然丢失或者数据错乱怎么办?这个称之为拜占庭问题,工程上需要一系列的措施检测出这些拜占庭错误,然后选择性的进行数据回滚或者直接丢弃。

降低冲突

Paxos一个实例,支持任意多个Proposer同时进行写入,但是最终确定出来一个相同的值,里面是运用了一些类似锁的方法来解决冲突的,而越多的Proposer进行同时写入,冲突的剧烈程度会更高,虽然完全不妨碍最终会确定一个值,但是性能上是比较差的。所以这里需要引入一个Leader的概念。

Leader就是领导者的意思,顾名思义我们希望有一个Proposer的领导者,优先由他来进行写入,那么当在只有一个Proposer在进行写入的情况下,冲突的概率是极小的,这样性能会得到一个飞跃。这里再次重申一下,Leader的引入,不是为了解决一致性问题,而是为了解决性能问题。

由于Leader解决的是性能问题而非一致性问题,即使Leader出错也不会妨碍正确性,所以我们只需要保证大部分情况下只有一个Proposer在工作就行了,而不用去保证绝对的不允许出现两个Proposer或以上同时工作,那么这个通过一些简单的心跳以及租约就可以做到。

记录实例编号

当一个实例已经完成值的确认之后,我们必须确保已经输入到状态机并且进行了状态转移,之后我们才能开启新的实例。但,当机器重启或者进程重启之后,状态机的数据可能会由于自身实现问题,或者磁盘数据丢失而导致回滚,

这个我们没办法像上文提到的fsync一样进行这么强的约束,所以提出了一种方法,状态机必须严格的记得自己输入过的最大实例编号。

这个记录有什么用?在每次启动的时候,状态机告诉paxos最大的实例编号x,而paxos发现自己最大的已确定值的实例编号是y,而x < y. 那这时候怎么办,只要我们有(x, y]的chosenvalue,我们重新把这些value一个一个输入到状态机,那么状态机的状态就会更新到y了,这个我们称之为启动重放。