分布式一致性算法——Paxos原理分析

Paxos是一个分布式一致性算法。在一个由许多没有固定主从关系的节点组成的“分布式系统”中,它可以让所有节点通过不稳定的互相通信,针对某个决定(例如某个公用变量的取值,或者谁来暂时主导整个系统)达成一致的意见。

这里,我们强调节点间的通信是不稳定的,正是这种不稳定性造就了Paxos存在的必要性。两个节点的传输的信息可能会丢失或延迟,也可能会以任意顺序被重新排序。不仅如此,任意一个节点都随时有可能会突然失效,暂时停止响应其它节点的请求。就算在如此严苛的环境下,只要保证信息不被恶意修改(否则问题就无解了,参见“拜占庭将军问题”),运行Paxos算法的节点就能高效地完成它们的使命,可见其精妙之处。

在解释Paxos的原理之前,首先试想一下,假设节点间的通信是绝对稳定的,又该用怎样的策略让节点达成一致意见呢?做法有很多种,其中最简单的一种如下:每个节点对自己的提案生成一个随机编号,而所有节点都接受拥有最大编号的提案。显然,由于最大编号的提案只有一个,并且所有节点都一定能收到任何其他节点的提案,整个系统最终一定会达成共识。然而现实世界远非如此理想,假如拥有最大编号的提案没有被成功发送到所有节点,就会有节点被“蒙在鼓里”,对最终决策产生错误的认识。哪怕让这些节点和周围节点进行比对,也会产生一个哲学矛盾:错的究竟是我,还是这个世界?可见,在不可靠的通信环境下,分布式节点是无法依靠简单算法达成绝对一致的共识的。

接下来让我们稍微改进一下上面的算法。在节点讨论并做出决议之前,它们会被分成三种角色:

  • 提案者(Proposer):向接受者提出自己的意见。
  • 接受者(Accepter):对来自众多提案者的意见进行判断,从中选出一个自己同意的。
  • 学习者(Learner):被动接受最终做出的决定。

学习者的存在对整个决策过程没有任何影响,我们可以不考虑它们。最终的决定是由提案者和接受者共同做出的。值得注意的是,一个物理节点有可能同时兼任多种角色,这并不会影响算法的正确性。

我们让提案者将自己的提案发给一部分接受者——不用发给每一个,只要发给一半以上(假设所有提案者都知道目前接受者有几个,这并不难)即可。接受者接受提案的原则很简单:只接受最先收到的提案,并回复该提案的发送者,之后收到的提案一律不理。当一个提案者收到了一半以上的接受回复,它就有权向整个网络进行广播:“我的提案就是最终决定,其他人可以不用争了!”

我们称上面的算法为“多数同意算法”。为什么这个算法能够确保达成共识——换个说法,能够保证最多只有一个节点有权进行广播呢?用反证法很容易证明这一点。假设有两个提案者都收到了一半以上的接受回复,那么根据抽屉原理,回复这两个节点的接受者里面,必定会至少有一个节点同时回复了这两个提案者。这就产生了明显的矛盾——说好的只回复一个提案者呢?到此,我们已经保证了这个算法的正确性,任务似乎已经完成了。

然而,有心人很快就会发现,上面的算法存在着致命的性能问题。如果提案者的数量只有两三个还好,但一旦超过这个数量(为了保证提案者集群的高可用性,也就是至少有一个提案者在线,这是很常见的情况),就很可能出现如下的尴尬情况:不同接受者接受了不同提案,其中没有一个提案的接受数超过一半,因此根本无法达成共识,只能再来一轮讨论。由于提案到达接受者的顺序是完全随机的,这种情况的概率甚至要远大于达成共识的概率。事实上,多数同意算法已经具有了Paxos的雏形,下面我们就要探明真正的Paxos算法是如何在它的基础上大幅提高达成共识的概率的。

在Paxos算法中,提案者依旧会给自己的提案附上一个随机生成的编号,只不过这一次它们不会直接向半数以上接受者发送自己的提案,而是会事先发送一个只包含编号的请求来“试探”一下接受者的意图,这个请求称为“准备请求”。接受者在接收到准备请求后,会基于如下有序策略回复发送者:

  1. 如果自己收到过编号更大的准备请求,直接拒绝发送者,因为要优先接受编号更大的提案。
  2. 如果自己收到过编号更小的提案,并且已经做出了接受回复,就会告诉发送者上一次接受的提案编号和包含的意见,并且说:“不好意思,虽然你的编号更大,但我也很难(并不是不能)反悔。尽量把你的提案内容改成我接受过的这个,这样我就能在不反悔的前提下接受你的提案了。”
  3. 否则,告诉发送者:“我已经准备好接受你的提案了,把它发过来吧!”

如果提案者收到了拒绝回复,证明此时系统中有编号更大的提案在等待接受,因此它会“礼貌”地放弃发送提案,等待最终共识或下一轮讨论。否则,提案者在接收到半数以上种类2和3的回复时,就会尝试向半数以上接受者(不一定是刚才回复它的那一批)真正发送自己的提案。如果它收到了种类2的回复,会从中挑一个编号最大的,把自己的提案内容改成对应提案的内容;如果所有回复都是种类3,则保持内容不变。接受者收到提案后,如果这个提案是目前它收到过的所有准备请求和提案中编号最大的,就会做出接受回复,否则不予理会。接下来的过程和多数同意算法完全相同——如果某个提案者收到半数以上接受回复,表明自己的提案成为了最终共识。如果没有任何一个提案成为共识,就要再来一轮讨论。

总结一下,如果从系统中某对特定的提案者/接受者的视角来看,Paxos的流程分为五个阶段:

  1. 提案者向半数以上接受者发送准备请求,附上自己的提案编号
  2. 接受者对编号最大的准备请求做出回复,附上之前接受过的提案内容
  3. 如果提案者收到半数以上回复,则根据最大编号回复修改提案内容后,向半数以上接受者发送自己的提案
  4. 接受者接受编号最大的提案,并回复该提案的发送者(注意和之前的“准备请求回复”区分开)
  5. 如果提案者收到半数以上接受回复,表明自己的提案成为了最终共识

首先依旧要证明Paxos算法的正确性。证明思路有点像多数同意算法,但这里放松了一个约束条件——一个接受者现在有可能接受一连串编号严格递增的提案了。这里要再次强调,如果这一连串提案的内容完全相同,那么对于任意接受者而言,接受后面的提案并不会对之前接受的提案构成“反悔”,对最终共识的内容也不会造成任何影响(可能会产生多个被选为共识的提案者,但共识的内容一定是相同的)。所谓的“反悔”只有一种情况,那就是某个接受者先对提案a做出接受回复,之后又对某个内容不同的提案b做出接受回复。因此,在某个提案者根据种类2回复中的最大编号修改自己的提案内容后,对于返回最大编号的接受者而言,它提出的新提案是”安全“的:这些接受者可以接受新提案而无需进行反悔。那么返回更小编号的接受者呢?显然,根据Paxos的规则,它们不得不进行一次反悔,转而接受这个内容不同的新提案,但我们可以断言:如果某个提案Pa遭到了至少一个接受者的反悔,那么它一定不可能成为最终共识!之所以如此肯定,是因为“成为最终共识”意味着Pa在遭到反悔之前的某个时刻,就已经获得了半数以上节点的接受,我们称该事件为A;此外,在反悔发生之前,必定至少存在一个提案Pb,它的编号比Pa更大,内容与Pa不同,且至少得到了一个接受者的接受,我们称最早发生的该事件为B。显然,只有在发生过事件B的前提下,编号比Pa和Pb都大的新提案才有可能被修改为Pb,进而导致某个接受者对Pa做出反悔。接下来,我们讨论A和B的先后关系:

  • 如果A先发生,那么根据抽屉原理,Pb的提案者在收到半数以上的准备请求回应之后,回应者之中至少会包含一个接受了Pa的接受者。根据Paxos的规则,Pb的内容将会被修改为Pa,也就是说B不可能发生。
  • 如果B先发生,意味着不管Pb是否已被任意接受者接受,Pb的准备请求都已经到达了半数以上的接受者并得到了回应,否则Pb的提案者不会进入提案发送阶段。此时由于Pa的编号小于Pb,这些接受者根本不可能接受Pa,也就是说A不可能发生。

既然A和B不可能同时发生,我们的断言自然得到了证明。在费尽心思堵上“反悔”带来的漏洞之后,剩下的证明就和多数同意算法完全一致了(只是结论变成了“不可能产生多个内容不同的最终共识”),这里不再赘述。

第二个问题是,Paxos在什么情况下可以达成共识呢?由于编号较大的提案可以随意覆盖编号较小的提案,我们可以很容易地做出猜测:只要某个编号足够大的提案能成功到达半数以上接受者即可。显然这只是一个充分非必要条件,但已经比多数同意算法的要求宽泛的多了。事实上,只要每个提案者都只提出一个提案,并且所有提案都能到达半数以上的接受者,最终一定会有至少一个提案者被选为最终共识,并且它们的提案内容完全相同。

从上面的结论中,我们可以发现另一种可能性:某个被拒绝的提案者过于“心急”,没有等到下一轮讨论开始就提出了新提案。如果这样的提案者多于两个,就有可能相互覆盖对方的提案,导致整个系统永远也无法得出共识。这种情况被称为“活锁”,意思是所有人都在不停做无用功,和死锁的完全僵死相对。为了减小这一问题的发生概率,可以让节点之间互相通知讨论的进行状态,也可以让提案者在经过随机长度的休眠后再提出新提案,但没有一种方法能彻底消除活锁的可能。正因Paxos的这种固有的不稳定性,在实际的分布式系统中,并不会对于每个决定都使用Paxos来讨论,而是只用Paxos来选定一个主节点,由它来进行暂时的“独裁统治”,直到它下线为止。

posted @ 2021-06-13 23:59  limejuiceOwO  阅读(360)  评论(0)    收藏  举报