Two Phase Commit (2PC) [转]

转自:http://nosql-wiki.org/foswiki/bin/view/Main/TwoPhaseCommit

2PC是工程上广泛使用的分布式一致性协议,它主要解决的问题是:一个事务,要么所有参与者都commit;要么所有参与者都abort。 在没有异常的情况下,2PC是很容易理解的。理解2PC的难点在于出现异常的情况下协议如何保证事务的正确执行执行。

2PC协议中有两种身份:协调者(coordinator)和参与制(participant)。2PC包括两个阶段,每个阶段各自包含两个步骤。下面请跟着 笔者的思路逐渐加深对2PC协议的理解。

理想时代:没有异常

此时,我们假设所有参与者、网络都不会出现异常,这种情况下2PC没有任何难度。

  1. 协调者向所有参与者发出VOTE_REQUEST请求,然后协调者阻塞等待所有参与者的响应
  2. 参与者在收到VOTE_REQUEST的时候,执行事务预处理,根据预处理的结果响应coordinator:VOTE_COMMIT或者VOTE_ABORT; 然后参与者等待协调者的最后决定(global_decision)
  3. 协调者等待所有的参与者的响应,如果所有参与者都响应VOTE_COMMIT,那么协调者就向所有参与者发出GLOBAL_COMMIT; 如果至少有一个参与者响应VOTE_ABORT,那么协调者就向所有参与者发出GLOBAL_ABORT
  4. 参与者根据协调者的决定(global_decision)在本地进行事务操作

在理想的时代,一切都是完美的,一切都是简单的。协调者的状态转移图如下:

2pc-coordinator.png

参与者的状态转移图如下:

2pc-participant.png

次理想时代:节点、网络异常会最终恢复

本节的算法摘自《Distributed Systems: Principles and Paradigms》。

Actions of Coordinator

01 write("START_2PC to local log");
02 multicast("VOTE_REQUEST to all participants");
03 while(not all votes have been collected)
04 {
05   waitfor("any incoming vote");
06   if(timeout)
07   {
08     write("GLOBAL_ABORT to local host");
09     multicast("GLOBAL_ABORT to all participants");
10     exit();
11   }
12   record(vote);
13 }
14 if(all participants send VOTE_COMMIT and coordinator votes COMMIT)
15 {
16   write("GLOBAL_COMMIT to all participants");
17   multicast("GLOBAL_ABORT to all participants");
18 }
19 else
20 {
21   write("GLOBAL_ABORT to local log");
22   multicast("GLOBAL_ABORT to all participants");
23 }

Actions of Participantsdata/Main/TwoPhaseCommit.txt

01 write("INIT to local log");
02 waitfor("VOTE_REQUEST from coordinator");
03 if(timeout)
04 {
05   write("VOTE_ABORT to local log");
06   exit();
07 }
08 if("participant votes COMMIT")
09 {
10   write("VOTE_COMMIT to local log");
11   send("VOTE_COMMIT to coordinator");
12   waitfor("DESCISION from coordinator");
13   if(timeout)
14   {
15     multicast("DECISION_REQUEST to other participants");
16     waituntil("DECISION is received"); /// remain blocked
17     write("DECISION to local log");
18   }
19   if(DECISION == "GLOBAL_COMMIT")
20   {
21     write("GLOBAL_COMMIT to local log");
22   }
23   else if(DECISION == "GLOBAL_ABORT")
24   {
25     write("GLOBAL_ABORT to local log");
26   }
27 }
28 else
29 {
30     write("GLOBAL_ABORT to local log");
31     send("GLOBAL_ABORT to coordinator");
32 }

最糟糕的时代:协调者和参与者在死亡后无法恢复

2PC很无辜的看着大家,其实这个与我无关。听我详细道来。

算法解析

2PC这个协议本身其实本不难,难的是很多人(包括我自己)在学习算法本身的时候会思考如何把他应用在实际系统上。是想, 如果我们假设任何阶段coordinator或者participant出现异常,那么整个算法就停止在那个地方一直循环等待,直到退出的节点 恢复,算法才继续往前走,这个算法其实一点难度都没有。但是每个人都会思考,这样的算法在实际过程中还有用吗?实际过程中 的工程师们是如何来处理这个问题的?只要一思考这些,读者就会觉得怎么都不对。其实就2PC而言,他本来就是一个阻塞的算法, 在所有participant都响应VOTE_REQUEST之后,在收到DECISION之前,coordinator宕机,那么算法就会一直阻塞,因为没有人 知道最后的decision是什么。既然它天生就是阻塞的,那么我们直接再弱化一下它好了,任何步骤主要出现异常,算法都阻塞。 这样理解到的才是算法的实质。

可能有人会问,上面算法中有的地方在超时后会进行一些操作,然后算法可以继续;有些地方在超时后算法无法继续;这是为什么? 什么时候决定算法可以继续,什么时候应该阻塞?以我对算法本身的理解,继续还是阻塞的标准是:

  • 是否会导致事务的结果处于一种不一致的状态(一部分参与者commit,一部分参与者abort);如果不会出现不一致的情况, 那么算法可以继续;否则就必须阻塞。

可以这么理解:非阻塞的部分是算法的优化。算法继续,唯一会出现不一致状态的情况是,所有的参与者都响应了VOTE_REQUEST,在 任何参与者收到decision之前coordinator宕机死亡,此时所有参与者都必须等待coordinator恢复。

有个同事的观点:所有参与者(包括协调者)都必须通过多副本的方式保证自己的高可用性, 因为单副本不可用的问题不是2PC这个协议的 目的,如果没有2PC这个协议,单副本的不可用性也是存在的,因此这种问题与2PC无关。可以说2PC本身不解决高可用性问题,它仅仅 解决的是atomic group commit的问题,这是2PC的假设,也是理解2PC的关键。一句话:每个协议解决自己的问题,不要带着你面临的 n个问题来理解2PC(包括其他分布式协议),这样只能使你自己陷入死角。

大家会说,那么每个协议如果这样去了解,岂不是都很简单,我作为架构师的最终目的是实现高可用的系统,而不是分开理解每个协议。 呵呵,可以理解,我和大家一样由于这个想法走了很多的弯路。我会后续慢慢的告诉大家2PC如何在高可用的系统中使用。在分布式 一致性这一系列文章中,我会为大家逐一解开谜底。

分析对工程实践的指导

还是从同事那里讨论得到的:如果在分布式系统中,协议包括这种逻辑:A发起一个请求给所有人; 等待所有人响应之后A继续进行处理。这样的东西一看就太复杂,不靠谱,因为这相当于实现了一个2PC,有些偏复杂,如果必须这么实现, 那么同学,你一定要按照2PC的理解方式去理解,去分析这个问题。

其实在分布式系统中,需要使用2pc思想指导设计的地方很多。一个很简单的例子,中心节点控制从一个数据节点拷贝一个分片到另外一个数据 节点就需要这样的协议。以gfs增加block副本为例,当gfs metaserver的后台线程发现某个block的副本数量小于配置的阈值的时候,就会发起 副本拷贝的任务:将block从一个chunkserver拷贝到另外一个chunkserver。这样的场景会产生如下问题:

  1. metaserver如何监控拷贝进度?
  2. 如果拷贝的源失败如何处理?
  3. 如果拷贝的目的失败如何处理?

一个比较挫的设计方法:meta不断的去询问源或者目的,任务是否结束,根据复制的结果决定如何进行后续的操作。想一想,这个实现起来有 多困难,metaserver上有上十万的block,如何处理?

看看伟大的google是如何处理的,metaserver为所有复制任务维护一个任务队列,任务队列中的任务有超时时间; 后台线程发现副本数量小于配置的阈值,首先查看任务队列中是否有任务正在进行该bock的复制操作,如果有任务 则不做任何事情;如果没有相应的任务,则发起任务。metaserver的工作到此为止。那么如何判断任务队列中的任务 完成与否呢?这是chunkserver的事情,复制的目的会在复制任务完成后向metaserver汇报新复制的block, metaserver在收到复制完成的汇报后会把相应的任务从任务队列中删除。这样,整个协议很简单,很清晰,不易出bug。 之前那种挫的设计,状态太难维护。在我们实际的工程实践中,一定要尽量少的使用一个进程去等待另外两个进程 完成某项任务的协议,这样的协议太难维护了。

原文地址:https://www.cnblogs.com/viviancc/p/2686475.html