这个月公司风波很多,比较忙,心态也有些变化,一个月没更了,终归还是要沉淀下来工作学习呀,继续学习Paxos。前面已经介绍了2PC和3PC,并了解了它们各自的特点以及解决的分布式问题,接着,我们来介绍Paxos:一种基于消息传递且具有高度容错性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法。
在常见的分布式系统中,设计者必须要考虑的一个问题就是节点宕机或网络异常,这意味着系统将在某个时间点损失某些可用节点,Paxos要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证无论发生以上任何异常,都不会破坏系统的一致性。
拜占庭将军问题
拜占庭帝国有许多军队,不同军队的将军之间必须制定一个统一的行动计划,从而做出进攻或者撤退的决定,同时,各个将军在地理上都是被分隔开来的,只能依靠军队的通讯员来进行通讯,然而通讯员中可能存在叛徒,这些叛徒可以任意篡改消息,从而达到欺骗将军的目的。
这就是著名的“拜占庭将军问题”。从理论上来说,在分布式计算领域,试图在异步系统和不可靠通道来达到一致性状态是不可能的,而在实际工程实践中,可以假设不存在拜占庭问题,Lamport在1990年根据一个叫Paxos的小岛的议会方式得到启发,并提出了Paxos算法(这篇论文在1990年投稿时居然还被拒了-_-!,六年后才被重新翻出来)。
Paxos作为一种提高分布式系统容错性的一致性算法,一直以来总是被很多人抱怨其算法理论太难以理解,于是便下载了Lamport另一篇论文Paxos Made Simple,估计Lamport也是了解到了像我们这种资质平平的,并不能读懂他的神作(通篇故事形式描述Paxos)The Part-Time Parliament,接着,我们将从对一致性问题的描述来讲解该算法需要解决的实际需求。
问题描述
假设有一组可以提出的进程集合,那么对于一个一致性算法来说需要保证以下几点:
在Paxos中,有三种参与角色,Proposer、Acceptor和Learner,在具体的实现中,一个进程可能充当不止一种角色,在这里我们并不关心进程如何映射到各种角色,假设不同参与者之间可以通过收发消息来进行通信,那么:
提案选定
首先应该考虑的是如何选定唯一的提案,最简单的方式是只允许唯一的Acceptor,但是一旦这个节点挂了,系统也就奔溃了(单点问题);因此我们应该使用多个Acceptor来避免单点问题,这里就引出了投票方案:Proposer可以向一个Acceptor集合发送提案,同样的,集合中的每个Acceptor都可以批准该提案,当有足够多的Acceptor批准该提案时,我们就可以认为该提案被选定了,这里的足够多定义为Acceptor总数的一半以上,同时规定每个Acceptor最多只能批准一个提案,这样就能保证只有一个提案被选定了。
推导过程
在没有消息失败和消息丢失的情况下,如果我们希望即使在只有一个提案被提出的情况下,仍然可以选出一个提案,因此可以得到如下规则:
P1:一个Acceptor必须批准它收到的第一个提案(不然在仅一个提案时,每个Acceptor都拒绝接受,则没有提案会被选取);
由P1会引出一个新的问题,假设多个Proposer各自提出不同的提案,各个Acceptor也都各自接受,但却没有票数优势的提案,如下图所示:
在这种场景下,是无法选定一个提案的,并且,即使只有两个提案,各自被差不多一半的Acceptor批准,而一个Acceptor又在此时出了错,同样会导致无法确定应该选定哪个提案,比如Acceptor A和B接受提案1,而C,D,E接受提案2,而E节点又宕机,这个时候无法确定应该选择哪个提案。这个时候就需要一个Acceptor能够批准不止一个提案,这里的提案=(编号,value),当具有某个value值的提案被半数以上的Acceptor批准后,也就可以认定该提案被选定了。即,虽然允许多个提案被选定,但是这些提案必须具有相同的value值,归纳为:
P2:如果编号为M0、V0的提案被选定了,那么所有比编号M0更高的,且被选定的提案,其Value=V0;
这里P2的证明非常冗长复杂(数学归纳法),其实总结一下,就是每次选取的提案一定要是编号最大的,而且在当前的Acceptor集合中,不存在批准过提案版本(编号)小于当前版本的Acceptor,这就生成了如下的提案生成算法。
Proposer生成提案
在确定了提案后,Proposer会将该提案再次发送给Acceptor集合,并期望得到批准,这个请求的过程即为Accept请求,但此时接受Accept请求的Acceptor集合不一定是之前响应的Acceptor集合了,但是由于Acceptor集合是半数以上,则至少会有一个共同的Acceptor。
Acceptor批准提案
一个Acceptor可能会收到来自Proposer的两种请求,分别是Prepare请求和Accept请求,对这两类请求做出响应的条件分别如下:
翻译一下就是,只要这个Acceptor尚未响应过任何编号大于Mn的Prepare请求,那么它就可以接收这个编号为Mn的提案,如果收到的请求编号小于当前响应的请求的最大编号,则可以忽略而不破坏算法安全性。
接着就可以对提案选定的过程进行一个概述了(类似2PC):
阶段一:
阶段二:
在实际运行过程中,一个Proposer可能会产生多个提案,但只要Proposer遵循如上规则,就一定能保证算法执行的正确性,通过接收最大编号的提案,可以保证其在提案选定上的正确性;
提案的获取
提案产生后便是选定的过程,Learner获取提案有以下三种方案:
主Proposer的作用
试想这样一种情况,P1提出了M1,完成了阶段一,同时,P2提出了M2也完成了阶段一,这个时候Acceptor集合会丢弃M1,因为M2的编号大于M1,于是P1又提出了M3,这样Acceptor又会丢弃M2,这样循环下去会导致Proposer提出了一系列的提案,但是都无法被选定,陷入死循环。
为了保证Paxos的可持续性,必须选择一个主Proposer,并规定只有主Proposer才能提出提案,这样一来,只要主Proposer与过半的Acceptor通信正常,那么但凡主Proposer提出一个编号更高的提案,就会被批准;同时,如果发现当前算法流程中有一个编号更大的提案正在接收批准,那么它会丢弃当前的提案,通过主Proposer可以保持Paxos算法的活性。
Paxos在2PC和3PC的基础上引入了“过半”的概念,即少数服从多数,,同时支持分布式节点角色的轮换,这一点是至关重要的,极大的避免了分布式单点的出现,在二阶段的基础上,解决了无限期等待和数据不一致(脑裂)的情况,是目前最优秀的分布式一致性协议之一。