技术专栏 | 走进分布式一致性协议

作者:TalkingData 战鹏弘

本文由TalkingData原创,转载请获取授权。

在分布式系统中,每一个机器节点虽然都能明确的知道自己在进行事务操作过程中的结果是成功或失败,但却无法直接获取其它分布式节点的操作结果。因此,当一个事务操作需要跨越多个分布式节点的时候,为了保证事务处理的ACID特性,需要引入一个成为“协调者”的组件来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点则被称为“参与者”。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务真正进行提交。基于这个思想,衍生出了二阶段提交(2PC)和三阶段提交(3PC)。

2PC

为了使分布式系统下所有节点在进行事务处理的时候能够保持原子性和和一致性而设计的算法。

两个阶段

阶段一:提交事务请求

协调者向各个参与者发送事务内容,询问是否可以执行事务操作,等待参与者响应。

参与者接收到询问之后,执行事务操作,但是不commit,将undo和redo信息保存到事务日志中。

参与者根据执行情况向协调者返回是否可以执行事务的响应。

阶段二:执行事务提交

阶段一参与者都返回yes

协调者收到所有的yes信号之后,通知参与者执行最后的commit,参与者执行完成之后,想协调者返回ACK信息。

协调者接收到所有的ACK信息之后,完成分布式事务操作

阶段一某个参与者返回no

说明其中一个参与者无法成功执行事务,协调者通知所有参与者回滚事务。

参与者进行事务回滚,完成之后向协调者返回ACK信息

协调者接收到所有的ACK信息之后,事务中断完成

协调者的作用

阶段一

发送给参与者信息

接收参与者反馈,确定下一步需要发送给参与者的信息

阶段二

根据阶段一获得的响应,确定需要发送给参与者的信息(此时如果协调者出问题,无法通知参与者执行commit,参与者的资源会一直被锁住)

接收参与者的反馈,事务结束

注意问题

同步阻塞
第一阶段各个参与者执行事务操作的过程都是阻塞的,各个所有参与者全部完成响应之前,资源都是被加锁的,无法进行其它操作。

协调者的单点问题
两个阶段都需要协调者进行协调,第二阶段中协调者向参与者发送事务commit的新号时,一旦出问题,参与者将无法提交commit操作,资源一直会处于锁定状态,无法释放。

数据不一致
第二阶段协调者发送事务commit信息时,如果与某个参与者的连接出问题,会出现其他参与者成功提交事务,而该参与者事务无法提交,各个参与者的数据会出现不一致的情况。

3PC

阶段一(canCommit)

区别于2PC,3PC的第一阶段会询问所有的参与者是否可以进行事务提交,但是不去执行事务(2PC的第一阶段实际上去执行了事务,但是没有commit而已),这一阶段可以在不锁定资源的前提下,判断各个参与者是否能够执行事务,当然各个参与者返回yes不代表后续执行事务一定会成功。

2PC的第一阶段会真正执行事务,如果某个参与者出现问题,消耗了很长时间返回给协调者no信号,再这个很长时间内,其它的参与者会一直锁定资源,block在那里。3PC的第一阶段有效的解决了该问题。

阶段二(preCommit)

阶段二同2PC的阶段一实际上是相同的,会执行事务但是不提交,但是很大程度上减少了2PC在他的阶段一出现阻塞的问题

阶段一参与者都返回YES

各个参与者执行事务,但是不提交,返回给协调者ACK信号

阶段一有某个参与者返回no

协调者通知参与者中断事务,因为第一阶段中没有执行操作,所以不需要回滚

阶段三(doCommit)

阶段二所有参与者返回yes

说明各个参与者事务执行成功,通知参与者进行commit,返回给协调者ACK,完成事务

阶段二某个参与者返回No

通知所有参与者回滚事务,返回给协调者ACK,中断事务

注意问题

进入阶段三之后,如果协调者出现故障或者与参与者之间的连接出现问题,参与者等待commit信号超时之后,会继续进行事务提交。这种机制一定程度上在协调者故障或者连接出问题时,解决数据不一致问题。

2PC和Paxos

2PC和Paxos作为分布式一致性协议,分别适用于两类一致性:操作原子性和副本一致性。

2PC:保证分布式环境中的多台机器的操作的原子性,要么全部成功,要么全部不成功

Paxos:保证同一个数据分片的多个副本之间的数据一致性

Paxos算法

Paxos算法是莱斯利-兰伯特于1990年提出的一种基于消息传递且具有高度容错性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。

Paxos算法需要解决的问题是如何在一个可能发生机器宕机或者网络异常等异常的分布式系统中,快速且正确的在集群内部对某个数据的值达成一致,并且保证无论发生上述任何异常,都不会破坏整个系统的一致性。

分布式系统的存在为了解决单点问题,例如典型的master slave模式,一旦master宕机之后,需要将slave节点切换为master节点,条件是slave和master节点的数据是一致的,也就是说在master上的一切操作都需要同步到slave节点上,这样的slave才具备选举为master的条件。Paxos算法可以为这种数据一致性提供有效的保障。

Paxos算法可以分为两个阶段,prepare阶段和accept阶段,下面简单阐述一下这两个阶段。

prepare阶段

假设现在分布式集群中有三个节点:A、B、C,A把申请操作的请求发送给A、B、C,发送的请求会携带一个Sequence Number(这个值会不断递增,且是唯一的),接受节点在prepare阶段会拒绝接受到的任何提案号小于当前提案号的请求。

如果接受节点接收到的提案号大于当前提案号,节点会给节点A返回yes,并且不在接受提案号更小的提案,也就是说节点总会对最新的提案号做承诺。

accept阶段

如果提案者A收到了半数以上的节点返回yes,他会再次向接收节点发送accept request,仍会携带一个sequence number(n),当接收节点收到accept request信号之后,如果n是接收者接收到的最新的提案号,那么会最终通过提案,如果发现存在比n更大的提案号,拒绝该request,同时提案者会重新进行第一阶段(prepare阶段)。

具体的执行过程如下图所示:

(图片来自:https://www.cnblogs.com/cchust/p/5617989.html)

分布式一致性算法的工程实践

Chubby

Google Chubby是一个有名的分布式锁服务,Google的GFS和Big Table等大型分布式系统都是用它来解决分布式写作、元数据存储和Master选举等一系列与分布式锁相关的问题。

假设Chubby集群中有5台服务器:A B C D E,其中A是当前的master,另外4台服务器实际上是A的副本。Chubby实际运行过程中,只有作为master的A服务器会对外提供写操作服务,其他四台服务器通过Paxos协议从A服务器中同步数据的操作,Paxos协议能够有效的保证副本节点的数据和master节点之间保持一致性。

Chubby中每次提案value的选定是一个Paxos instance,提案由多个proposer提出,最终得到一个value。每次提案的选举都会计入到底层的log日志中,由于Chubby需要不间断的对外提供服务,因此会不断产生Paxos instance,事务日志也会不断增长。每个Paxos instance负责确定一个value,多个instance之间是互不相关的,可以并发进行。将Paxos每两个阶段的提交prepare→promise→propose→accept记作一个round,每个Paxos instance执行过程中可能会经历多轮round,最终才能确定最后的提案。

上述基本的Paxos算法执行过程存在可优化的地方:多个proposer的活锁问题会严重影响效率,导致一个instance最终选定提案需要执行多轮round。

实际执行过程中,可以将多个instance的prepare阶段合并成一个阶段。首先必须在多个proposer中选举出一个master作为唯一的proposer,原本多个instance之间是互相独立的,只需要保证instance内部的round序号不重复即可。现在为了合并prepare阶段,多个instance之间公用一套序号,具体做法如下:

当某个replica通过选举获得master资格后,用新分配的编号N广播一个prepare消息,这个prepare消息被所有未达成一致的instance和将来还未开始的instance共用。

当acceptor接收到prepare后,现在必须对多个instance同时做出回应,这可以封装在一个数据包中,假设最多允许K个instance同时选举,那么:

当前至多有K个未达成一致的instance,将这些未决的instance各自最新accept的value(若没有用null代替)封装进一个数据包,作为promise消息返回

同时,标记这些未决instance和所有未来instance的highestPromisedNum为N,如果N比它们原先的值大的话。这样,这些未决instance和所有未来instance都不能再accept编号小于N的proposal。

然后master就可以对所有未决instance和所有未来instance分别执行propose→accept阶段,始终使用编号N,如果这个master保持稳定的话,就再也不需要prepare→promise了。但是,一旦发现acceptor返回了一个reject消息,说明另一个master启动,用更大的编号M>N发送了Prepare消息,这时自己就要分配新的编号(必须比M更大)再次进行prepare→promise阶段。

ZooKeeper

在ZooKeeper中,client向ZooKeeper提出一个事务,例如创建一个znode,请求会发送到ZooKeeper中的所有机器上,leader需要协调事务的执行,只要集群中半数以上的机器成功执行了该事务,leader便会通知follower进行事务提交。leader作为ZooKeeper中最重要的角色,协调事务执行过程中会保存一个全局的变更序列,可以保证如果一个状态变更已经被处理了,那么所有以来的状态变更应该已经被提前处理了。
ZooKeeper中使用的分布式一致性协议ZooKeeper Atomic Broadcast(ZAB,ZooKeeper原子消息广播协议),ZAB协议的消息广播过程使用的是一个原子广播协议,类似于一个二阶段提交过程。根据客户端的事务请求,leader服务器会为其生成对应的事务poposal,并发送给集群中其余所有的机器,然后收集各自的选票,最后进行事务的提交。

与二阶段提交不同的是,ZAB协议移除了终端逻辑,所有的follower服务器要么正常反馈leader提出的事务proposal,要么抛弃leader服务器,这意味着leader可以在过半的follower服务器已经反馈ACK信号之后就开始提交事务proposal,而不需要等待所有的follower服务器都反馈响应。当时,这种简化的二阶段提交下,无法处理leader服务器崩溃退出而带来的数据不一致问题,因此ZAB协议添加了崩溃恢复模式来解决这个问题。

ZAB协议的执行过程

ZAB主要包括消息广播和崩溃恢复两个过程,进一步可以分为三个阶段,分别是发现(Discovery)、同步(Synchronization)、广播(Broadcast)阶段。ZAB的每一个分布式进程会循环执行这三个阶段,称为主进程周期。

发现:选举产生PL(prospective leader),PL收集Follower epoch(cepoch),根据follower的反馈,PL产生new epoch(每次选举产生新Leader的同时产生新epoch)。

同步:PL补齐相比follower多数派缺失的状态、之后各Follower再补齐相比PL缺失的状态,PL和follower完成状态同步后PL变为正式leader(established leader)。

广播:leader处理客户端的写操作,并将状态变更广播至follower,follower多数派通过之后leader发起将状态变更落地(deliver/commit)。

在正常运行过程中,ZAB协议会一直运行于阶段三来反复进行消息广播流程,如果出现崩溃或其他原因导致leader缺失,那么此时ZAB协议会再次进入发现阶段,选举新的leader。

leader可能在事务提交完成或者提交了一半的时候崩溃,因此leader选举算法需要确保提交已经被leader提交的事务的proposal,同时丢弃已经被跳过的事务proposal。

如果让leader选举算法能够保证新选举出来的leader服务器拥有集群中所有机器最高编号(ZXID最大)的事务proposal,那么就可以保证这个新选举出来的leader一定具有所有已经提交的提议,更为重要的是如果让具有最高编号事务的proposal机器称为leader,就可以省去leader服务器查询proposal的提交和丢弃工作这一步骤了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注