目录
8.1 GTM如何记录日志比较方便?目前相关项目的GTM是如何记录日志的?
8.2 GTM恢复时“查一下”的逻辑,其实在有些场景是很难实现的,有没有其它的保障方法?
8.3 2PC和paxos是什么关系?所谓的分布式一致性对应的是事务ACID中的一致性还是原子性?
1. 什么是两阶段事务?
在分布式事务中,两阶段提交协议(two phase commit, 2PC)用于保证分布式事务的原子性,具体协议内容直接引用《数据库系统概念第6版》
两阶段事务在一阶段提交之前和普通事务是完全一样的,正常进行页面修改和记录日志,直到发送一阶段提交命令后才进入以上流程。事务一阶段提交成功后进入prepare状态,随后与session无关,任何有权限的用户在任何session连接中都可以进行后续的二阶段提交。PG中一阶段提交的命令为:
PREPARE TRANSACTION ‘name’;
二阶段提交的命令为:
COMMIT PREPARED ’name’;
二阶段回滚的命令为:
ROLLBACK PREPARED ’name’;
两阶段提交协议需要在数据库层和业务层的事务管理器两方面分别实现。数据库层实现以上描述中关于“站点”部分的内容,全局事务管理器(GTM)需要实现以上描述中协调者Ci的逻辑(详见5部分),以及各类异常场景的处理(详情见6部分)。
2. 为什么要用两阶段事务?
简单来说,一个全局事务在多个节点上做了修改,每个节点开启了一个事务,这些修改为了满足全局事务原子性需要同时生效或同时回滚。如果志勇普通事务来实现,当其中一个节点因为各种原因(详见4部分)无法提交儿回滚时,其它节点的事务有可能已经被提交,这样就造成了全局事务原子性的破坏,而两阶段提交协议可以保证这一点,先确定所有节点是否都可以提交,再决定大家一起提交还是一起回滚。
一阶段提交使所有节点检测是否可以提交,如果可以则转换为prepare状态。prepare意味着事务仍然持有所有的资源,并且在收到后续提交/回滚指令之前不会做任何修改。一阶段提交结果记录日志,同时也记录所有占用的资源,因此可以保证即使提交后发生崩溃,prepare状态仍然可以恢复。一阶段提交的结果需要一个协调者来进行收集,决定二阶段需要下发提交还是回滚指令。协调者可以是更上层的事务管理器,也可以是任何一个参与者。这样当下发二阶段提交/回滚指令时,可以保证所有节点要么一起提交,要么一起回滚,从而保证了全局事务的原子性。
3. PG的两阶段事务是如何实现的?
PG在执行一阶段预提交时会产生一个临时文件记录其事务信息以及占用的资源,存放在data目录下pg_twophase/文件夹中,并将该事务记录在pg_prepared_xacts视图中。事务正式提交或回滚时会删除对应的两阶段临时文件,释放占用的资源,并将事务从pg_prepared_xacts视图中移除。PG中存在配置参数max_prepared_transactions限制最多允许的预提交事务个数,默认为0时即关闭两阶段提交功能,备机上该参数必须大于等于主机,否则会build失败。
如果预提交完毕后发生崩溃,在恢复完成前有检查所有预提交事务的逻辑,获取对应临时文件中该事务占用的资源(主要是重新上锁),完成后才能接受新事务。如果预提交完成后发生主备倒换,此时备机(同步备)也已收到事务预提交的日志,在重放日志后也会生成同样的两阶段事务临时文件。主备倒换时同样会根据日志找出所有预提交的两阶段事务,按照其对应的临时文件获取占用的资源,完成后才能接受新事务。
一阶段预提交的代码逻辑在PrepareTransaction()函数中,主要实现在StartPrepare()和EndPrepare()两个函数里,包括数据结构、如何写日志、同步到备机时机等。二阶段提交/回滚的逻辑实现在FinishPrepareTransation()中。twophase.c这个文件包括了两阶段事务的所有数据结构、函数定义和具体实现。
4. 一阶段预提交在什么情况下会失败?
2PC协议是为了防止直接提交事务失败的情况,但是正常情况下事务每条语句执行成功的话是不会提交失败的,那么除了节点异常崩溃以外,还有哪些情况会导致事务直接提交失败呢?
a) 磁盘满
b) 内存满
c) 串行化事务在提交时的检查失败
d) 事务提交时的trigger错误
… …
尽管以上情况都少见,但是一旦发生就会导致数据一致性的破坏,这是不可接受的,因此2PC协议还是十分必要的。
5. 上层的全局事务管理器(GTM)需要实现哪些逻辑?
流程可以参考以下这个图,基本包括了GTM需要实现的最小逻辑:
分为三个阶段:
a) 分布式事务执行完毕后,向所有节点发送一阶段提交(prepare)指令
b) 接受并统计所有节点的反馈结果,如果在规定时间内收到所有节点prepare成功的返回,则发送二阶段提交指令,否则(有节点一阶段提交失败或超时未收到所有回复)发送二阶段回滚指令。
c) 接收并统计所有二阶段提交/回滚指令的执行结果,收到所有节点成功返回后才向上返回事务结束,否则进入异常处理逻辑。
6. 两阶段提交协议能够处理哪些异常逻辑?
理论上如果协调者是独立于所有参与者的第三方节点且可恢复,并且在其发送任何指令前记录日志(即协调者崩溃恢复后知道自己下发过哪些指令),那么两阶段提交协议可以处理所有异常逻辑(尽管有些异常场景处理效率比较低),可以保证分布式数据的一致性(但并不保证强一致性)。
具体而言,两阶段提交的每个阶段都要按顺序做GTM写日志、GTM发送指令、所有节点接收指令、所有节点执行指令、所有节点发送结果、GTM接收结果这几件事,这其中每一件事的前后执行到一半时都有可能发生节点故障和GTM故障。实际上所有的故障场景都是以上的排列组合。这里举例列举出几条典型的故障场景及相应的解决办法:
a) GTM 在下发部分一阶段预提交指令后崩溃,此时GTM已经记录一阶段提交日志,恢复后需要查看所有节点事务的状态是否是prepare,如果有节点上不存在该事务(因为GTM故障断链而回滚),则向已prepare的节点发送二阶段回滚指令,并记录回滚日志。
b) 所有节点都已prepare,GTM下发二阶段提交指令,某节点未执行提交指令时崩溃。崩溃节点恢复后重建prepare事务,GTM未收到该节点提交成功的回复,尝试重新连接知道该节点恢复完成,并重新下发二阶段提交指令,节点执行二阶段提交。
c) GTM下发二阶段提交指令后崩溃,尚未收集到所有节点的执行结果。GTM恢复后查看日志得知事务已进行二阶段提交,此时需要向所有节点查询prepare事务列表,如果有节点的事务仍处于prepare状态说明其没有收到二阶段提交指令,需重新补发,直到所有节点提交完成后向上返回。
这里还有一个很自然的疑问:既然一阶段提交在发生异常时有可能失败,那二阶段提交怎么能保证一定成功呢?事实上一阶段提交已经做了大多数检查,屏蔽了问题4中多数异常,二阶段提交发生失败的可能性大大降低,但仍有失败的可能(至少提交时节点崩溃是无法避免的)。但是一阶段提交失败导致的是事务直接回滚,该事务无法再次提交了;但二阶段事务提交失败大不了回到prepare状态,还可以重新提交。
7. 通过2PC实现全局事务有什么缺点?
2PC协议最大的缺点是单点阻塞问题,即当GTM发送完成一阶段提交,所有节点prepare成功后,GTM发生故障。由于prepare的事务占据的资源并没有释放,直到收到二阶段指令后才释放,而此时GTM已崩溃,此时将长时间占用这些资源造成阻塞,直到GTM恢复完成。该问题的解决办法是3PC协议,在一阶段完成到二阶段提交之间加入了一个通知阶段,通知所有节点知晓大家都已处于prepare状态了。这时如果GTM挂掉,节点等待超时未收到提交指令将自动提交。而如果prepare后长时间未收到通知,说明GTM可能已挂,将自动回滚事务。这样既保证了事务的原子性,又不会因GTM故障而造成阻塞。
2PC协议是一个过分依赖GTM(协调者)角色的协议,一旦GTM故障整个系统将无法运转,即使3PC协议解决了阻塞的问题,也不可能脱离GTM运行。此外,从另一个角度看,2PC的数据一致性,是建立在GTM可恢复的前提下的,而如果GTM在下发部分commit后崩溃且无法恢复,不同节点就存在部分提交部分未提交的状态,并因为GTM无法恢复而使数据无法达成一致。
2PC协议可以看做是一个比较简单的分布式一致性方案,并且数据库层只实现了一部分,把另一部分逻辑抛给了上层的GTM来实现,这样的设计有诸多不合理的地方。因此,目前的分布式数据库一般都采用更加完善的Paxos/Raft分布式一致性方案。从这个角度看,不管是2PC还是3PC,其实只是分布式一致性算法的一个残次版本,毕竟“世界上只有一种一致性算法,那就是Paxos”。
8. 补充问题
8.1 GTM如何记录日志比较方便?目前相关项目的GTM是如何记录日志的?
GTM需要在每次下发指令前记录日志(至少应包括指令本身、事务ID和哪些节点参与),才能保证崩溃可恢复,从而保证2PC协议的正确性。但是一般GTM都是存在于数据库上层的组件,如何保证其持久化的日志是可靠的,难道自己再起一个数据库来记录日志吗?这个方法当然不行,但是开销有点大。以下是目前集中GTM记录日志的实现方法,正好代表了几类解决办法。
a) SDN业务
GTM的日志在底层共享了其中一个数据库,单开一个库专门记录。这样做的好处是不用单独维护,依赖底层数据库的能力保证GTM日志的可靠性。缺点是占用了业务资源,压力大时可能影响性能;同时该数据库的宕机会影响GTM,从而导致整个系统跨事务不可用。
b) DDS中间件
GTM自身维护一套单机数据库(GMDB),保证所有持久化的日志可恢复。优点是自己管理的话,不影响下层业务,本机数据库记录不需要网络传输,效率也会高一些。缺点是数据库和GTM部署在同一节点,如果该节点发生不可恢复的故障,可能引起两阶段事务残留或数据丢失;另外单独维护一套数据库开销比较大。
c) MPPDB集群数据库系统
MPP把GTM逻辑集成到集群数据库里了(实际是在CN节点来做),数据库的所有事务都会取一个唯一的全局事务ID。MPP实际上把GTM逻辑弱化了,CN节点完全不记GTM的日志,故障时也不做两阶段事务的恢复,DN上残留的两阶段事务是通过定时清理的进程解决的。这样做的优点是轻量化,实现逻辑简单。缺点是DN残留的两阶段事务要等到下次清理进程启动时才能清除,期间一直持有资源,可能会阻塞其它业务。
8.2 GTM恢复时“查一下”的逻辑,其实在有些场景是很难实现的,有没有其它的保障方法?
有些场景下,GTM可能只是一个组件,无权决定残留的预提交事务是提交还是回滚(SDN);还有可能GTM管理的数据库非常多,逐个查询代价较高(MPPDB)等等。
对于MPP的场景,GTM不记录日志,清理进程在发现残留的两阶段事务后,通过全局事务ID向所有DN查询ID是否已提交,如果所有DN都没有该事务ID的提交记录,则说明是残留事务,进行清理(回滚),否则直接提交。这种方法只能对统一全局事务ID的两阶段事务有效。
对于SDN的场景,GTM无法决定残留事务的命运,处理方法是通过上层补发一边指令和定时清理两种方法结合保证两阶段事务无残留和数据完整性。GTM恢复后首先会受到上层补发的一次指令,GTM重新下发给数据库。如果此时又发生了故障,或GTM已不可恢复,则有定时清理进程处理数据库节点的残留事务。清理进程在发现残留的两阶段事务后,会查询GTM的日志(日志记在数据库上,即使GTM节点不可恢复也无妨),根据日志判断该事务是提交还是回滚,进行清理。这种方法要求GTM必须记日志,且日志本身需要一定的可靠性保障。
8.3 2PC和paxos是什么关系?所谓的分布式一致性对应的是事务ACID中的一致性还是原子性?
2PC可以处理的场景其实有两种,一是同一提交事务在多个不同的数据库节点上统一提交,二是不同的提交事务在多个不同的数据库节点上统一提交。在第一种场景下,2PC是paxos算法的一种实现(2PC的要求比paxos严格很多,所以效率也低很多);但对于第二种场景,只有2PC可以保证,paxos完全不是为这类场景服务的。
分布式一致性(consensus)是指不同节点的数据保持一致,而传统事务的ACID中的一致性(consistency)是指外键、唯一性等约束不被破坏,这两者完全不是一码事。而分布式一致性对应分布式事务,其实保证的是其原子性(atomicity),即事务中所有的内容要么被全部执行(包括在全部节点上被执行,不会出现一部分节点执行一部分未执行),要么全部不执行。只是现在事务一致性(consistency)很少被提及,一般所谓的一致性都是指分布式一致性了。