Author: saimu
DDL是数据库所有SQL操作中最繁重的一种,本文总结介绍了PolarDB中DDL全链路MDL锁治理的经验和进展,持续优化用户的使用体验,为用户打造最佳的云原生关系型数据库。
在日常数据库操作中,用户总是谈DDL色变,原因在于总是担心DDL的执行会影响业务SQL,这里面最核心的因素在于DDL持有的MDL表锁导致的锁堵塞问题。另一方面,由于DDL类型众多,用户难以区分不同类型DDL的锁行为,无法判断执行DDL可能导致的后果,这进一步加剧了该问题的复杂度。通过多年大量线上实例的经验积累, 我们非常理解用户在面对这类MDL锁问题时的困惑。本文整理总结了PolarDB MySQL内核团队在全链路MDL锁治理这块的经验和进展,鞭策我们为“DDL无锁”、为用户可以毫无担忧地执行DDL而持续努力。针对MDL锁相关的背景知识,我们有持续的内核月报在介绍这方面的原理,感兴趣的读者可以自行查询 常用SQL语句的MDL加锁及源码分析和 MDL锁实现分析。在开始全文前,我们首先回顾用户主要关注哪些方面的DDL锁问题:
很不幸的是,无论是MySQL内核原生的DDL,还是各种第三方插件(gh-ost、pt-osc,以及云厂商们的“无锁变更”),几乎所有的DDL都会申请表级别的MDL互斥锁。这里的核心原因在于:DDL的目标是表结构/表定义变更,它必然会修改元数据/字典信息,因此DDL依赖MDL锁来完成元信息、文件操作和相应缓存信息的正确更新。当DDL修改元数据时,它申请表级别的MDL互斥锁,从而堵塞并发的元数据查询/修改操作,继而可以线程安全地更新元信息缓存,从而保证所有线程用正确版本的元数据解析对应版本的表数据。
说到这里,很多熟悉MySQL的读者一定会问,那为什么gh-ost等第三方插件在做DDL时似乎呈现出一种类似“无锁”的表现呢?其实这里的核心差别在于,MySQL内核和第三方插件,在处理“拿不到锁”这个问题时采用了完全不一样的策略。
相比于第三方插件,MySQL内核的MDL拿锁机制简单粗暴:当DDL申请MDL-X(互斥锁)时,如果目标表存在未提交的长事务或大查询,DDL将持续等待获取MDL-X锁。由于MDL-X锁具有最高的优先级,DDL在等待MDL-X锁的过程中将阻塞目标表上所有的新事务,这将导致业务连接的堆积和阻塞,继而可能带来整个业务系统「雪崩」的严重后果。
为了避免这个问题,MySQL社区开发了很多外部工具,比如pt-osc和github的gh-ost。它们均采用拷表方式实现,即创建一个空的新表,通过select + insert的方式拷贝存量数据,然后通过触发器或者binlog的方式拷贝增量数据,最后通过rename操作切换新表和旧表。云厂商的各种工具,例如DMS的无锁变更也与这些外部工具原理类似。但很遗憾,这种方式也存在明显的劣势:1. 可能由于大事务/大查询的存在,DDL持续拿不到锁,持续等待直到反复失败(「饥饿」);2. 不管是Instant DDL(例如秒级加列),还是仅增加二级索引,第三方工具都无脑选择了全表重建的方式,通过大幅牺牲性能来追求稳定性。我们之前的测试表明(月报链接),相比于内核原生的DDL执行方式(INSTANT / INPLACE / COPY),gh-ost有着10倍甚至几个数量级的性能下降,这在数据量快速增长的今天是完全无法忍受的。
不管是第三方插件,还是MySQL内核,很遗憾,任何一种方式都不能在所有场景里都达到最优。PolarDB-MySQL内核团队尝试在保留最佳性能的前提下,同时解决雪崩和饥饿这两个问题。
在解决了「拿不到锁」的问题后,我们同样要解决「拿到锁后」会有什么问题,即如果互斥锁持有时间过久,同样会导致业务堆积雪崩等问题。熟悉MySQL的用户都知道,MySQL有三种DDL类型,分别是「INSTANT DDL」、「INPLACE DDL」和「COPY DDL」。其中,Online DDL(用户常说的“非锁表”DDL,包括INSTANT DDL和绝大多数INPLACE DDL)在执行DDL期间绝大多数时刻并不锁表,只在修改元数据时短暂持有表的MDL-X锁(持有时间一般秒级),用户体验良好。当前的MySQL 8.0,已经实现了常见高频DDL的Online能力,例如增加索引、秒级加列,加减主键等等。但是,因为涉及一些SQL层的操作,目前依然存在COPY类型的DDL,它在执行DDL期间「全程锁表」(只能读不能写),例如修改表的字符集、修改列类型等操作。针对这类COPY DDL,PolarDB MySQL的解决方案是扩展Online DDL(不锁表)的范围,例如支持Instant Modify Column(秒级修改列类型),例如尝试在SQL层支持所有DDL的Online能力,我们将这类能力统称为「Fast DDL」,笔者后续会统一介绍这方面的工作,本文不再赘述。
相比于MySQL,PolarDB的集群架构使得这一问题变得更加复杂:MDL锁不仅要关注单个节点,更要关注集群多个节点/集群同步链路上的锁问题,需要集群维度的全链路解决方案。熟悉MySQL的用户,对基于Binlog的MySQL主备集群一定非常熟悉。在依赖Binlog的MySQL主备复制集群上,主备节点是逻辑隔离的。也就是说,主节点的MDL锁行为,并不会对备节点的MDL锁有任何影响,因此MySQL只需要考虑单个节点的MDL锁问题。然而,PolarDB MySQL是基于共享存储的架构。以一写多读集群为例,写节点和多个只读节点共享同一个分布式存储,依赖物理复制完成不同节点之间的数据同步。写节点在做DDL操作时,多个只读节点都会看到DDL过程中的实时数据。因此,PolarDB的MDL表锁,是一个集群维度的分布式锁,需要考虑多节点上的锁堵塞问题。
基于PolarDB的架构特征,结合多年的线上运维经验,我们认为从集群维度看,要实现用户体验良好的DDL锁机制,需要达到以下几个目标:
如前文所述,非阻塞DDL(用户文档,月报链接)用于解决因MDL锁堵塞而导致的业务雪崩问题。非阻塞DDL功能采用了和第三方插件(gh-ost、pt-osc)类似的拿锁逻辑:当DDL操作获取MDL锁失败时,拿锁线程会进入短暂的Sleep阶段,接着重新尝试获取MDL锁。通过此种方式,非阻塞DDL保证了DDL执行过程中,业务真正的online。非阻塞DDL目前已经灰度一段时间,受到大量用户的欢迎,后面会尝试默认开启此功能。此外,我们将在8.0.2的2.2.15版本中,支持集群维度的Non-Block DDL:如果主节点已经获取MDL锁,但是只读节点同步MDL锁堵塞(当前默认堵塞时间为50s,由参数loose_replica_lock_wait_timeout控制),Non-Block DDL会在集群维度重试拿锁的操作,从而实现集群维度的非阻塞DDL。
可以通过设置参数loose_polar_nonblock_ddl_mode为ON来打开非阻塞DDL功能(用户文档),下面给出使用sysbench模拟用户业务,对比开启Non-Block DDL功能和使用原生DDL功能对业务的影响。
begin;
select * from sbtest1;
# 由于当前session 1大查询持有MDL锁,当前DDL无法获取MDL锁,被堵塞
alter table sbtest1 add column d int;
TPS持续跌零,默认超时时间为31536000,严重影响用户业务。
TPS周期性下降,但未跌零。对用户业务影响较小,能保证业务系统的稳定。
上文非阻塞DDL解决了DDL获取MDL锁阻塞导致的业务雪崩问题,但是如果DDL迟迟无法获取MDL锁,会导致DDL执行频繁失败。目前线上值班偶尔会遇到由于RO上面存在大查询、长事务导致的DDL执行失败问题,并返回错误ERROR 8007 (HY000): Fail to get MDL on replica during DDL synchronize。由于此报错与PolarDB共享存储的架构相关,与传统MySQL不一致,用户经常会一头雾水,无从下手。当前已有官方文档(执行DDL操作提示“获取不到MDL锁”)介绍这类问题的解决方案,用户可以根据此文档找到只读节点上持有表MDL锁的事务,手动进行Kill,来保证DDL同步MDL锁的成功。但是这种方式在部分场景下依然非常晦涩,一方面用户进行kill操作的时间窗口有限(当前同步MDL锁超时时间为50秒,可通过loose_replica_lock_wait_timeout进行调整),另一方面随着PolarDB上面客户不断增多,出现了许多10+个只读节点的集群,手动kill操作显得狼狈且低效,为此我们提供了抢占式DDL功能。
当只读节点通过物理复制,解析到当前表上有DDL操作时,只读节点会尝试获取表的MDL锁。如果此时表上存在大查询或长事务时,开启Preemptive DDL后(用户文档),如果只读节点在预期时间内无法获得MDL锁,便会尝试kill掉占有MDL锁的线程,从而保证MDL锁同步的成功,解决DDL的饥饿问题。
可以通过设置参数loose_polar_support_mdl_sync_preemption为ON来打开抢占式DDL功能,下面给出DDL同步MDL锁被只读节点长事务堵塞时,开启和关闭抢占式DDL的实验效果。
mysql> use test
Database changed
#大查询,执行100s
mysql> select sleep(100) from t1;
mysql > alter table t1 add column c int;
ERROR 8007 (HY000): Fail to get MDL on replica during DDL synchronize
由于只读节点存在大查询,同步MDL锁失败,DDL执行失败,并回滚。
mysql> use test
Database changed
#大查询,执行100s
mysql> select sleep(100) from t1;
mysql> alter table t1 add column c int;
Query OK, 0 rows affected (11.13 sec)
Records: 0 Duplicates: 0 Warnings: 0
开启抢占式DDL功能后,加列操作完成,同时可以看到只读节点(右图),大查询连接已经断开。
不管是Non-Block DDL还是Preemptive DDL,都是在有互斥锁的场景下,尽可能最优地满足用户的DDL变更需求。然而,用户在部分场景下依然要感知MDL锁的存在,例如在极限场景下,用户依然需要手动触发Preemptive DDL,来解决DDL饥饿的问题。我们一直在探索,是否可以实现DDL与DML更细粒度的并发控制,类似于InnoDB MVCC能力。然而,如前文所述,DDL是个复杂操作,其执行过程涉及文件操作/表数据变更/元信息变更/表缓存处理等一系列流程。因此,考虑到MySQL代码的强耦合性,我们对这一目标做了切分,在控制代码切口和稳定性风险的情况下,逐步支持这一能力。在第一阶段,我们优先支持线上高频DDL与DML的MVCC能力,即按照statement维度,满足Instant Add Column与DML的MVCC能力(用户文档待新版本802上线)。为了兼容MySQL的默认表现,我们不仅支持DDL和未提交事务的并发,而且支持DD的readview,使得跨越了DDL的DML事务可以选择以RC或者RR的隔离级别读取表结构信息,从而让用户自行决定使用新或者旧的表定义。
具体的效果如下:
步骤一:开启会话A,创建一个新的表t1并插入一些数据;随后开启一个新事务,在事务中进行数据的插入和更新操作,但事务不提交:
步骤二(DDL不会被未提交的事务所堵塞):开启一个新的会话B,查询performance_schema,此时t1的MDL正被会话A中未提交的事务持有。进行DDL操作(add column),该操作可以立即完成,而不会被未提交的事务阻塞。
步骤三(跨DDL的事务可以选择访问表时使用的隔离级别):回到第一个会话A,将表访问的隔离级别参数table_def_isolation设置REPEATABLE-READ,因为DDL的执行在该事务之后,因此新增的列c不可见,该事务将始终看到与事务开始时一致的表定义。将table_def_isolation设置为READ-COMMITTED,因为DDL已经提交,列c将对该事务可见
提交事务后,DD的readview随之释放,随后将只能看到最新的表结构。
目前的云原生数据库,不论是PolarDB,或者是Aurora等其它厂商的数据库,都以“存算分离”+“共享存储”的形态提供一写多读的能力。对这类架构感兴趣的读者,可以阅读我们之前的相关月报(PolarDB 物理复制解读,PolarDB 物理复制热点页优化)。对这类针对存算分离场景下IO优化感兴趣的读者,可以阅读我们去年发表在VLDB上的相关论文(CloudJump: Optimizing Cloud Databases for Cloud Storages)。简单来说,云原生数据库依赖物理复制(Redo日志)完成不同节点之间的数据同步,而DDL触发的元数据/表数据/文件变更同样随着物理复制完成多节点的同步,这三者之间依赖分布式MDL锁提供 实时&一致性 的保证。然而,当MDL锁和物理复制相耦合时,会产生一系列的问题,尤其是日志流 / 锁同步 / 文件操作这三者之间的一致性问题。这里,我们介绍与用户密切相关的两类问题:
高频DDL场景下分布式MDL锁的稳定性&实时性。尤其是在MDL锁被堵塞时,不能影响正常物理日志的进行。为了解决这个问题,PolarDB设计了全新的分布式MDL锁机制(用户文档,已默认开启),主要体现在以下两个方面:
高压力DDL场景下物理复制的稳定性&实时性。PolarDB中的数据是通过B-Tree来维护索引的,然而大部分Slow DDL操作(如增加主键或二级索引、Optimize Table等)往往需要重建或新增B-Tree索引,导致大量物理日志的产生。而针对物理日志进行的操作往往出现在DDL执行的关键路径上,增加了DDL操作的执行时间。此外,物理复制技术要求只读节点解析和应用这些新生成的物理日志,DDL操作而产生的大量物理日志可能严重影响只读节点的日志同步进程,甚至导致只读节点不可用等问题。 针对上述问题,PolarDB提供了DDL物理复制优化功能(用户文档,已默认开启),主要体现在以下两个方面:
DDL是PolarDB所有SQL操作中最繁重的一种,DDL的易用性是PolarDB良好使用体验非常重要的一环。本文总结介绍了PolarDB 在全链路MDL锁治理的经验和进展,把简单留给客户,把复杂留给自己,持续优化用户的使用体验。后续将总结介绍PolarDB在Fast DDL方面的工作,PolarDB内核团队将始终如一地为用户打造最佳的云原生关系型数据库。