Basic Paxos算法解析

历史溯源

对Paxos追踪溯源还得从遥远的拜占庭说起,拜占庭位于如今的土耳其伊斯坦布尔,是东罗马帝国的首都。由于当时拜占庭罗马帝国国土辽阔,为了防御目的,每个军队都分隔很远,将军与将军之间通过派遣信使相互通信协调发起攻击。因为这些将军位于不熟悉和敌对的区域,信使可能未到达目的地(如同分布式网络中的节点可能失败,或者发送的信息被破坏和篡改)。这个问题的另一方面是某些将军可能是叛徒,他们可能个人叛变或者合谋成为叛变团体,因此他们发送消息的目的旨在使仍然忠诚的将军们发动注定失败的计划(如同分布式系统中的邪恶成员尝试让系统接受伪造的消息),拜占庭问题就此形成。拜占庭假设是对现实世界的模型化,由于硬件错误、网络拥塞或断开以及遭到恶意攻击,计算机和网络可能出现不可预料的行为。

Leslie Lamport老先生一直研究这类问题,大家也许对他不太熟悉,但提起LaTex肯定都知道,Lamport就是LaTex的作者,2013年图灵奖的获得者。他从小天赋异禀,麻省理工数学系本科毕业后,在素有“犹太哈佛”美称的布兰迪斯(Brandeis)大学获得数学硕士和博士学位。Lamport解决了下面三个问题:

1. 类似拜占庭将军这样的分布式一致性问题是否有解?
2. 如果有解的话需要满足什么样的条件?
3. 在特定前提条件的基础上,提出一种解法。

前两个问题在论文《The byzantine Generals Problem》中给出了答案,在后来的论文《The part time parliament》中给出了一种叫Paxos的算法解决了第三个问题。

算法分析

Paxos算法是在多个成员之间对某个值(提议)达成一致的协议。这个值可以是任何东西。比如多个成员之间进行选主,那么这个值就是主的身份。在应用到日志同步中时,这个值就是一条日志。通过多次Paxos的执行就可确定出一个日志的序列,保证在主备服务器上看到相同的日志顺序。Paxos算法有很多变种,旨在提高共识收敛速度、减少通信延迟或减少磁盘IO次数,典型的包括Multi-Paxos、Fast Paxos、Generalized Paxos等,今天我们就以《The part time parliament》论文中的场景聊聊Basic Paxos算法的工作原理。

场景介绍

希腊岛屿Paxon上的执法者(legislators,后面称为牧师priest)在议会大厅(Chamber)中表决通过法律,并通过服务员传递纸条的方式交流信息,每个执法者会将通过的法律(Decree)记录在自己的账目(Ledger)上。问题在于执法者和服务员都不可靠,他们随时会因为各种事情离开议会大厅,并随时可能有新的执法者进入议会大厅进行法律表决,使用何种方式能够使得这个表决过程正常进行,且通过的法律不发生矛盾。

不难看出故事中的议会大厅就是我们的分布式系统,牧师对应节点或进程,服务员传递纸条的过程就是消息传递的过程,法律即是我们需要保证一致性的值(Value)。牧师和服务员的进出对应着节点/网络的失效和加入,牧师的账目对应节点中的持久化存储设备。上面表决过程的正常进行可以表述为算法的推进需求(Progress requirements):当大部分牧师在议会大厅呆了足够长时间,且期间没有牧师进入或者退出,那么提出的法案应该被通过并被记录在每个牧师的账目上。

工作机制

牧师们以表决的方式投票通过议案,一份议案的单次表决称为“一轮(round)”,每轮表决牧师可以选择投票或不投票。参与每轮表决的牧师集称为“quorum”,只有quorum中的全体牧师都对议案投了票,议案才算通过。因此,一轮表决包含以下部分:

  - Bdec:表决的议案;
  - Bqrm:参与议案表决的非空牧师集(round的quorum);
  - Brnd:轮号(每轮投票都会有一个递增的数字编号);

轮号是一个无限的有序数集,如果B'rnd > Brnd,表示Round B'rnd排在Brnd之后,但这并不表示二者实际执行的先后顺序,有可能Brnd先于B'rnd执行。

假设Rs为Round的集合,Paxon的数学家们对Rs中的多轮投票定义了下面的三个条件,满足这些条件就能保证从众多次N多议案的表决中选出一个所有牧师都一致认可的议案。这种说法稍稍不太严谨,因为这三个条件只是保证了议案的一致性,有可能无数round投票都“议而不决”,但加上些许条件就能保证算法的推进(Progress)。

  - C1:每轮投票都有唯一的编号;
  - C2:任何两轮的牧师quorum有交集;

  - C3:对Rs中的每个R,如果R的quorum中有牧师在早于R的某轮进行过投票,设那些有过投票的较早round的议案集为D,D中Round号最接近R的议案为d,则R轮投票的议案必须等于d。

通过下面的图例可以更清楚的表示出这些限制条件的含义。Rs是一个包含5 Rounds的集合,总共有Α、Β、Г、Δ和Ε 5个牧师。每Round投票的牧师是quorum的子集,图示中名字由方框圈住。例如,编号为14的Round对议案α进行表决,quorum包含三个牧师,其中2个投票者。


1. 编号为2的Round是最早的表决轮,无论对哪个议案表决都满足三个限制条件;
2. 编号为5的Round中4个quorum成员在更早的Round中都没有投过票,因此也满足条件;
3. Round 14中仅有成员Δ在更早的Round 2中投过票,条件3要求Round 14的议案必须等于Round 2的议案,即α;
4. Round 27成功通过了议案,因为quorum成员的全体Α、Г和Δ都对β议案进行了投票、牧师Α没有在更早的Round进行过投票,牧师Г之前投票最早的Round是5,牧师Δ之前投票最早的Round是2,因此,条件C3要求Round 27的表决议案必须等于Round 5的议案,即β;
5. Round 29的quorum是Β、Г和Δ。Β在Round 14投了票,Г在Round 5和Round 27投了票,Δ在Round 2和Round 27投了票,这些投票中最近的Round是27,因此条件C3要求Round 29的议案必须等于Round 27的议案,即β。

Basic Paxos算法

从满足条件C1-C3出发就可以构造出基本的Paxos算法。为了满足条件C1,需要确保各个牧师独立提出的Round号之间具有全序关系(Total Order),一种可行的编号方式由数字和牧师名的Pair对表示,数字相同时,按照牧师名的词典序排序,如

(5,Г)< (5,Λ)< (6,Г)

为了满足条件C2,发起一轮Round的牧师选择Bqrm为全体成员的半数以上;

定义MaxVote(b,р,Rs)表示牧师р在小于Round b的最近Round的投票,进一步MaxVote(b,р,Rs)dec为对应的议案,MaxVote(b,р,Rs)rnd为对应的Round号;

MaxVote(b,Q,Rs)表示对集合Q的满足条件C3的投票。如果MaxVote(b,Q,Rs)dec为空,Round b可以表决任一议案。因此Paxos算法的前两步就是通过消息互换获取MaxVote(b,Q,Rs),以确定本轮Round要表决的议案。

1. 牧师р选择一个新的Round号b,发送NextRound(b)消息给牧师集;
2. 牧师q收到NextRound(b)后,以消息LastVote(b,V)作为响应,其中V表示牧师q在小于Round b的投票中最大编号Round的投票,如果他没有在小于b的Round投过票,则返回nullq表示(表示Round号为-∞,投票议案为BLANK);
牧师q需要在他的账簿后面记下他之前投过的票。当他发送LastVote(b, V)时,V等于MaxVote(b, q, Rs),集Rs也随着新Round的发起和投票不断改变。因为牧师р选择议案时将用V作为MaxVote(b, q, Rs)的值,因此为了保证条件C3为真,在q发送了LastVote(b, V)后MaxVote(b, q, Rs)不应该再改变,牧师q不能对Vrnd和b之间的Round投票。因此,可以认为LastVote(b, V)是牧师q做出的不进行这样投票的承诺。
3. 在收到多数派集Q中每个牧师返回的LastVote(b, V)消息后,牧师р发起新的Round b(quorum为Q,议案d),其中议案d由C3条件决定),牧师р在账目后面记下Round号,然后发送BeginRound(b,d)消息给Q中的每个牧师;
4. 收到BeginRound(b,d)后,牧师q决定是否对Round b投票(他可能因为发送了LastVote(b’,v’)对某个新Round b’进行了承诺而不能对旧的Round b投票)。如果q决定对Round b投票,他发送消息Voted(b, q)给р,并对这次投票进行记录;
5. 牧师р收到Q中每个牧师q回复的消息Voted(b, q)后,他在账簿中记下议案d作为通过议案,发送Success(d)消息给每个牧师;
6. 收到Success(d)消息后,牧师在自己的账簿中记下议案d。


民主向集权的过渡

任一个牧师在任何时候都可以发起自己的提案表决,只要满足C1-C3条件即可,这里可能产生的问题是:发起太多的Round可能会导致算法没法推进。

回顾Basic Paxos算法的流程,如果b是当前最大的Round号,牧师q在算法第2步收到NextRound(b)消息,他即做出了承诺:不会在第4步中对小于b之前的Round进行投票。因此,一个新发起的Round可能导致之前的Round失败。如果新Round连续发起,并不断增大Round号,则之前的Round因为不能获得quorum的全部投票而失败,导致算法永远也不能达成共识。

解决这个问题的方法就是将任意提案方式变为专人负责,在牧师中选择一个Leader,提案都由他发起,这样性能会得到一个飞跃。Leader的引入,不是为了解决一致性问题,而是为了解决性能问题。因此,即使Leader出错也不会妨碍算法的正确性,最多退回到Basic Paxos算法的基本流程,所以我们只需要保证大部分情况下只有一个Leader在工作就行了,而不用去保证绝对的不允许出现两个Leader或以上同时工作,这个通过一些简单的心跳以及租约就可以做到。

Multi-Paxos

如果要确定的是一系列议案,而不仅仅是一个议案又该怎么办呢?最简单的办法就是运行Paxos算法多次(每次称之为一个instance),每个instance确定一个议案。这样就可以在全部牧师之间达成议案共识的顺序。

这种方法可以进一步改进,因为提案现在都由Leader发起,可以控制Leader发起instance的时机,保证Acceptor做出的承诺不会因为新的instance而失效,从而使得Leader节点可以连续提交,获得去NextRound(b)的效果。将原来2-Phase过程简化为了1-Phase,从而加快了提交速度。


Paxos应用:状态机方法

下图表示Leader,Acceptor,Learner,State machine的协同工作关系。

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

状态机是通过由Paxos选出的有序Value序列驱动进行状态转移的,由于每台机器的实例instance编号都是连续有序增长的,而每个实例确定的值是一样的,可以保证各台机器的状态机输入是完全一致的。根据状态机的理论,只要初始状态一致,输入一致,那么产生的最终状态就是一致的。如下图这个例子是一个状态机结合Paxos实现了一个具有多机一致的KV系统。


实例0-3的值都已经被确定,通过这4个值最终产生(b, ‘jeremy’)这个状态,而各台机器实例序列都是一致的,所以大家的状态都一样,虽然产生状态的时间有先后,但确定的实例序列确定的值产生确定的状态。

日志删减和状态机Checkpoint

Paxos选出的有序Value序列构成了Paxos的日志PaxosLog,理论上,每次启动的时候都可以从0开始将所有的PaxosLog重新输入状态机(Replay),从而保证状态机数据的完整性。但这个做法的缺陷是:PaxosLog无限增长,每次都重新遍历,重启效率很低;由于每次都要遍历,那么这些数据无法删除,从而要求无限的磁盘空间。

Checkpoint的出现解决了这个问题。可以为状态机生成一个Checkpoint,代表某一时刻被固化下来的状态机数据,于是每次启动的Replay,只需要从这个Checkpoint表示的时刻而不是从0开始。


有了一份Checkpoint,即可完成PaxosLog的删除,Checkpoint之前的PaxosLog都是不再需要的。



猜你喜欢

转载自blog.csdn.net/feng12345zi/article/details/79653996