分布式架构之 Paxos 协议
这周一下了个决定"裸辞",逼自己一把。当你在一个复杂的环境下,对所负责的项目失去激情时、不开心时你会选择怎样?欢迎进行留言一起讨论。已经进入到分布式架构系列的尾声了,倒数第三篇文章。Paxos、Raft、以及变种/类似的协议都是用于在分布式里面解决选举的问题。2pc、3pc、Waro是保证数据的强一致性,它们之间的强一致性在层次上是不同的概念、解决的问题不同,需要注意区分。
这里在讲一下怎么区分中心化和去中心副本控制协议,中心化中每个节点不是对等的,是由元数据信息决定谁是primary,可能很多节点都没有参与过选举、更别说当primary。去中心化是每个节点都是公平公正对等的,选举时可以全民参与,都有机会当primary。当去中心化选举出的primary确定后,它其实就是从去中心化状态转为类似中心化的状态。
简介
Paxos协议是少数在工程实践中证实的强一致性、高可用的去中心化分布式协议。
Paxos协议的流程较为复杂,但其基本思想却不难理解,类似于人类社会的投票过程。Paxos协议中,有一组完全对等的参与节点(成为accpetor),这组节点各自就某一事件作出决议,如果某个决议获得了超过半数节点的同意则生效。Paxos协议中只要有超过一半的节点正常,就可以工作,能很好对抗宕机、网络分化等异常情况。
介绍Paxos协议的资料很多,Lamport的论文也写得简明有趣。与大多数材料不同的是,本文不首先介绍协议的推理和证明过程,而是从工程上的算法流程描述起,感性的介绍协议过程。进而用一些复杂的例子演示协议的过程。最后,本文在接收协议是如何推导设计出来的。
协议描述
节点角色
Paxos协议中,有三类节点:
Proposer:提案者。Proposer可以有多个,Proposer提出议案(value)。所谓value,在工程中可以是任何操作,例如“修改某个变量的值为某个值”、“设置当前primary为某个节点”等等。Paxos协议中统一将这些操作抽象为value。不同的Proposer可以提出不同的甚至矛盾的value,例如某个Proposer提议”将变量X设置为1“,另一个Proposer提议”将变量X设置为2“,但对同一轮Paxos过程,最多只有一个value被批准。
Acceptor:批准者。Acceptor有N个,Proposer提出的value必须获得超过半数(N/2+1)的Acceptor批准后才能通过。Acceptor之间完全对等独立。
Learner:学习者。Learner学习被批准的value。所谓学习就是通过读取各个Proposer对value的选择结果,如果某个value被超过半数Proposer通过,则Learner学习到这个value。类似Quorum机制,某个value需要获得W=N/2+1的Acceptor批准,从而学习者至少读取N/2+1的Acceptor,至多读取N个Acceptor的结果后,能学习到一个通过的value。
上述三类角色只是逻辑上的划分,实践中一个节点可以同时充当这三类角色。
流程描述
Paxos协议一轮一轮的进行,每轮都有一个编号。每轮Paxos协议都可能会批准一个value,也可能无法批准一个value。如果某一轮Paxos协议批准了某个value,则以后各轮Paxos只能批准这个value。上述各轮协议流程组成了一个Paxos协议实例,即一次Paxos协议实例只能批准一个value,这也是Paxos协议强一致性的重要体现。
每轮Paxos协议分为两个阶段,准备阶段和批准阶段,在这两个阶段Proposer和Acceptor有各自的处理流程。
Proposer-准备阶段的流程:
1、向所有的Acceptor发送消息”Prepare(b)“;这里b是paxos的轮数,每轮递增;
2、如果收到任何一个Acceptor发送消息”Reject(B)“,则对于这个Proposer而言本轮Paxos失败,将轮数b设置为B+1后重新步骤1;
Proposer-批准阶段的流程:
(根据收到Acceptor的消息做出不同选择)
3、如果接收到Acceptor的”Promise(b,v_i)“消息达到=N/2+1(N为Acceptor总数,除法取整)v_i表示Acceptor最近一次在i轮批准过的value v。
3.1、如果收到的”Promise(b,v)“消息中,v都为空,Proposer选择一个value v,向所有Acceptor广播Accept(b,v)
3.2、否则,在所有收到的”Promise(b,v_i)“消息中,选择i最大的value v,向所有Acceptor广播消息Accept(b,v)
4、如果收到Nack(B),将轮数b设置为B+1后重新步骤1;
Acceptor-准备阶段的流程:
1、接受某个Proposer的消息Prepare(b)。
参数B是该Acceptor收到的最大Paxos轮数编号;V是Acceptor批准的value,可以为空
1.1、如果b>B,回复Promise(b,v_B),设置B=b;表示保证不再接受编号小于b的提案。
1.2、否则,回复Reject(B)
Acceptor-批准阶段的流程:
2、接收Accept(b,v)
2.1、如果b<B,回复Nack(B),暗示Proposer有一个更大编号的提案被这个Acceptor接收了
2.2、否则设置V=v。表示这个Acceptor批准的value是v。广播Accepted消息。
实例
理解Paxos协议的最直观的方法就是考察几个协议运行的实例。本文给出几个典型的场景下协议运行的例子。
基本例子
基本例子里有5个Acceptor,1个Proposer,不存在任何网络、宕机异常。我们着重考察各个Acceptor上变量B和变量V的变化,以及Proposer上的变量b的变化。
1.初始状态
2.Proposer向所有Acceptor发送”Prepare(1)“,所有Acceptor正确处理,并回复Promise(1,NULL)
3.Proposer收到5个Promise(1,NULL),满足多余半数的Promise的value为空,此时发送Accept(1,v1),其中v1是Proposer选择的value。
4.此时,v1被超过半数的Acceptor批准,v1即是本次Paxos协议实例批准的vaue,如果Leader学习value,学到的只能是v1
批准的value无法改变
在同一个Paxos实例中,批准的value是无法改变的,即使后续Proposer以更高的序号发起Paxos协议也无法改变value。
1.例如,某次Paxos协议运行后,Acceptor的状态是:
5个Acceptor中,有3个已经在第三轮Paxos协议批准了v1作为value。其它两个Acceptor的V为空,这可能是因为Proposer与这两个Acceptor的网络中断或者这两个Acceptor宕机造成的。
2.此时,即使有新的Proposer发起协议,也无法改变结果。假设Proposer发送”Prepare(4)消息“,由于4大于所有的Acceptor的B的值,所有收到Prepare消息的Acceptor回复promise消息。但是前3个Acceptor只能回复promise(4, v1_3),后两个回复promise(4,NULL)。
3.此时,Proposer可能收到若干个Acceptor发送的Promise消息,没有收到的Promise消息可能是网络异常造成的。无论如何,Proposer要收到至少3个Acceptor的Promise消息后才满足协议中大于半数的约束,才能发送Accept消息。这3个Promise消息中,至少有1个消息是Promise(4,v1_3),至多3个消息都是Promise(4,v1_3)。另一方面,Proposer始终不可能收到3个Promise(4,NULL)消息,最多收到2个。综上,按协议流程,Proposer发送的accept消息只能是”accept(4,v1)“而不能自由选择value。
无论这个accept消息是否被各个Acceptor接收到,都无法改变v1是被批准的value这一个事实,即从全局看,有且只有v1是满足超过多数Acceptor批准的value。例如,假设accept(4,v1)消息被Acceptor1、Acceptor2、Acceptor4收到,那么状态变为:
从这个例子看一旦某一个value被批准,此后永远只能批准这个value。
一种不可能出现的状态
Paxos协议的核心就在与”批准的value无法改变“,这也是整个协议正确性的基础,为了更好的理解后续对Paxos协议的证明。这里再看一种看似可能,实际违反协议的状态,这种状态也是后续反正明协议时的一种错误状态。
上诉状态中,3个轮次为1的Acceptor的value为v1,2个轮次更高的acceptor的value为v2。此时被批准的value是v1。
假设此时发生新的一轮b=3的Paxos过程,Proposer有可能收到Acceptor3、4、5发出的3个Promise消息分别为”promise(1,v1_1)“,”promise(2,v2_2)“,“promise(2,v2_2)”。按照协议,Proposer选择value变化最大的promise消息,即v2_2的promise消息,发送消息“accept(3,v2)”,从而使得最终批准的value从v1变成了v2。
上面假设看似正常,其实不可能发生。是因为本机中给出的初始状态就是不可能出现的。是因为,要达成上述状态,发起prepare(2)消息的proposer一定成功的向Acceptor 4、Acceptor 5发送了accept(2,v2)。但发送accept(2,v2)的前提只能是proposer收到了3个“promise(2,NULL)”消息。然而,从状态我们知道,在b=1的那轮Paxos协议里面,已经有3个Acceptor批准了v1,这3个Acceptor在b=2时发出的消息只能是promise(2,v1_1),从而造成proposer不可能收到3个“promise(2,NULL)”,至多只能收到2个“promise(2,NULL)”。另外,只要proposer收到一个”promise(2,v1_1)“,其发送的accept消息只能是accept(2,v1)。
从这个例子我们看到Prepare流程中第3步是协议中最关键的一步,它的存在严格约束了“批准的value无法改变”这一事实。在后续协议推导中我们将看到这一步是如何被设计出来的。
节点异常
这里给一个较为复杂的异常状态下Paxos运行实例。本例子中有5个Acceptor和2个Proposer。
1.Proposer1发起第一轮Paxos协议,然而由于异常,只有2个Acceptor收到了prepare(1)消息。
2.Proposer1只收到了2个promise消息,无法发起accept消息;此时,Proposer2发起第二轮Paxos协议,由于异常,只有Acceptor 1、3、4处理了prepare消息,并发送promise(2,NULL)消息
3.Proposer2收到了Acceptor 1、3、4的promise(2,NULL),满足协议超过半数的要求,选择了value为v1,广播accept(2,v1)的消息。由于异常,只有Acceptor 3、4处理了这个消息。
4.Proposer1以b=3发起新一轮的Paxos协议,由于异常,只有Acceptor1、2、3、5处理了prepare(3)消息。
5.由于异常,Proposer1只收到Acceptor1、2、5的promise(3,NULL)的消息,符合协议要求,Proposer1选择value为v2,广播accept(3,v2)的消息。由于异常,这个消息只被Acceptor1、2处理。
到目前为止,没有任何value被超过半数的Acceptor批准,所以Paxos协议尚没有批准任何value。然而由于没有3个NULL的Acceptor,此时能被批准的value只能是v1或者v2其中之一。
6.此时proposer1以b=4发起新的一轮Paxos协议,所有的Acceptor都处理了prepare(4)消息。
7.由于异常,Proposer1只收到了Acceptor3的promise(4,v1_3)消息、Acceptor4的promise(4,v1_2)、Acceptor5的promise(4,NULL)消息,按协议要求,只能广播accept(4,v1)消息。假设Acceptor2、3、4收到了accept(4,v1)消息。由于批准v1的Acceptor超过半数,最终批准的value为v1。
竞争及活锁
从前面的例子不难看出,Paxos协议的过程类似于“占坑”,哪个value把超过半数的“坑”(Acceptor)占住了,哪个value就能得到批准了。
这个过程也类似于单机系统并行系统的加锁过程。假如有这么个单机系统:系统内有5个锁,有多个线程执行,每个线程需要获得5个锁中任意3个才能执行后续操作,操作完成后释放占用的锁。我们知道,上述单机系统中一定会发生“死锁”。例如。3个线程并发,第一个线程获得2个锁,第二个线程获得2个锁,第三个线程获得1个锁。此时任何一个线程都无法获得3个锁,也不会主动释放自己占用的锁,从而造成系统死锁。
但是Paxos协议过程中,虽然也存在着并发竞争,不会出现上述死锁。这是因为,Paxos协议引入了轮数的概念,高轮数的Paxos提案可以抢占低轮数的Paxos提案。从而避免了死锁的发生。然后这种设计却引入了“活锁”的可能,即Proposer相互不断以更高的轮数提出议案,使得每轮Paxos过程都无法最终完成,从而无法批准任何一个value。
1.Proposer1以b=1提出议案,发送prepare(1)消息,各Acceptor都正确处理,回应promise(1,NULL)
2.Proposer2以b=2提出议案,发送prepare(2)消息,各Acceptor都正确处理,回应promise(2,NULL)
3.Proposer1收到5个promise(1,NULL)消息,选择value为v1发送accept(1,v1)消息,然而这个消息被所有的Acceptor拒绝,收到5个Nack(2)消息。
4.Proposer1以b=3提出议案,发送prepare(3)消息,各Acceptor都正确处理,回应promise(3,NULL)
5.Proposer2收到5个个promise(2,NULL)消息,选择value为v2发送accept(2,v2)消息,然而这个消息被所有的Acceptor拒绝,收到5个Nack(3)消息。
上述过程交替进行,则永远无法批准一个value,从而形成Paxos协议活锁。Paxos协议活锁问题也是这个协议的主要问题。
协议推导
Paxos协议是被人为设计出来,其设计过程也是协议的推导过程。Paxos协议利用了Quorom机制(多数派),选择的W=R=N/2+1。简单而言,协议就是Proposer更新Acceptor的过程,一旦某个Acceptor成功更新了超过半数的Acceptor,则更新成功。Learner按Quorom去读取Acceptor,一旦某个value在超过半数的Proposer上被成功读取,则说明这是一个被批准的value。协议通过引入轮次,使得高轮次的提议抢占低轮次的提议来避免死锁。
协议设计关键点是如何满足“在一次Paxos算法实例过程中只批准一个value”这一约束条件。称这个约束条件为“约束条件1”。由于直接在工程中实现“约束条件1”很困难,所以协议的设计过程就是不断推导出“约束条件1”的必要不充分条件,直到某个必要不充分条件在工程上易于实现。从而满足这个条件也就能满足“约束条P1”。
一、提出“约束条件2:一旦一个value获得超过半数的Acceptor批准,之后Paxos协议只能批准这个value”,易证明“约束条件2”=>“约束条件1”。
二、加强该约束“约束条件2”,寻找其必要不充分条件,提出“约束条件3:一旦一个value获得超过半数Acceptor批准,之后任何Acceptor只能批准这个value”。更容易证明“约束条件3”=>“约束条件2”
三、既然“约束条件3”中要使得“之后任何Acceptor只能批准这个value”那么等价于“之后Proposer发送的accept消息也只能是这个value”。所以“约束条件3”等价于“约束条件4:一旦一个value v获得超过半数的Acceptor批准,之后Proposer提议的value只能是v”。
四、加强“约束条件4”。得到Paxos协议中的Proposer流程的“步骤3”,即“约束条件5:Proposer提议一个value v前,要么之前没有任何一个value被批准,要么存在一个大小为N/2+1的Acceptor集合,这个集合内的各个Acceptor批准过的轮数最大的value是v”。
“约束条件5“的前半部分”提议value前,没有任何一个value被批准“所以选择任意value提案一定不违背”约束条4“。确定”之前没有任何一个value被批准“的方法就是读取Acceptor,如果有超过半数的Acceptor批准的value为空,那么肯定没有一个value被批准过。这也就是Proposer流程步骤3.1。
”约束条件5“的后半部分对应Proposer流程步骤3.2,即读取了N/2+1的Acceptor的状态,这些Acceptor批准了某些value,由于没有读取所有Aacceptor。故可能无法确定是否一定有value已经被批准了。例如,5个Acceptor时读取3个Acceptor状态情况是(B=1,V=v1)(B=2,V=v2)(B=3,V=NULL),v2可能是一个已经被批准的value,如果另外两个Acceptor上的value为NULL,那么还没有批准任何value。”约束条件5“的做法是在这种情况下选择一个轮数号较大的value即v2,从而可以保证:要么此时Paxos协议还没有批准任何一个value,要么之前Paxos协议批准的也只能是v2。
下面证明”约束条件5“的后半部分=>”约束条件4“:
1.如果之前Paxos尚没有批准任何vallue,那么选择轮次编号最大的value提案显然是”安全的“不违反”约束条件4“
2.假设之前Paxos已经批准过一个value记为v0。下面证明在任意第n轮paxos过程中,v0一定是此时任意一个N/2+1的acceptor集合中轮数最大的value。
令v0是在第m轮被批准的,则在m轮时至少有N/2+1个Acceptor的状态是(B=m,V=v0)
在第m+1轮,由于Quorom限制,任意一个N/2+1个Acceptor组成的集合中,至少有1个Acceptor的状态是(B=m+1,V=v0),即Proposer至少收到一个promise(m+1,v0_m)消息,由于此刻m必然是所有value中最大的编号,所以Proposer按”约束条件5“发出只能提案value只能是v0。
在第m+1轮、m+2轮….n-1轮,按递推规则,这些轮提案的value也只能是v0。
进一步反正法证明第n轮:假设在第n路面中,提案的value不是v0而是vx。根据”约束条件5“,因为vx不是v0,那么vx只能是一个没有被超过半数Acceptor批准过的value,说明在n-1轮存在一个N/2+1的集合,该集合内所有value都没有批准过v0。这个与从m轮到n-1轮提案的value是v0相互矛盾。至此已经归纳证明了”约束条件5“的后半部分。
工程投影
Chubby中的Paxoschubby中的paxos
Chubby是最早基于Paxos的分布式系统之一。Chubby的设计人员没有直接提供一种Paxos的开发,而是利用Paxos实现了一个高可用的分布式系统,再利用这个分布式系统对外提供高可用存储、分布式锁等服务,从而间接的提供了Paxos功能。
Chubby中的节点完全是对等的,通过Paxos协议,这些节点选举出一个Master节点(Primary),公开的资料中没有解释Chubby使用Paxos的细节,例如如何选择Paxos的轮次号,如何避免Paxos活锁等。当选举出的Primary节点后,所有读写操作都由Primary节点控制,Chubby系统从一个完全对等的去中心化状态变为了一个Primary-Secondary的中心化状态。当Primary异常时,Chubby节点将重新利用Paxos协议发起新一轮的选举以确定新的Primary节点。新primary节点与原primary节点具有完全一样的持久化信息,新primary将代替原primary节点对外提供读写服务。
基于Chubby的服务,其他的分布式系统可以很容易的实现选择primary、保存最核心元数据等功能。利用Chubby可以大大简化分布式系统的设计:可以认为整个Chubby集群逻辑上是一个magic的高可用(几乎不会停服务)的中心节点,其他分布式系统可以基于这个大中心节点可以实现中心化的副本控制协议。由于Chubby集群本身是由多个节点组成的分布式系统,基于Chubby的分布式系统无需直接实现Paxos协议,就可以利用Paxos协议实现全局完全无单点。
Zookeeper中的Paxos
Zookeeper使用了一种修改后的Paxos协议。
首先,Zookeeper的协议运行依赖TCP协议实现FIFO,Zookeeper通过TCP协议获得两点保障:
1、数据总是严格按照FIFO(first in first out)规则从一个节点传递到另一个节点的;
2、当某个TCP链接关闭后,这个链接上不再有数据传递。由于TCP协议为传输的每一个字节设置了序列号(sequence number)及确认(acknowledgment),上述两点在TCP协议上是完全可以保证的。需要注意的是Zookeeper并不要求TCP协议可以可靠的将数据传输到对端节点,正如上文分析过的,基于TCP协议实现真正意义上的可靠传输也是做不到的。Zookeeper基于TCP的上述两点保障,可以较大的简化问题模型,忽略诸如网络消息乱序、网络消息重复等的异常,从而较大的简化协议设计。
再者,在Zookeeper中,始终分为两种场景:
一、Leader activation,在这个场景里,系统中缺乏Leader(primary),通过一个类似Paxos协议的过程完成Leader选举。
二、Active messaging,在这个场景里,Leader接收客户端发送的更新操作,以一种类似两阶段提交的过程在各个follower(secondary)节点上进行更新操作。在Leader activation场景中完成leader选举及数据同步后,系统转入Active messaging场景,在active messaging中leader异常后,系统转入Leader activation场景。
无论在哪种场景,Zookeeper依赖于一个全局版本号:zxid。zxid由(epoch,count)两部分组成,高位的epoch部分是选举编号,每次提议进行新的leader选举时epoch都会增加,低位的count部分是leader为每个更新操作决定的序号。可以认为,一个leader对应唯一的epoch,每个leader任期内产生的更新操作对应一个唯一的有序的count,从而从全局的视野,一个zxid代表了一个更新操作的全局序号(版本号)。
每个zookeeper节点都有各自最后commit的zxid,表示这个zookeeper节点上最近成功执行的更新操作,也代表了这个节点的数据版本。在Leader activation阶段,每个zookeeper节点都以自己的zxid作为Paxos中的b参数发起Paxos实例,设置自己作为leader(值为value)。每个zookeeper节点即是proposer又是acceptor,所以,每个zookeeper节点只会accept提案编号b大于自身zxid的提案。不难理解,通过paxos协议过程,某个超过quorum半数的节点中持有最大的zxid的节点会成为新的leader。值得注意的是,假设参与选举的每个zookeeper节点的zxid都一样,即所有的节点都以相同的b=zxid发起提案,那么就有可能发生上面无法选出leader的情况。zookeeper解决这个问题的办法很简单,zookeeper要求为每个节点配置一个不同的节点编号,记为nodeid,paxos过程中以b=(zxid,nodeid)发起提议,从而当zxid相同时会优先选择节点编号较大的节点成为leader。成为新leader的节点首先与follower完成数据同步后,再次说明,数据同步过程可能会涉及删除follower上的最后一条脏数据,参考之前的文章quorum机制。当与至少半数节点完成数据同步后,leader更新epoch,在各个follower上以(epoch+1,0)为zxid写一条没有数据的更新操作。这个更新操作称为NEW_LEADER消息,是为在各个节点上更新leader信息,当收到超过半数的follower对NEW_LEADER的确认后,leader发起NEW_LEADER的COMMIT操作,并进入active messaging状态提供服务。
进入active messaging状态的leader会接收客户端发来的更新操作,为每个更新生成递增的count,组成递增的zxid。Leader将更新操作以zxid的顺序发送给各个follower(包含leader本身,一个leader同时也是follower),当收到超过半数的follower的确认后,leader发送针对该更新操作的COMMIT消息给各个follower。这个更新的过程很类似两阶段提交,只是leader永远不会对更新操作做abort操作。
如果leader不能更新超过半数的follower,也说明leader失去了quorum,此时可以发起新的leader选举,最后一条更新操作处于”中间状态“,其是否生效取决于选举出新的leader是否有该条更新操作。从另一个角度看,当leader失去quorum的follower,也说明可能有一个超过半数的节点集合正在选举新的leader。
Zookeeper通过zxid将两个场景阶段较好的结合起来,且能保证全局的强一致性。由于同一时刻只有一个zookeeper节点能获得超过半数的follower,所以同一个时刻最多只能存在唯一的leader;每个leader利用FIFO以zxid顺序更新各个follower,只有成功完成前一个更新操作的才会进行下一个更新操作,在同一个leader任期内,数据在全局满足quorum约束的强一致性,即读超过半数的节点一定可以读到最新已提交的数据;每个成功的更新操作都至少被超过半数的节点确认,使得新选举的leader一定可以包含最新的已成功提交的数据。
Megastore中的Paxos
Megastore中的副本数据更新基于一个改良的Paxos协议进行。与Chubby和Zookeeper仅仅利用Paxos选出primary不同的是,Megastore的每次数据更新都是基于一个Paxos协议的实例。从而使得Megastore具有一个去中心化的副本控制机制。另一方面,为了获得较大的数据更新性能,Megastore又引入了类似Primary的leader角色以在绝大部分的正常流程时优化原有paxos协议。
基本的Paxos协议两个特点使得其性能不会太高:
1.每个Paxos运行实例,至少需要经历三轮网络交互:Proposer发送prepare消息、Acceptor发送promise消息、Proposer再发送accept消息;
2.读取Paxos上的数据时,需要读取超过半数的Acceptor上的结果才能获得数据。对于一个高吞吐、高并发的在线存储系统,上述特性会制约系统的性能。为此,Megastore使用了一些方式对Paxos协议进行了改良。Megastore中的副本一般都是跨机房、跨地域部署,在通常状态下,某个用户只会访问特定机房中的副本。为此,Megastore在每个机房为每个副本部署一个特殊的称为协调器(Coordinator)的服务。Coordinator服务相对Megastore的底层big table系统而言显得非常简单,其主要功能就是维护副本之间一致性的信息,外部节点(主要是Megastore的client)可以通过访问Coordinator获知当前本地副本是否与其他副本一致,即当前副本是否具有最新的已提交的数据。利用Coordinator,如果判断出当前本地副本已经是最新的数据,则只需要读取本地副本,而不需要读取超过半数节点就可以读取到最新的数据。
下面介绍Megastore中的数据读取流程,Megastore中的数据读取流程除了读取数据外还有两个重要功能:
1.尝试更新本地副本的Coordinator。
2.解决中间态数据问题。这里需要说明的是,Megastore的更新日志与数据是分离的,每个Megastore副本收到更新操作后,都会立刻更新自己的日志,但不会立刻把更新操作应用到对应的big table中。这是因为,在类似Paxos的prepare阶段就发送到各个副本了,此时更新操作会写入日志,但是只有收到Accept消息后,副本才能确定这是一个已经超过提交的Paxos的数据,才可以将更新操作真正写入big table。
Megastore数据读取流程:
1.查询本地副本对应的Coordinator以获取本地副本是否已经是最新的已提交的数据。
2.选择一个要读取的副本,使得该副本肯定包含最新的已提交的更新操作。
a)如果从1中发现本地副本已经包含最新的已提交的数据,则选择本地副本。
b)如果1中检查失败,则读取半数以上的副本,从中挑选版本号最大的副本。
3.追赶数据。对于选择的副本,如果不能确定副本上某次更新操作是已经提交的,则通过查询其它副本确定。如果读取所有副本都无法确定,则Paxos协议发起一次空的更新操作。则要么空操作成为本次Paxos的value,要么之前不能确定的更新操作成为Paxos的value。
4.修正Coordinator。如果在2中选择了本地副本,且在1中Coordinator认为本地副本不包含最新已提交的数据,则向Coordinator发送一个validation消息,告诉Coordinator本地副本以及其它副本一致。
5.向2中选择的副本查询数据,如果查询失败,重新发起本流程并选择其他副本读数据。
这里解释追赶数据这一过程。假设有3个副本,正如在上文中的分析,如果仅仅读取两个副本,虽然已经满足Quorum,但在这两个副本中选择版本号最大的一个副本,却不能知道该副本上最后一个版本的数据是不是最新的已提交的数据。例如,如果3副本的版本是(3,2,3),那么读取前两个副本,3已经是最新的已提交的版本,但如果3副本的版本是(3,2,2),那么读取前两个副本,3不一定是最新的已经=提交的版本。在Megastore中,系统利用Paxos协议更新,如果某个副本收到对某个版本的Accept消息,则说明该版本数据已经提交,对于没有收到Accept消息的数据,副本本身无法判断该数据是否已经提交。为此Megastore在读取数据增加了追赶数据的过程,就是为了在各个副本上确定每个Paxos实例最终产生的value。
另一方面,如果某次更新对应的Paxos实例不完整,那么也无法确定该次更新产生的value。例如,Accept消息只在某一个副本上产生效果并生成对应的更新日志,此时读取所有的副本可以发现该日志并非一个已经成功提交的更新,且对应的那个Paxos实例也还产生value,有可能那次更新操作已经失败,有可能那次更新操作正在进行。为此Megastore发起一次空操作,要么空操作成为最后Paxos的value,要么正在进行的更新操作成功Paxos的value。
上述流程不难发现,在没有异常,各副本一致的情况下,查询只会发生在本地副本,而无需读取多个副本。
再继续讨论Megastore的更新流程。Megastore每次成功的更新操作都会附带指定下一次更新操作的leader副本。通常,客户端指定本地机房的副本作为leader副本。所谓Leader副本非常类似Primary-secondary中的Primary,但leader副本不是必须的,只是一种性能优化,利用leader副本尝试跳过Paxos的准备阶段,简化了Paxos流程。但当leader副本失败(类似于代码优化中的fast path失败),系统退化到普通的Paxos过程。
Megastore中的数据更新流程:
1、尝试使用Leader直接提交数据。访问Leader节点,请求Leader节点以paxos编号0直接向各副本发送Accept消息。如果成功,转3。
2、准备阶段:通过更新操作在日志中的位置,获得当前paxos实例的编号。在本次paxos实例中,选择一个最大的轮次号b发起正常的paxos准备流程,如果收到超过半数的promise消息,则转3。
3、批准阶段:向所有的副本发送Accept消息,如果失败,则转2。
4、修正Coordinator。如果没有收到某个副本的Accepted消息,向该副本对应的Coordinator发送一个Invalid消息,告知该Coordinator对应副本已经不与其他副本同步。
5、各个节点根据本次操作日志更新对应的big tbale中的数据。
上述流程中,步骤1尝试使用Leader副本进行快速更新。如果该Leader副本收到的是本次paxos实例(对应全局更新操作的次序)第一个更新请求,则该流程可以生效。当leader节点失效,或者有并发的多个更新请求时,该优化失败,转为正常的Paxos过程。
流程中的步骤4是不能失败的,如果某个副本处于不一致的状态,而又不能通知对应的Coordinator,则用户就有可能在读取流程中读到该副本上的数据,从而打破系统的强一致性。Megastore对此的办法是:
1.相比底层的big table系统,Coordinator是一个非常简单的无状态的轻量级服务,其稳定性本身较高。
2.每个Coordinator的状态都会计入Chubby,一旦Coordinator失去Chubby中的锁,即失去Chubby lease,Coordinator会将对应副本的状态标记为不一致。其它节点可以通过监控coordinator在Chubby中对应的锁而获知Coordinator的状态。一旦一个Coordinator异常失效,更新流程可能会阻塞在第4步直到这个Coordinator失去在Chubby中的锁。在极端网络分化等异常下,可能有这样的情况:更新流程的执行节点无法给Coordinator发送Invalid消息,而Coordinator却能始终占有Chubby中对应的锁。Megastore将这种极端情况通过OP手动杀Coordinator解决。
Megastore通过改良的协议给出了一种跨机房、跨地域实现高可用系统的方案。与PNUTS的跨机房方案相比,Megastore的方案具有强一致性,且可以随时在多个副本上读取最新的已经提交的数据。而PNUTS的虽然也具有读取最新已提交的数据的功能,但是由于副本之间采用异步同步的方式,通常只能在primary副本上才能读取到最新的已经提交的数据。
参考资料:《分布式系统原理介绍》作者:刘杰