zoukankan      html  css  js  c++  java
  • 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。 之前那种挫的设计,状态太难维护。在我们实际的工程实践中,一定要尽量少的使用一个进程去等待另外两个进程 完成某项任务的协议,这样的协议太难维护了。

  • 相关阅读:
    D. Constructing the Array
    B. Navigation System
    B. Dreamoon Likes Sequences
    A. Linova and Kingdom
    G. Special Permutation
    B. Xenia and Colorful Gems
    Firetrucks Are Red
    java getInstance()的使用
    java 静态代理和动态代理
    java 类加载机制和反射机制
  • 原文地址:https://www.cnblogs.com/viviancc/p/2686475.html
Copyright © 2011-2022 走看看