数据库内核月报

数据库内核月报 - 2025 / 10

RDS MySQL:DDL实时应用功能

Author: 晔之

MySQL使用的是逻辑复制,一个事务在主库执行结束后,会发送事务产生的Binlog event到备库应用,进而保证主备的一致性。

在这种架构下,对于耗时长的DDL,会导致备库有显著的复制延迟。

逻辑复制的复制延迟可以大略分为两块:Binlog event传输时间 + 备库事务回放时间。对于DDL,仅产生一个Gtid_log_event和一个Query_log_event,传输量少,Binlog event传输时间可以忽略不计,所以DDL复制延迟的主因就是执行DDL的长耗时。

image.png

对于这一问题,MySQL实现了Instant DDL,并行DDL功能等来降低DDL的执行耗时,但是这些功能无法覆盖全部类型的DDL,因此DDL导致的复制延迟还是广泛存在。

针对这一问题,MariaDB在10.8提出了一种解决方案,见往期月报《无DDL延迟的主备复制》。但是因为该方案有诸如:对binlog有改造,从而会对消费binlog的下游生态产生影响;和备库现有worker线程过度耦合导致扩展性差;线程调度复杂,在worker线程繁忙时做不到无延迟等问题;我们并没有直接采纳。而是在此基础上,提出了一套全新的方案。

DDL实时应用介绍

通过上面的分析,我们可以看到DDL导致复制延迟的根因是MySQL现有架构要求必须在主库提交DDL后,备库才能应用。因此解决这个问题的直观想法就是让备库不用等待主库,而是和主库并行执行DDL。

DDL实时应用功能的核心,也正是如此。我们在主库开始执行DDL时,即通知备库开始执行DDL;备库在完成DDL的主要工作后,等待主库的执行结果;主库如果执行成功,则通知备库提交DDL,主库如果执行失败,则通知备库回滚DDL。最终如下图所示,DDL导致的复制延迟被减少为通知备库所需的一次网络传输耗时+DDL提交的耗时,此两步耗时极低,一般最高为十毫秒级。

image.png

接下来将对该功能的实现做进一步介绍。下文用BRR(binlog realtime replication)来指代该功能。

BRR 架构

BRR的整体架构如图。按时间线,以Online DDL为例,主备执行的操作如下:

  1. 主库在DDL开始执行,获取MDL X锁后,即向一个Buffer中写入Brr binlog event,然后通过实时传输功能(具体介绍见往期月报《用了RDS MySQL,我的半同步复制终于不超时了!》)传到备库。

  2. 备库的IO线程接收Brr binlog event,存储在Relay log cache中。

  3. 备库的Brr worker读取Brr binlog event,并执行DDL,达到主备同时执行DDL的效果。

  4. 主库DDL执行结束,向Buffer中写入Brr binlog event(即传递DDL执行结果),传到备库。

  5. 备库的Brr worker,读取Brr binlog event,根据主库的执行结果提交或回滚DDL。

  6. 主库向Binlog中写入Binlog event,传到备库。

  7. 备库的IO线程接收Binlog event,存储在Relay log中。

  8. 备库SQL线程读取Relay log,分配DDL给Woker线程应用,worker线程根据gtid跳过该DDL。

image.png

主库

主库创建了Brr binlog event(新类型的binlog event),通过实时传输功能传到备库。Brr binlog event不会记录Binlog,备库接收到Brr binlog event后,将其写入了Relay log cache而非Relay log。

MySQL原逻辑中DDL执行产生的Gtid_log_event和Query_log_event,会被正常记录Binlog,传到备库,写入Relay log。

BRR对Binlog和Relay log的内容没有做改变,对消费binlog的下游生态不会有影响。

备库

备库创建了一组Brr worker线程来应用Brr binlog event,从而实现DDL实时应用。

BRR回放时机

MySQL备库应用事务需要满足一定的先后顺序,这也就是所谓的回放时机。

如下图所示,对于Online DDL,在主库Trx 1 和 Trx 2 使用老表结构,Trx 3 使用新表结构。Online DDL在执行阶段,允许并发的DML,但在提交阶段会进行锁升级,禁止DML执行,因此备库在提交DDL前必须等待Trx1和Trx2先提交。

image.png

对于Copy DDL,在主库Trx 1 和 Trx 2 使用老表结构,Trx 3 使用新表结构。因为Copy DDL全程持有X锁,不允许并发的DML,所以备库在执行Copy DDL前,必须等待Trx 1和Trx 2先提交。

image.png

MySQL 原逻辑是通过逻辑时间(logic clock)的方式来确认备库的回放时机,而逻辑时间是在事务提交时产生的,BRR的核心是要主备同时执行DDL,因此逻辑时间对BRR并不适用。

在MySQL原逻辑中事务有唯一的标识即gtid,当前server执行的所有事务的gtid会被记录在gtid_executed变量中,所以gtid_executed值天然可以反映事务的依赖性。gtid_executed介绍见官网文档

因此我们通过获取gtid_executed的值写入Brr binlog event,从而帮助BRR worker确认回放时机。

由上文分析可知,Copy DDL需要在执行前等待,Online DDL需要在提交前等待。因此我们在主库执行DDL,获取MDL X锁后,将此时主库的gtid_executed值,写入Brr binlog event,作为BRR worker执行DDL的回放时机判断依据;在主库DDL提交阶段,再次获取此时主库的gtid_executed值,作为BRR worker提交DDL的回放时机判断依据。

BRR worker在执行或提交DDL前,会等待直到当前备库的gtid_executed值,是Brr binlog event所携带的gtid_executed值的超集(代表该DDL执行和提交所依赖的事务在备库都已经被执行),才会进行执行或提交。

Worker线程跳过DDL

在主库章节中,我们已经介绍过,主库除了发送Brr binlog event外,还会发送原有的Binlog event,发送顺序是先发送Brr binlog event,再发送Binlog event,因此Brr worker一定先于Worker线程获得该DDL。

Brr worker在应用DDL时,会获得该DDL的gtid所有权,Brr worker在提交DDL时,会将gtid添加到gtid_executed中,释放gtid的所有权。

Worker线程应用DDL前会检查该DDL的gtid是否被其他线程拥有,如果被拥有则等待;该gtid被释放后,结束等待,Worker线程会检查该gtid是否在gtid_executed中,如果在,则跳过该事务。这部分逻辑是MySQL原有的逻辑,实际运维中,DBA也往往会通过在备库执行set gtid_next=’xxx’; begin; commit; set gtid_next=AUTOMATIC这样的方式让备库跳过某事务。BRR在这里的实现算是异曲同工。

image.png

测试效果

测试效果如下图所示,本测试使用sysbench构造包含80000000行记录,大小23G的单表,然后对该表做重建表操作。未开启BRR时,备库有持续242s的复制延迟,开启BRR时,仅有持续为1s的延迟尖刺。该延迟尖刺的产生系MySQL的一个官方bug,可以忽略。

image.png

BRR功能基本完全消除了DDL产生的复制延迟,保证了高可用,同时可以提供更低延迟的读扩展。本功能已在RDS MySQL 8.0 20250731版本灰度开放,欢迎试用。