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 有三个变更模式,分别为:

  1. 主从模式(默认模式)

    • ghost连接上主库
    • 从主库中读取数据rowCopy主库上的影子表中
    • 添加对从库binlog的监听。将binlog-event转换成sql应用在主库上的影子表上
    • cutOver在主库上完成
  2. 主主模式

    • ghost会连接上主库
    • 从主库中读取数据rowCopy主库上的影子表中
    • 添加对主库binlog的监听。将binlog-event转换成sql应用在主库上的影子表上
    • cutOver在主库上完成
  3. 测试模式(主要用于测试流程)

    • 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 这种基于触发器的实现,由于可以进行并行回放,所以效率相对较高,相应的性能对比测试可以参考这里

参考资料

  1. https://github.com/github/gh-ost
  2. https://www.cnblogs.com/ZhuChangwu/p/13381733.html

posted on 2023-06-30 22:14  爱笑的张飞  阅读(202)  评论(0编辑  收藏  举报

导航