PolarDB-X Operator 基于两次心跳事务的指定时间点恢复方案介绍

简介: 本文将介绍,PolarDB-X Operator将在事务策略为XA事务或者TSO事务时,如何实现全局一致的任意时间点恢复,提出了基于两次心跳事务的恢复方案。

1. 简介


数据库恢复方式有备份集恢复和任意时间点恢复(PITR, point-in-time recovery)。备份集恢复顾名思义是直接使用保存的数据备份集进行恢复,只能恢复到固定某一时刻的数据库状态;任意时间点恢复,利用数据库的数据备份和日志备份,先利用数据备份将数据恢复到某一个时刻的数据库状态,数据备份集中会有一个日志位点,下载从该日志位点到指定时间时候那个日志位点之间的日志进行回放,便能恢复到指定时刻的数据库状态。


单机MySQL,在存在数据备份集和binlog日志的前提下,可以使用XtraBackup工具对备份集作恢复,然后使用mysqlbinlog工具回放binlog日志。PolarDB-X作为一款分布式数据库,存储数据的组件有元数据服务(GMS,Global Metadata Service)节点和数据节点(DN,Data Node),计算节点(CN,Compute Node)是无状态的,PolarDB-X在做指定时间点恢复的时候,大概的想法便是,只需要将GMS和DN都恢复到某个一个时刻。想法本身是正确的,在分布式数据库中,存在分布式事务,分支事务会落在不同的DN上,各个DN如果各自独立恢复,将无法保证分布式事务原子性。


前文 PolarDB-X 是如何拯救误删数据的你(二) 备份恢复 讲解了,在事务策略为TSO事务时,如何做全局一致的任意时间点恢复。本文将介绍,PolarDB-X Operator将在事务策略为XA事务或者TSO事务时,如何实现全局一致的任意时间点恢复,提出了基于两次心跳事务的恢复方案。


2. 方案对比

阅读前面的两篇文章 PolarDB-X 是如何拯救误删数据的你(二) 备份恢复PolarDB-X 2.0 全局 Binlog 和备份恢复能力解读 ,在PolarDB-X已经有两种方案进行任意时间点的恢复,在此,我们称之为基于TSO的任意时间点恢复 和 基于全局Binlog的任意时间点恢复,我们进行如下比较:


方案名称

是否要求业务侧必须使用TSO事务

全量备份集

日志恢复量

是否依赖CDC组件的稳定性

回放效率

是否需要心跳事务

基于TSO的任意时间点恢复

是。TSO事务基于XA事务,增加了全局时间戳,达到可重复读的隔离级别,性能低于XA事务

DN物理备份

较小,全量备份集结束点+增量日志

高,可使用DN上SQL线程回放binlog,各个DN并发回放

基于全局Binlog的任意时间点恢复

DN物理备份或者PolarDB-X逻辑备份

较大,全量备份集开始点+ 增量日志

底,需要三方工具把全局binlog转化为SQL,在CN上执行

基于两次心跳的任意时间点恢复

DN物理备份

较小,全量备份集结束点+ 增量日志

高,可使用DN上SQL线程回放binlog,各个DN并发回放

基于全局Binlog的任意时间点恢复的方案与其他两种方案,有较大差异,对PolarDB-X的内部实现依赖较小,但需要消耗大量计算资源,通常可以用于将PolarDB-X上数据同步到别的系统。

基于两次心跳的任意时间点恢复方案,引入了持续的心跳事务,解决了基于TSO的任意时间点恢复方案里 “要求业务侧必须使用TSO事务”的问题。


3. 方案解读


我们提出一种,基于连续两次心跳事务的全局一致的binlog位点裁剪方案,该方案只要求PolarDB-X的事务策略为XA事务或者TSO事务。

该方案要求,需要有一张广播表,比如建表语句为:

CREATE TABLE `__heartbeat__` (
        `id` bigint(20) NOT NULL AUTO_INCREMENT BY GROUP,
        `sname` varchar(10) DEFAULT NULL,
        `gmt_modified` datetime(3) DEFAULT NULL,
        PRIMARY KEY (`id`)
) ENGINE = InnoDB AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8mb4  broadcast


定时更新这个表上的某条记录(PolarDB-X Operator里的周期是1秒,保证了恢复时间点的误差不超过1秒),可产生分布式的心跳事务,比如:

set drds_transaction_policy='TSO';
replace into binlogcut.`_heartbeat__`(id, sname, gmt_modified) values(1, 'binlogcut', now());


接下来,将解释如何利用两个心跳分布式事务,确定一致性位点。 假设一个是分布式事务Tm , 我们将它在binlog的上Prepare Event标记为Pm , Commit Event标记为Cm ,连续的发生的两个心跳事务为 Tnh和 Tn+1h

我们可得到一下已知信息:


  1. 单个binlog里的event按照时间顺序写入,比如Event 1的文件偏移量大于Event 2的文件偏移量,则表示Event 1发生时间早于Event 2;
  2. 根据XA协议,只有在所有分支事务都Prepare之后,才能执行Commit,所以同一个事务在任何一个DN的binlog都是Prepare Event早于Commit Event
  3. 心跳事务单线程写入到广播表中同一条记录,由于行锁的存在,每个DN binlog上,连续的心跳事务在每个DN binlog的Prepare Event和Commit Event也连续出现,不存在某个binlog的心跳事务Prepare Event和Commit Event中间有其他成功执行的心跳事务的Prepare或者Commit Event。


结合已知信息1 和 已知信息2,如果我们发现 T1 事务和 T2事务,在某个binlog上存在如下事件流: C1P2, 可知事务 T1 的提交时间早于 T2 事务完成Prepare时间,进而得出: 事务 T1 的完成Prepare时间 < 事务T1 的提交时间 < 事务 T2的完成Prepare时间 < 事务 T2 的提交时间, 也就是:


  1. 宏观视角:事务 T1的完成Prepare时间 < 事务 T2的提交时间
  2. 微观视角:每个DN上binlog流上 P1先于 C2


我们尝试用一个心跳事务 T1h的Prepare Event的结束位置的来裁剪每个DN的binlog:

image.png


按上图裁剪后,我们来使用分布式事务 TnpnCn在不同DN上位置,展开分析:


情况1

Tn 是一个在心跳事务 T1h在发起前,完成了提交,PnCn 都落在 P1h 之前。显然当前的裁剪线,不会破坏 Tn 的事务特性;


情况2

Tn在心跳事务发起前,未决定提交,没有下发过一个commit。该情况,我们只需要在恢复后,将悬挂事务回滚即可。


情况3

Tn 在心跳事务发起前,决定提交,下发过一个commit,但是还没有完成全部分支的提交。在某个DN的binlog中存在... Pn ... Cn ... P1h... C1h ...的事件流, 根据上文的推论,我们得出:每个DN的binlog上, Pn先于 C1h  。如果 Pn 都出现在 P1h 之前,我们只需要恢复后,将该悬挂事务提交即可。


问题1 Pn 出现在 P1h 之后:


Pn 可能会出现在如下的位置:

image.png


存在DN3上无法提交事务 Tn 分支事务的问题。 因此需要将DN3上的裁剪位置设置为Pn 事件的结束位置。这样又可能会有新的可提交事务出现 Tm,同样保留它的所有Pm事件,满足: Tm的完成Prepare时间 < Tm 的提交时间 < Tn 的提交时间 < T1h 的提交时间 , 所以 Tm 完成Prepare时间 < T1h 的提交时间,因此 Pm在 C1h

之前。 上面的裁剪裁剪点扩张的过程不会无限持续下去,会在 C1h  之前结束。


问题2 如何确定 Tn 在心跳事务 T1h 前,发送过一个commit?:


如果遍历每个DN在 P1h 位置之前的所有binlog,我们肯定能找到这个commit event。生产环境里,每日产生的binlog数据量巨大,binlog会有有个保存期限,因此遍历所有的binlog是不可行的。

在两阶段提交中的提交阶段,一旦已经决定了提交,发生异常情况,也必须让提交进行下去,而这个决定提交的行为, 在实践中,需要写一条事务日志,记录这个分布式事务已经决定提交,等数据库恢复健康状态后,发现有未提交的分支事务,则去查询该事务日志来判断,最后提交或者回滚该事务。

当某个DN上的 T1h 之前有一个 Cn , 可知,记录事务日志的时间 < 分支事务commit的时间 < 心跳事务完成Prepare时间 < 心跳事务提交时间, 记录事务日志的操作在心跳事务提交之前,裁剪时能做的是,保留它的所有 Pn 及其事务日志,我们需要裁剪到 C1h 。 在完成恢复后,我们可以查询事务日志来判断来决定回滚或者提交该事务。


引入第二次心跳事务


image.png


如上图所示,为了同时解决问题1 和 问题2, 我们引入第二次心跳事务 T2h ,分析每个DN binlog [ P1h

开始位置, C2h 结束位置 ], 将 P2h作为初始的裁剪位置:


1. C1hP2h 之前,所以问题2解决,即commit event出现在 P1h 之前,未出现在 [P1h 开始位置, P2h

的结束位置 ] 区间的 事务可以被正确处理;

2. commit event出现在 [ P1h开始位置, P2h 的结束位置 ] 区间内的事务,可用问题1中迭代方式,在 C2h

之前获得一个最终的裁剪点,迭代过程记录可提交事务ID结合,便于恢复之后提交对应的悬挂事务。

其中,不在记录的待提交悬挂事务列表里的事务,我们查询事务日志判断是否需要提交,剩下的分支则进行回滚。


一致性备份和任意时间点恢复


对PolarDB-X下的DN发起物理备份后,各个DN的物理备份完成时间不同,所以:全局一致的备份集 = 物理备份 + 用于恢复到某个全局一致的状态增量日志。


如何获取增量日志的一致性位点?


有锁的方式:在所有DN完成物理备份后,锁住数据库,使其无法提交事务,记录DN的binlog位点;

无锁的方式:在所有DN完成物理备份后,保留一部分增量日志,可通过任意时间点恢复方式,恢复到全局一致的位置。

有了一致性备份集后,必要时,用户可通过恢复物理备份,然后尽可能少地回放增量日志,快速恢复出一个全局一致的数据库实例,


元数据和数据文件的一致性恢复


此方案,可以保证所有DN都恢复到全局一致的状态,但是GMS上的元数据因为不参与分布式事务协同,如果遇到NTP机器时钟不一致(比如GMS机器节点上时间比DN机器上时间慢一秒钟),可能会出现,恢复出来的GMS元数据认为一张表存在某一个列,但恢复出来的DN缺没有这一列。

总体来讲,在备份过程中有并发的DDL操作、增减DN节点等需要联动修改DN数据和GMS上元数据的操作,都可能导致恢复后的元数据和业务数据不一致。 因此,针对该问题,我们有两种优化方案:

  1. GMS节点,同样采用分布式事务TSO来进行管理,确保基于TSO恢复策略时采用大家一致的时间戳,而不是每个主机的物理时间。
  2. 恢复之后,查询GMS元数据检查在最近一段时间(可以设置为一个变量,10秒或者1分钟)是否有此类操作。如果有,则做出提示并建议用户调整恢复时间来规避此类问题。


4. 总结


本文介绍了如何利用连续两次的分布式心跳事务,结合Prepare Event和Commit Event之间的依赖关系,来获取一致性的binlog裁剪点,恢复的时候将日志恢复到裁剪点,提交裁剪过程中记录的待提交事务,并利用事务日志提交或者回滚剩余的一部分事务。


5. 计划


我们将会再出两篇文章,做演示和源代码解读,尽请期待。




数据库PolarDB-X新人入门一站式页面,快速体验集中分布式一体化新特性!


云原生数据库PolarDB分布式版新人入门

作者介绍
目录

相关产品

  • 云原生分布式数据库 PolarDB-X