您的位置:首页 > 其它

Two Phase Commit (2PC) [转]

2012-09-15 14:15 513 查看
转自:http://nosql-wiki.org/foswiki/bin/view/Main/TwoPhaseCommit

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

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

理想时代:没有异常

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

协调者向所有参与者发出VOTE_REQUEST请求,然后协调者阻塞等待所有参与者的响应

参与者在收到VOTE_REQUEST的时候,执行事务预处理,根据预处理的结果响应coordinator:VOTE_COMMIT或者VOTE_ABORT; 然后参与者等待协调者的最后决定(global_decision)

协调者等待所有的参与者的响应,如果所有参与者都响应VOTE_COMMIT,那么协调者就向所有参与者发出GLOBAL_COMMIT; 如果至少有一个参与者响应VOTE_ABORT,那么协调者就向所有参与者发出GLOBAL_ABORT

参与者根据协调者的决定(global_decision)在本地进行事务操作

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



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



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

本节的算法摘自《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如何在高可用的系统中使用。在分布式 一致性这一系列文章中,我会为大家逐一解开谜底。

Large-scale Incremental Processing Using Distributed Transactions and Notifications 看google如何使用2PC实现实时搜索,通过BigTable自身的高可用性解决解决participants的高可用性问题;通过 乐观锁解决coordinator不具备高可用性的问题。看了这篇分析,你会发现前面我关于2PC的分析是正确的。

Chubby一种可能的实现解析,看2PC如何与PAXOS结合实现replicated state machine,通过 分布式选举解决coordinator的高可用性问题,通过replicated state machine解决participants的高可用性问题。

分析对工程实践的指导

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

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

metaserver如何监控拷贝进度?

如果拷贝的源失败如何处理?

如果拷贝的目的失败如何处理?

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

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