gh-ost 无锁变更
摘要
在我工作的实际项目中,参与了一个 DMS 系统,主要有三个模块,分别是数据变更、SQL审核和SQL控制台。而其中的 数据变更模块底层依赖的就是 gh-ost 的开源组件,通过该组件对大表进行 online DDL 操作。
这也是我写这篇文章的一个契机。那让我们接着往下看吧。
什么是 DDL
DDL全称:Data Definition Language。它包含三个主要的关键字: CREATE、DROP、ALTER。
操作 | Statement |
---|---|
创建数据库 | create database |
删除数据库 | drop database |
创建表 | create table |
删除表 | drop table |
修改表 | alter table |
创建索引 | create index |
删除索引 | drop index |
表级锁和元数据锁
MySQL的表级锁有两种,一种是表锁,一种是元数据锁MDL。
表级锁
表级锁其实相当鸡肋,只是会在一些特殊情况下用到(比如系统崩溃恢复时),尽量避免在 InnoDB 引擎的表上使用 LOCK TABLES
这样的锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。
- LOCK TABLES t READ
- LOCK TABLES t WRITE
释放时机: 通过 UNLOCK TABLES
主动释放,当客户端断开时也会自动释放。
元数据锁
元数据锁也是一种表级锁:metadata lock。
作用:
- 保证读写的正确性
说白了就是:当有用户对表执行DML相关操作时,其他线程不能把表结构给改了(想改表结构也可以,等排在它前面的DML全部执行完)。反之,当有线程在更改表结构时,其他线程需要执行的DML也会被阻塞住。
特性:
- 系统默认添加
- 读锁之间不互斥
- 读写锁之间互斥
什么是无损 DDL
一般对公司对业务线来说,总是难免遇到需要修改线上表结构的需求。比如搞个活动,结果发现:现有的表中的列不够用了,那么就需要对现有的表进行无损DDL操作,添加一列。
有损 DDL
为什么直接执行 alter talbe add column
是有损的呢?因为我们知道,当执行 alter
的时候,是需要获取元数据锁(MDL)的写锁的, 而所有的 DML 操作又会被默认的加上元数据读锁。那么就会造成执行 DDL 的期间,后续所有 DML 都会阻塞。我们称这种情况对业务来说是有损的。
无损 DDL
所谓的无损是相对于业务来说的,如果能做到执行 DDL 的过程中,对业务无影响,那我们称这种 DDL 是无损的。
什么是重建表
什么是重建表?为什么需要重建表?
当我执行delete语句删除表A中的数据时,对应Innodb来说其实只是在标记删除,而是不实实在在的将表空间中的数据删除,对应innodb来讲被标记删除的位置是可以可重复使用。
那么delete语句多了,表空间上的空洞就多了,磁盘的占用量也只增不减。这时我们就得重建表。缩小表A上的空洞。
MySQL5.5 之前重建表
在 5.5 之前,mysql执行 alter table A engine=InnoDB
的流程如下图:
在上面的过程中,MySQL会自动的为我们创建临时表,拷贝数据,交换表名,以及删除旧表。
特点:
- 这个过程并不是安全的。因为在往tmp表中写数据的过程中,如果有业务流量写入表A,而且写入的位置是不久前完成往tmp中拷贝的位置,就会导致数据的丢失。
- 即使是MySQL会我们自动的创建临时表,数据拷贝的过程依然是在 MySQL-Server 层面做的。
MySQL5.6 之后重建表
重建表的过程如下图:
1、创建一个tmp_file, 扫描表A主键的所有数据页。
2、使用数据页中的记录生成B+树,存储进tmp_file中。
这一步是对针对数据文件的操作。由innodb直接完成。
3、在生成转存B+数的过程中,将针对A的写操作记录在row_log日志中。
4、完成了B+树的转存后,将row_log中记录的日志在tmp_file中回放。
5、使用临时文件替换A中的数据文件。
可以看到,这个过程其实已经实现无损了。因为在做数据迁移的过程中,允许对原表进行CRUD。
局限性:
- 这种DDL(
alter table A engine=innodb
)本质上是在替换表空间中的数据文件,仅仅是用于对原表进行无损DDL瘦身。而不是解决我们开题所说的动态无损加列的情况。
什么是 COPY|INPLACE | INSTANCE 算法
MySQL 的 DDL 有很多种方法。
- copy 算法为最古老的算法,在 MySQL 5.5 及以下为默认算法。
- 从 MySQL 5.6 开始,引入了 inplace 算法并且默认使用。inplace 算法还包含两种类型:rebuild-table 和 not-rebuild-table。MySQL 使用 inplace 算法时,会自动判断,能使用 not-rebuild-table 的情况下会尽量使用,不能的时候才会使用 rebuild-table。当 DDL 涉及到主键和全文索引相关的操作时,无法使用 not-rebuild-table,必须使用 rebuild-table。其他情况下都会使用 not-rebuild-table。
- 从 MySQL 8.0.12 开始,引入了 instant 算法并且默认使用。目前 instant 算法只支持增加列等少量 DDL 类型的操作,其他类型仍然会默认使用 inplace。
开源在线变更工具
目前来说,开源变更工具按照实现方式来说,主要有两类,一类为触发器模式,最典型的比如 pt-osc, 另外一类为订阅binlog模式,最典型的比如 gh-ost。下面来分别说一下两者的实现方式和特点对比。
pt-osc 的变更流程
pt-osc 的具体变更步骤如下:
- 创建一个与原表结构相同的空表,表名是 _new 后缀。
- 修改创建的空表的表结构。
- 在原表上加三个触发器: delete/update/insert,用于 copy 数据过程中,将原表中要执行的语句在新表中执行。
- 将原表数据以数据块(chunk)的形式 copy 到新表。
- rename 原表为 old 表,并把新表 rename 为原表名,然后删除旧表。
- 删除触发器。
由于 pt-osc 工具较老,并且基于触发器同步数据,负载不可控,并且不能暂停等原因,所以现在一般不推荐使用,这里不再进行详细的阐述。
gh-ost 的变更流程
gh-ost 有三个变更模式,分别为:
-
主从模式(默认模式)
- ghost连接上主库
- 从主库中读取数据rowCopy主库上的影子表中
- 添加对从库binlog的监听。将binlog-event转换成sql应用在主库上的影子表上
- cutOver在主库上完成
-
主主模式
- ghost会连接上主库
- 从主库中读取数据rowCopy主库上的影子表中
- 添加对主库binlog的监听。将binlog-event转换成sql应用在主库上的影子表上
- cutOver在主库上完成
-
测试模式(主要用于测试流程)
- ghost会连接上从库
- 从从库中读取数据rowCopy从库上的影子表中
- 添加对从库binlog的监听。将binlog-event转换成sql应用在从库上的影子表上
- cutOver在从库上完成
上面所说的主主、主从、并不是MySQL实例的主从关系。说的是binlog的同步是在谁身上进行。
至于 gh-ost 涉及到的更详细的细节,可以通过官方文档找到答案,包括并不限于:
- 使用 gh-ost 必须要满足的前提条件?
- 变更过程中如何保证数据一致性?
- cut-over 如何保证原子切换?
pt-osc 和 gh-ost 的优缺点比较
参考阿里云无锁变更
对比项 | 有触发器 | 无触发器 |
---|---|---|
数据库开销 | 触发器是一个存储过程,随着业务的DML,触发器的执行必然存在开销,业务繁忙时更甚。 | 触发器开销占用的问题不存在。作为一个伪装的SLAVE订阅主、备的Binlog事件,将其中的源表事件过滤下来并回放到目标表。这个过程和源表的变更没有任何关系,也不需要数据库上任何存储过程等干涉这个写入。 |
锁 | 触发器将两张表的操作关联到一个事务空间中,所以锁的竞争会增加,即一个事务中的两张表锁并集。触发器的设计中拷贝数据和变更数据只能并行,无疑将会增加锁竞争。 | 无触发器解耦了源表和目标表的依赖,所以锁竞争也就不复存在。关于目标表上的拷贝和更新时的竞争,我们在逻辑上使用交叉执行的方式避免和降低锁竞争,虽然会影响变更效率,但是很显然降低了数据库负载。 |
异常处理 | 触发器的设计,意味着触发器永远保持运行无法暂停。当服务器繁忙、主备延迟、异常等情况时,在变更流程中的任何一个阶段都无法取消触发器,强行取消将导致变更中断或数据丢失,从而导致A_gst表数据不准确。 | 订阅Binlog的线程随时可以暂停或者放慢速度,在系统繁忙和主备延迟较大时对工作中的应用开启节流,避免问题扩大。 |
可靠性验证 | 在验证方案上我们期望得到任务的预期时间等信息,在备库上创建触发器并模拟,前提需要在Statement模式下。ROW模式下无法模拟,因为在主库上的触发器产生的数据效果重放到了备库上。另外,即使是Statement模式,MySQL的回放是单线程的,Statement的单线程执行无法模拟、复现主库上的并发场景,也就无法验证和测试并发和锁相关的问题。 | 基于Binlog在主库和备库上操作Online没有任何区别,避免对线上业务的干扰或资源争用。另外,通过在备库上模拟操作变更,实际并不切换源表和目标表,可以对源表和目标做校验来持续验证可靠性。 |
代码复杂性 | 主要依赖触发器的同步和数据库内部操作,工具的作用相对较小。 | 非触发器的设计基于Binlog,有很大的自由度,但是复杂度会大幅增加。需要注册为一个SLAVE、订阅事件并转为SQL重新写入,异常处理相对简单的如处理连接失败、复制延迟以及数据类型等,其他程序的异常诸如程序负载、不可控异常等都要在代码上进行关注。同时逻辑中需要包含大量的代码以及更复杂的并发控制逻辑。 |
网络流量 | 触发器在数据库的内部处理数据。 | 非触发器需要订阅事件流以及回写数据,这将使用到主机间的流量,占用MySQL的进程流量。代码的复杂性依赖缜密的算法逻辑,完善的测试用例集来保证健壮性和稳定性。但是相比之下,它带来了更多的好处,比如可以指定时间切表、拷贝或者增量流量控制等一些额外的功能。 |
总结
在百度数据库DMS产品的具体实现中,我们同样使用了 gh-ost 进行大表变更,因为 gh-ost 支持暂停、终止等基于负载的控制操作,对变更过程更加可控,同时也便于集成到管控平台上开放给普通用户使用。
但是由于 gh-ost 是单协程进行 binlog event 的处理,当负载非常高时,会导致 gh-ost 处理 binlog 的速度跟不上生成增量 binlog 的速度,从而一直无法完成变更。在这种情况下,采用 pt-osc 这种基于触发器的实现,由于可以进行并行回放,所以效率相对较高,相应的性能对比测试可以参考这里。