为了解决分布式系统的一致性问题,在长期的探索研究过程中,涌现出了一大批经典的一致性协议和算法,其中最著名的就是二阶段提交协议、三阶段提交协议和Paxos算法。
2PC与3PC
在分布式系统中,每一个机器节点虽然都能够明确地知道自己在进行事务操作过程中的结果是成功或失败,但却无法直接获取到其他分布式节点的操作结果。因此,当一个事务操作需要跨越多个分布式节点的时候,为了保持事务处理的ACID特性,就需要引入一个称为”协调者“的组件来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点则被称为“参与者”。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务真正进行提交。基于这个思想,衍生出了二阶段提交和三阶段提交两种协议。
2PC
2PC,是Two-Phase Commit的缩写,即二阶段提交,是计算机网络尤其是在数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务处理过程中能够保持原子性和一致性而设计的一种算法。通常,二阶段提交协议也被认为是一种强一致性协议,用来保证分布式系统数据的一致性。
2PC执行流程
二阶段提交协议是将事务的提交过程拆分成了两个阶段来进行处理,其执行流程如下:
阶段一:提交事务请求
1、事务询问
协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者响应。
2、执行事务
各参与者节点执行事务操作,并将Undo和Redo信息记入事务日志中。
3、各参与者向协调者反馈事务询问的响应
如果参与者成功执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行;如果参与者没有成功执行事务,那么就反馈给协调者No响应,表示事务不可以执行。
阶段二:执行事务提交
协调者会根据各参与者的反馈情况来决定最终是否可以进行事务提交操作,正常情况下,包含以下两种可能:
执行事务提交:假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务提交。
1、 发送提交请求
协调者向所有参与者节点发出Commit请求。
2、事务提交
参与者接收到Commit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。
3、 反馈事务提交结果
参与者在完成事务提交之后,向协调者发送Ack消息。
4、完成事务
协调者接收到所有参与者反馈的Ack消息后,完成事务。
中断事务:假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
1、发送回滚请求
协调者向所有参与者节点发出Rollback请求。
2、事务回滚
参与者接收到Rollback请求后,会利用其在阶段一中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
3、反馈事务回滚结果
参与者在完成事务回滚之后,向协调者发送Ack消息。
4、中断事务
协调者接收到所有参与者反馈的Ack消息后,完成事务中断。
简单地讲,二阶段提交将一个事务的处理过程分为了投票和执行两个阶段,其核心是对每个事务都采用先尝试后提交的处理方式,因此也可以将二阶段提交看作一个强一致性的算法。
2PC存在的问题
1、同步阻塞
在执行过程中,所有参与该事务操作的逻辑都处理于阻塞状态。即节点之间在等待对方的相应消息时,它将什么也做不了。特别是,当一个节点在已经占有了某项资源的情况下,为了等待其他节点的响应消息而陷入阻塞状态时,当第三个节点尝试访问该节点占有的资源时,这个节点也将连带陷入阻塞状态。
2、 单点问题
一旦协调者出现问题,那么整个二阶段提交流程将无法运转,更为严重的是,如果协调者是在阶段二中出现问题的话,那么其他参与者将会一直处于锁定事务资源的状态中,而无法继续完成事务操作。
3、 数据不一致
当协调者向所有的参与者发送Commit请求之后发生了局部网络异常或者是协调者在尚未发送完Commit请求之前自身发生了崩贵,导致最终只有部分参与者收到了Commit请求,于是整个分布式系统便出现了数据不一致性现象。
3、太过保守
二阶段提交没有设计较为完善的容错机制,任意一个节点的失败都会导致整个事务的失败。
3PC
3PC,是Three-Phase Commit的缩写,即三阶段提交,是2PC的改进版。由CanCommit、PreCommit和doCommit三个阶段组成的事务处理协议。
3PC执行流程
阶段一:CanCommit
1、事务询问。
协调者向所有的参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
2、各参与者向协调者反馈事务询问的响应。
参与者在接收到来自协调者的canCommit请求后,正常情况下,如果其自身认为可以顺利执行事务,那么会反馈Yes响应,并进入预备状态,否则反馈No响应。
阶段二:PreCommit
协调者会根据各参与者的反馈情况来决定最终是否可以进行事务提交操作,正常情况下,包含以下两种可能:
执行事务预提交;假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
1、发送预提交请求
协调者向所有参与者节点出preCommit的请求,并进入prepared阶段。
2、事务预提交
参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日志中。
3、各参与者向协调者反馈事务执行的响应
如果参与者成功执行了事务操作,那么就会反馈给协调者Ack响应,同时等待最终的指令:提交(commit)或中止(abort)。
中断事务:假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有者的反馈响应,那么就会中断事务。。
1、发送中断请求
协调者向所有参与者节点发生abort请求。
2、中断事务
无论是收到来自协调者的abort请求,或者是在等待协调者请求过程中出现超时,参与者都会中断事务。
阶段三:doCommit
该阶段将进行真正的事务提交,会存在以下两种可能的情况。
执行提交
1、发送提交请求
进入这一阶段,假如协调者处于正常状态,并且它接收到了来自所有参与者的Ack响应,那么它将从“预提交”状态转换到“提交”状态,并向所有参与者发送doCommit请求。
2、事务提交
参与者接收到doCommit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。
3、反馈事务提交结果
参与者在完成事务提交之后,向协调者发送Ack消息。
4、完成事务
协调者接收到所有参与者反馈的Ack消息后,完成事务。
中断事务
进入这一阶段,假设协调者处于正常工作状态,并且有任意一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应。
1、发送中断请求
协调者向所有的参与者节点发送abort请求。
2、事务回滚
参与者接收到abort请求后,会利用其在阶段二中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
3、反馈事务回滚结果
参与者在完成事务会滚之后,向协调者发送Ack消息。
4、中断事务
协调者接收到所有参与者反馈的Ack消息后,中断事务。
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者abort请求时,在等待超时之后,会继续进行事务的提交。即当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到协调者的doCommit或者abort请求,但事务仍然会提交。
3PC存在的问题
三阶段提交协议与两阶段提交协议有两个主要的不同:
增加了一个询问阶段,询问阶段可以确保尽可能早的发现无法执行操作而需要中止的行为,但是它并不能发现所有的这种行为,只会减少这种情况的发生。
在准备阶段以后,协调者和参与者执行的任务中都增加了超时,一旦超时,协调者和参与者都继续提交事务,默认为成功,这也是根据概率统计上超时后默认成功的正确性最大。
三阶段提交协议在去除阻塞的同时也引入了新的问题,那就是在参与者接收到preCommit消息后,如果网络出现分区,此时协调者所在的节点和参与者无法进行正常的网络通信,在这种情况下,该参与者依然会进行事务的提交,这必然出现数据的不一致性。
Paxos算法
Paxos算法是一种基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。
问题描述
在古希腊的有一个叫做Paxos的小岛,岛上采用会议的形式来通过法令,议会中的议员通过信使进行消息的传递。值得注意的是,议员和信使都是兼职的,他们随时有可能会离开议会厅,并且信使可能会重复的传递信息,也可能一去不复返。因此,议会协议要保证在这个情况下法令仍然能够正确的产生,并且不会出现冲突。
首先将议员的角色分为Proposer,Acceptor,和Learner(允许身兼数职)。Proposer提出提案,提案信息包括提案编号和提议的value;Acceptor收到提案后可以接受(accept)提案,如果提案获得多数Acceptors的接受,则称该提案被批准(chosen);Learner只能“学习”被批准的提案。划分角色后,就可以更精确的定义问题:
决议(value)只有在被Proposers提出后才能被批准(未经批准的决议称为“提案”(proposal);
在一次Paxos算法的执行实例中,只批准(chosen)一个value;
learners只能获得被批准(chosen)的value。
由上面的三个语义可演化为下面几个约束:
P1:一个Acceptor必须接受(accept)第一次收到的提案。
P2:一旦一个具有value v的提案被批准(chosen),那么之后批准(chosen)的提案必须具有value v。
P2a:一旦一个具有value v的提案被批准(chosen),那么之后任何Acceptor再次接受(accept)的提案必须具有value v。
P2b:一旦一个具有value v的提案被批准(chosen),那么以后任何Proposer提出的提案必须具有value v。
P2c:如果一个编号为n的提案具有value v,那么存在一个多数派,要么他们中所有人都没有接受(accept)编号小于n的任何提案,要么他们已经接受(accept)的所有编号小于n的提案中编号最大的那个提案具有value v。
P1a:当且仅当Acceptor没有回应过编号大于n的prepare请求时,Acceptor接受(accept)编号为n的提案。
Paxos算法内容
决议的提出与批准
阶段一(决议提出)
1、Proposer选择一个提案编号M,然后向Acceptors的某个超过半数的子集成员发送编号为M的prepare请求。
2、如果一个Acceptor接收到一个编号为M的pepare请求,且编号M大于该Acceptor已经响应的所有prepare请求的编号,那么它就会将它已经批准过的最大编号的提案作为响应反馈给Proposer,同时该Acceptor会承诺不会再批准任何编号小于M的提案。
阶段二(批准阶段)
1、如果Proposer收到来自半数以上的Acceptor对其发出的编号为M的prepare请求的响应,那么它就会发送一个针对[M,V]提案的accept请求给Acceptor。注意,V的值就是收到响应中编号最大的提案的值,如果响应中不包含任何提案,那么它就是任意值。
2、如果Acceptor收到这个针对[M,V]提案的Accept请求,只要该Acceptor尚未对编号大于M的prepare请求做出响应,它就可以通过这个提案。
提案的获取
如何让Learner获取提案,大体可以有以下几种方案:
-
方案一
Learner获取一个已经被选定的提案的前提是,该提案已经被半数以上的Acceptor批准了。因此,最简单的做法就是一旦Acceptor批准了一个提案,就将该提案发送给所有的Learner。这种做法虽然可以让Learner尽快地获取被选定的提案,但是却需要让每个Acceptor与所有Learner逐个进行一次通信,通信的次数至少为二者个数的乘积。
-
方案二
我们可以让所有的Acceptor将它们对提案的批准情况,统一发送给一个特定的Learner(这样的Learner称为“主Learner”),假定Learner之间可以通过消息通信来互相感知提案的选定情况。当主Learner被通知一个提案被选定时,它会负责通知其他的Learner。
方案二虽然需要多一个步骤才能将提案通知到所有的Learner,但其通信次数却大大减少了,通常只是Acceptor和Learner的个数总和。但同时,该方案引入了一个新的不稳定因素;主Learner随时可能出现故障。
-
方案三
将主Learner的范围扩大,即Acceptor可以将批准的提案发送给一个特定的Learner集合,该集合中的每个Learner都可以在一个提案被选定后通知所有其他的Learner。这个Learner集合中的Learner个数越多,可靠性就越好,但同时网络通信的复杂度也就越高。
通过选取主Proposer保证算法的活性
假设存在这样一种极端情况,有两个Proposer依次提出了一系列编号递增的议案,但是最终都无法被选定,具体流程如下:
Proposer P1提出了一个编号为M1的提案,并完成了上述阶段一的流程。但与此同时,另外一个Proposer P2提出了一个编号为M2(M2>M1)的提案,同样也完成了阶段一的流程,于是Acceptor已经承诺不再批准编号小于M2的提案了。因此,当P1进入阶段二的时候,其发出的accept请求将被Acceptor忽略,于是P1再次进入阶段一并提出了一个编号为M3,(M3>M2)的提案,而这又导致P2在第二阶段的accept请求被忽略,以此类推,提案的选定过程将陷入死循环。
为了保证Paxos算法流程的可持续性,以避免陷入上述提到的“死循环”,就必须选择一个主Proposer,并规定只有主Proposer才能提出议案。这样一来,只要主Proposer和过半的Acceptor能够正常进行网络通信,那么但凡主Proposer提出一个编号更高的提案被提出或正在接受批准,那么它会丢弃当前这个编号较小的提案,并最终能够选出一个编号足够大的提案。因此,如果系统中有足够多的组建(包括Proposer、Acceptor和其他网络通信组建)能够正常工作,那么通过选择一个主Proposer,整套Paxos算法流程就能够保持活性。
参考资料
从Paxos到Zookeeper分布式一致性原理与实践