一、写在之前
Byzantine 算法面对分布式系统中节点的可信问题,提出了可信节点达成一致性解决方法。Paxos 算法是为了解决分布式系统中节点失效而提出的一致性算法。就分布式存储系统而言,Paxos 算法的价值更大,因为节点是可信的且会因为故障或者扩容发生节点失效。
不仅只用在分布式系统,凡是多个过程需要达成某种一致性的都可以用到Paxos 算法。一致性方法可以通过共享内存(需要锁)或者消息传递实现,Paxos 算法采用的是后者。下面是Paxos 算法适用的几种情况:一台机器中多个进程/线程达成数据一致;分布式文件系统或者分布式数据库中多客户端并发读写数据;分布式存储中多个副本响应读写请求的一致性。
Lamport 最初Paxos 算法的论文The Part-Time Parliament 在理解起来比较有挑战性,个人认为部分原因是Lamport 通过故事的方式来表述、解释这个问题,所以在阅读文章的时候读者需要透过故事讲的本身看到作者想说明什么。比如文章中会有很多讲到Paxon 文明没有被发现和考证的,这些映射到实际系统中往往是简单、大家都心知肚明的基础,但如果读者苦于想知道这些内容是什么时,就上当了。下面章节安排如下:第二节对应原文的1.1-2.1。第三节对应原文2.2-3.2。
二、Paxos 算法中的数学
2.1 问题的描述
既然Lamport 是通过故事的方式提出Paxos 问题,我们就有必要简述下这个问题:希腊岛屿Paxon 上的执法者(legislators,后面称为牧师priest)在议会大厅(chamber)中表决通过法律,并通过服务员传递纸条的方式交流信息,每个执法者会将通过的法律记录在自己的账目(ledger)上。问题在于执法者和服务员都不可靠,他们随时会因为各种事情离开议会大厅,并随时可能有新的执法者(或者是刚暂时离开的)回到议会大厅进行法律表决,使用何种方式能够使得这个表决过程正常进行,且通过的法律不发生矛盾。
说明:不难看出故事中的议会大厅就是我们的分布式系统,每个牧师就是对应的每个节点或者进程,服务员传递纸条的过程即通信的过程,法律即是我们需要保证一致性的值(value)。牧师和服务员的进出对应着节点/网络的失效和加入,牧师的账目对应节点持久化存储设备。上面表决过程正常进行可以进一步表述为过程需求(progress requirements):当大部分牧师在议会大厅呆了足够长时间,且期间没有牧师进入或者退出,那么提出的法案应该被通过并被记录在每个牧师的账目上。
2.2 Paxos 算法的数学基础
Paxon 中的法律通过选举(ballots)完成,每次选举涉及到的一群牧师称为法定人数(quorum),当且仅当法定人数中的所有牧师都赞成这个法案时,选举成功并通过该法律。每次选举B 包含以下内容:
- B_dec 正在进行的选举
- B_qrm 法定人数牧师的集合(非空牧师集合)
- B_vot 赞成的牧师集合
- B_bal 选举编号
有了以上定义,我们看出选举B 通过的充要条件是:B_qrm 属于 B_vot。接着我们定义B 为一个选举的集合,并说明选举如果满足下面三个条件,那么一致性可以得到保证。实际中每一次选举都可以看做是一次读写请求,所有法定人数的牧师赞成才通过法律表示:所有涉及到这次请求的节点都同时响应请求(比如更新某个值)才能保证一致性。这里选举编号的大小在实际中并不代表选举发起的时间。下面给出三个重要的定义:
- B1(B) B 中每个选举都有一个独一无二的选举编号。
- B2(B) B 中每两个选举至少有一个共同的牧师。
- B3(B) B 中每一次选举B ,如果其法定人数中任意牧师在之前的一次选举中赞成,那么这次选举B 等于之前最近一次有B 中牧师赞成的选举。即新的法律等于所有参与选举牧师中最近投赞成票的法律。
说明:看到这里,读者八成已经很迷糊了,下面我们以一个版本更新的分布式key-value 数据库为例,每个key-value 有多个副本,如果客户端发起一个update(key,vaule) 的操作,则会产生由一个节点发起、相关节点进行响应的一次一致性操作,即选举B。对保存了该key-value 的副本进行更新。需要注意的是法定人数牧师(B_qrm)是例子中所有保存这个key-value 副本的节点的一个大部分子集,因为可能在某些时候某些保存这个key-value 副本的节点不可达。B 是关于某个key-value 的一系列更新操作,不同的法律实际上是一个key-value 的不同值。那么B1-B3就好明白了,B1指一次只进行一个更新操作;B2指每两次更新操作必须有共同的节点参与;B3指某次key-value 操作的key-value 值与所有参与节点中之前进行投赞成票的最新值一致。这是因为如果某个节点在之前已经投票,说明它已经确认可以修改该值,而其他法定人数的牧师/节点还没有确认该值。
下面说明为什么B1-B3 蕴含一致性!
引理1.1 如果B1(B),B2(B)$ 和B3(B) 满足,那么对于在B中的任意B 和B’ ,有
证明略,有兴趣的可以参考原文
定理1.2 如果B1(B),B2(B) 和B3(B) 满足,那么对于在B 中的任意B 和B’ ,有
证:如果$B’_bal=B_bal 那么由B1(B) 可知B’=B 。如果B’_bal 不等于 B_bal ,那么总有一个编号大、一个小,根据引理1.1 可得。
定理1.3 b>B_bal 且对于所有B 中的B 都有Q 和 B_qrm 交集不为空。有一个选举B’ 满足B’_bal=b、B’_qrm=B’_vot=Q,那么如果B1(B)、B2(B)、B3(B)满足,则B1(B并B’)、B1(B并B’)、B1(B并B’) 也满足。
证明略,见原文。
这个定理说的是在一个选举集合B 之后的每次成功选举,只要和之前集合中每次选举都有交集,那么这些成功的选举合并选举集合B 满足一致性。
三、几种Paxos 算法
上面通过证明如果一个协议满足B1-B3 约束条件,那么就可以保证一致性。直接从这些约束得到preliminary protocol ,basic protocol 是preliminary protocol 的限制版,保证了一致性。complete Synod protocol 进一步限制了basic protocol ,满足一致性和过程需求(progress requirements)。下面将这三个算法的具体过程。
3.1 初始协议
满足B1,牧师发起选举的编号必须满足偏序关系,有一个方法是每个发起牧师使用递增的数值作为选举编号,但这样牧师无法立即知道他们选的数值有没有被其他牧师选作选举编号已经被使用。还有一个方法是使用数字+牧师姓名作为选举编号,这样就避免了自己的选举编号被其他牧师使用。
满足B2,每次选举的法定人数必须是一个大部分集合(majority set)Q,这样任意两个选举都会有一个共同的牧师。这里大部分集合是一个灵活的选择,在原文中Lamport 使用体重打比方,体重的人更有可能呆在议会大厅,这样就可以使用体重超过一半的牧师集合作为大部分集合。至于实际情况中的大部分集合是什么要看具体情况了。
满足B3,要求每个牧师p 每次在发起选举前必须找到B_qrm 中每个牧师q 的MaxVote(b,q,B)。
根据以上要求,可以得到初始协议:
- 牧师p 选择一个选举编号b ,并发送NextBallot(b)送给其他牧师
- 其他牧师q 在收到NextBallot(b) 后,返回LastVote(b,v) 给牧师p,v=MaxVote(b,q,B)$是小于b 编号的q 投的最大的赞成票。为了保证B3,q 不能在b 和b_bal 之间的选举投赞成票。(如果q 在发送了LastVote(b,v)又对新的选举投票了那么v 也就不是q 投的最大赞成票)
- 牧师p 从一个大部分集合Q 中每个牧师q 中都收到LastVote(b,v) 后,发起一个新的选举,编号为b,法定人数为Q,法律d满足B3。然后牧师p 将这个法律写在自己账目的背面,发送BeginBallot(b,d)给Q 中每个牧师。
- 牧师q 收到BeginBallot(b,d) 后决定是否为这次选举投赞成票,如果赞同,则他将发送Vote(b,q) 给牧师p。
- 如果牧师p 收到Q 中每个牧师q 发来的赞成票Vote(b,q),则将法律d 写入他的账目中,并向所有q发送Success(d) 消息。
- 收到Success(d) 消息后,牧师q 将法律d 写入到自己的账目中。
说明:第一步表示发起法律的牧师p 希望下一个选举的编号是b 。牧师q 用LastVote(b,v) 回应了牧师p 的请求,也就是向牧师p 通过法律时保证了v=MaxVote(b,q,B) 的被改变,具体来说就是不在b 和b_bal 之间的选举投赞成票。
第三步要求法律d 需要满足B3,这里我开始有点迷糊,实际系统中的值是客户端决定的,而不应该是B3 决定的。这里我们还是用上面的key-value 数据库的例子来理清下思路:当某个节点/牧师第一次发起更新前相当于B 为空集,发起更新/选举的操作不断进行,直至所有法定人数(quorum)都对法律投了赞成票(即majority set 的节点都更新了该key-value 的值则认为更新成功),B3对应的就是之前的更新没有成功,那么新的选举值需要保持的情况。第四步允许牧师可以不发送Vote(b,q) 或者发送几次,对应的是发送的信息可能因为通信而失败而未发送或者被多次发送。一旦牧师投了赞成票则确认可以修改该值。
考虑到最后第六步法律d 才被牧师q 写入到账目,有可能出现的情况就是在第五步的时候牧师p 将法律写入到了自己账目中,接着发送Success(d) 给其他牧师,其中因为通信或者牧师离开议会大厅而没有被写入到自己的账目中,导致不一致。所以真正写入到账目时机应该是在第四步牧师q 在发送给牧师p 赞成票的同时就法律写入到了各自账目中。而不用考虑如何保证牧师q 第四步写入的法律会导致不一致,因为法律如果没有通过则还有更多的选举来保证一致性。后面也谈到了当法律第一次别写入到账目中算通过法律。
3.2 基础协议
初始协议(Preliminary Protocol)要求每个牧师都保存 (i) 他发起的每个选举; (ii) 他投的每个赞成票; (iii) 他发送的每个$LastVote$。为了简化牧师需要保存的数据,我们队上面的协议做一个限制,得到基础(Basic Protocol)协议。首先介绍三个新的参数:
- lastTried[p] 牧师p 发起的最后一个选举
- prevVote[p] 牧师p 最近一次的投票
- nextBal[p] 收到的选举编号的b 的最大值,即牧师p参加的最大选举编号
在初始协议中,每个牧师可以同时发起任意个选举,在基础协议中要求每个牧师只能发起一个选举lastTried[p],一旦发起一个选举,那么之前发起选举的信息就都不重要了。在初始协议中要求每个牧师不能在b_bal 和b 之间投赞成票,在基础协议中则更严格地要求不能给小于b 的选举投赞成票。那么基础协议可以概述为下面几步:
- 牧师p 选择一个大于lastTried[p] 的选举编号b ,发送NextBallot(b)给其他牧师
- 牧师q 收到NextBallot(b) 且b>nextBal[q] 后设置NextBallot(b)=b ,接着发送LastVote(b,v) 给牧师p,其中v==prevBa[q] 。(如果b 小于或等于nextBal[q],则不回复)
- 从满足某个大部分集合Q 中每个牧师收到了LastVote(b,v) 信息,牧师p 发起一个编号为b ,法定人数为Q ,法律为d(满足B3 )的选举,并将BeginBallot(b,d) 发送给Q 中每个牧师。(如果没有满足任意大部分集合Q 的牧师返回,则返回第一步)
- 牧师q 收到BeginBallot(b,d) ,决定投赞成票,设置prevVote[p] 为这次投票,并发送Vote(b,q) 给牧师p。(如果在收到BeginBallot(b,d) 后发现b 不等于nextBal[q] 则忽略这条信息,说明这期间牧师q 还收到了其他的编号更大的选举)
- 牧师p 从大部分集合Q 中每个牧师q 收到了Voted(b,d) ,且b==lastTried[p] ,则认为这次选举成功,将法律d 记录在账目中,并向Q 中每个牧师q 发功成功消息Success(d) 。
- 每个牧师q 收到Success(d) 消息后将法律写入账目。
基础协议是初始协议的限制版,因为两者都对牧师没有行为要求,所以也不保证过程(QS)。下面介绍一个保证过程的协议— 完整议会协议(complete Synode protocol)。
3.3 完整议会协议
基础协议保证了一致性却没有保证任何过程,因为它只阐述了牧师可能做什么,没有要求牧师应该做什么。为了达到之前谈到的过程需求(Qrogress Requirements),我们需要添加一些额外的要求使得牧师们尽快执行完2-6 步。
考虑一种情况如果牧师q 第二步收到的选举编号b 都比之前收到的要大,那么他就要放弃之前收到的所有选举。可是在选举编号为b 的选举在未确认前,可能又会收到更大编号的选举b’ ,这样就无法通过任何法律,过程也不能保证。所以为了达到过程需求则需要一个选举成功后再发起另一个选举。而首先应该知道服务员传递消息和牧师处理消息的时间,在网络中常常通过设置timeout 来实现,同样的如果超过了一定时间牧师没有收到服务员的回复,则认为该服务员或者对应的牧师离开了议会大厅。
假设牧师执行一个动作在7 分钟以内,服务员传递一个消息在4 分钟以内,那么一个牧师p 发送消息给牧师q ,希望其回复的时间应该是在22 分钟内(7+4+7+4 分钟)。
有了上面时间的假设,再考虑上面讨论过的情况,如果发起选举的牧师p 会在第二步和第四步期望22 分钟内收到其他牧师的回复,如果没有则可能是一些牧师或者服务员离开了议会大厅,或者还有一些牧师发起了编号更大的选举。遇到这两种情况都牧师p 应该终止本次选举,而重新开始发起一个新的选举,为了不至于新发起的选举编号还是太小而仍不能执行,需要从其他牧师哪里获取最新的选举编号,从而选取一个更大的编号发起选举。
进而假设牧师p 是唯一能够发起选举的牧师且议会大厅内有大部分集合的牧师,那么可以保证在99分钟内通过一条法律:22 分钟内发现了有更大编号的法律,22 分钟内获取最大编号并选择个更大的编号,55 分钟内完成1-6 步完成一次成功的选举(疑问:既然只有牧师p 能够发起选举,那么编号都是由其控制的,前两步发现并选择更大的编号似乎就没有必要了。答:并不是所有的选举都是president发起的,其他牧师发起选举,president向其他希望发起选举的牧师配发选举编号)。从上面的过程我们发现完整议会协议需要一个选举president的过程,president的选举算法不是文章重点,所以文章中仅用T 分钟代替了选举president的时间,这样T+99 分钟内可以通过一部法律。
文中选择president的方法是谁的姓在字母表中最后,并将自己的姓发送给议会大厅内所有牧师,如果在T-11 分钟内某个牧师没有收到比自己姓在字母表中更靠后的姓,则认为自己是president(我觉得广播体重也应该不错,不是说体重更重的呆在议会大厅会更久么?^_^)。还有一个细节:在选举president的时候每个牧师p 需要将自己的lastTried[p] 发送给其他牧师,以使得president能够在第一次选举时选择一个足够大的编号。
至此,通过选举president和设置超时,完整议会协议就可以保证过程了。
3.4 多法律国会协议
上节的议会协议(complete Synod protocol)中,president被选举出来后,每个希望发起选举的牧师通知他,president给牧师配发选举编号,每次仅通过一部法律。多法律国会协议(The Multi-Decree Parliment)选择一个president通过一系列法律,且只需要执行前两步一次即可。
具体方法是president第一步发送NextBallot(b,n) 代替NextBallot(b) ,表示希望通过n-b 之间的所有的法律,在president 的账目上,编号n 之前的法律都是连续记录了的,b>n 。其他牧师q 收到消息后将每部已经出现在其账目中编号大于$n$的法律都返回给president,不在账目上的返回正常的LastVote 信息。
下面谈到多法律国会协议有关性质,首先是法律的顺序,不同法律编号的选举同时进行,发起选举的每个牧师都认为自己是president(不知道president 是怎么选举出来的,也不知道法律通过的顺序)。在完整议会协议第三步中法律被提议,第一次写入到账目上时称法律被通过。当一个president需要提出新的法案时,他必须从大部分集合牧师中学习到那么法律他们都投了赞成票,每部法律都被大部分集合牧师中至少一个牧师投了票,所以president发起新的选举前总能学到所有之前通过了的法律。president不会在空缺的法律编号内填补重要的法律。,也不会乱序提议法律,所以协议满足“法律有序性”:如果法律A 和法律B 都是重要的法律,法律A 在法律B 提议之前通过,那么法律A 有比法律B 更低的法律编号。第二点属性是president在选举出后且没有人再进出议会大厅,法律是按照下面步骤不断通过的(对应完整议会协议的3-5步):
3. president 向一个法定人数牧师中每个牧师发送BeginBallot ;
4.每个牧师向president 发送Voted 信息。
5.president向每个牧师发送Success 消息。这样通过每部法律只需要三次消息传递,通过合并BeginBallot 和Success 命令可以进一步减少消息传递。
以上根据最近学习Paxos 算法总结而得,如有不妥请多指教。