OnlineDDL概述
锁表变更对业务甚至是致命的,DDL无锁变更通过引入非触发器的方式可以解决大表无锁变更这个难题。它并不完美,但是带来的好处是显而易见的。
[TOC]
背景信息
在MySQL5.6之前还没有OnlineDDL解决方案时,大部分表的ALTER操作都要涉及到如下过程,表越大锁表时间越长,所以在线上业务修改一张表是非常困难的。
Lock > copytonewtable > rename > unlock
从MySQL5.6开始,官方提供了Innodb-OnlineDDL的能力可以实现大部分的ALTER操作不锁表,但依然无法覆盖例如修改列的类型、修改列的长度与修改字符集等操作。
OnlineDDL变更方案对比
如下三种工具在alter表时不需要锁定表,所以不会阻塞读写,使用外部干预的方式完成表的变更。
pt-online-schema-change
工作原理:
- 创建和源表xx一样的中间表xx_gst,执行DDL操作,同时在xx上创建一个DML触发器
- 将xx中的数据拷贝到xx_gst
- 在拷贝过程中产生的增量变更,就用触发器完成同步更新
- 拷贝结束后,执行两张表的rename完成变更
OnlineSchemaChange
工作原理和pt-online-schema-change基本一致,不同的地方是:
- 它在xx_gst的基础上创建了一张日志表,触发器的条目更新将直接落在日志表中
- 后台进程将日志表中的条目应用到xx_gst表
这样整个流程上是异步的,也能够控制回放速度。
gh-ost
与上面两种变更流程基本一致,但是没有使用触发器的设计,所以增量变更的数据来源不是触发器,而是Binlog文件。订阅读取该文件中xx表的变更记录,将记录解析并应用到xx_gst表。这样的数据对于xx_gst表回放非常有利,binlog中存储的都是xx表的记录,易于直接读取和应用。
gh-ost 作为一个伪装的备库,可以从主库/备库上拉取 binlog,过滤之后重新应用到主库上去,相当于主库上的增量操作通过 binlog 又应用回主库本身,不过是应用在幽灵表上。
- 检查数据库实例的基础信息,比如是否可连通、验证权限、row格式、是否innodb引擎等
- 模拟slave,作为一个备库连接到其中一个真正的备库或者主库上(根据具体的参数来定),获取当前的点位信息,创建binlog streamer监听binlog
17760 Query show /* gh-ost readCurrentBinlogCoordinates */ master status 17762 Connect root@127.0.0.1 on using TCP/IP 17762 Query SHOW GLOBAL VARIABLES LIKE 'BINLOG_CHECKSUM' 17762 Query SET @master_binlog_checksum='NONE' 17762 Binlog Dump Log: 'mysql-bin.000005' Pos: 795282
- 创建日志记录表 _xx_ghc 和影子表 _xx_gho,并且执行alter语句将影子表 变更为目标表结构,gh-ost会将核心步骤记录到 _b_ghc 中
17760 Query create /* gh-ost */ table `test`.`_xx_ghc` (...) 17760 Query create /* gh-ost */ table `test`.`_xx_gho` like `test`.`xx` 17760 Query alter /* gh-ost */ table `test`.`_xx_gho` engine=innodb 17760 Query insert /* gh-ost */ into `test`.`_xx_ghc`(id, hint, value) values(...)
- insert into xx_gho select * from xx 拷贝数据
- 获取当前的最大主键和最小主键,然后根据命令行传参 chunk 获取数据 insert到影子表里面。
- rowcopy过程中是对原表加上 lock in share mode,防止数据在copy的过程中被修改。
insert /* gh-ost `test`.`xx` */ ignore into `test`.`_xx_gho` (...) (select ... from `test`.`xx` force index (`PRIMARY`) where (`id` > _binary'1') and (`id` < _binary'21') lock in share mode;
- 拉去增量数据的binlog,解析到insert/update/delete语句就对应转换为replace into/update/delete语句,把binlog应用回主库。
- copy完数据之后进行原表和影子表cut-over 切换,锁住主库的源表,等待 binlog 应用完毕,然后替换影子表为源表。
gh-ost的cut-over切换是原子操作,基本是通过两个会话的操作来完成。
gh-ost 在执行中,会在原本的 binlog event 里面增加 hint 和心跳包,用来控制整个流程的进度,检测状态等。当然 gh-ost 也会做很多前置的校验检查,比如 binlog_format ,表的主键和唯一键,是否有外键等等这种架构带来诸多好处。
触发器存在的问题
基于触发器设计的工具代码逻辑相对简单,大部分数据上的工作交给了触发器去完成,包含数据库的隐式处理、数据类型以及切换等相关操作都在触发器的单元中完成,简化了进行实时表迁移的大量流程。但同样的存在以下几个问题:
1.数据库开销
触发器是一个存储过程,随着业务的DML,触发器的执行必然存在开销,业务繁忙时更甚。
2.锁
触发器将两张表的操作关联到一个事务空间中,所以锁的竞争会增加,即一个事务中的两张表锁并集。触发器的设计中拷贝数据和变更数据只能并行,无疑将会增加锁竞争。
3.异常处理
触发器的设计,意味着触发器永远保持运行无法暂停。当服务器繁忙、主备延迟、异常等情况时,在变更流程中的任何一个阶段都无法取消触发器,强行取消将导致变更中断或数据丢失,从而导致xx_gst表数据不准确。虽然上述两个产品均有限流的概念,但是基于触发器设计的节流只是部分性的,比如在拷贝表阶段,可以暂停拷贝或减缓拷贝速度,但是触发器仍在运行并占用开销。
4.可靠性测试
在验证方案上我们期望得到任务的预期时间等信息,在备库上创建触发器并模拟,前提需要在statement模式下。ROW模式下无法模拟,因为在主库上的触发器产生的数据效果重放到了备库上。另外一方面,即使是statement模式,MySQL的回放是单线程的,statement的单线程执行无法模拟、复现主库上的并发场景,也就无法验证和测试并发和锁相关的问题。
无触发器设计
无触发器设计最大的优点就是和数据库的工作负载解耦。触发器的设计无论何种情况下,源表的DML都会同时在另外一张表上同步操作。非触发器设计将这个过程解耦,即新表的写入和源表的写入不存在直接依赖。这样的流程会完美解决触发器设计带来的几个问题:
1.数据库开销
作为一个伪装的SLAVE订阅主、备的Binlog事件,将其中的源表事件过滤下来并回放到目标表。这个过程和源表的变更没有任何关系,也不需要数据库上任何存储过程等干涉这个写入,触发器开销占用的问题不存在。
2.锁
上面提到方案解耦了源表和目标表的依赖,所以锁竞争也就不复存在。关于目标表上的拷贝和更新时的竞争,我们在逻辑上使用交叉执行的方式避免和降低锁竞争,虽然会影响变更效率,但是很显然降低了数据库负载。
3.异常处理
仍然是解耦带来的好处,订阅Binlog的线程随时可以暂停或者放慢速度,在系统繁忙和主备延迟较大时对工作中的应用开启节流,避免问题扩大。
4.可靠性
基于Binlog的另外一个好处是,在主库和备库上操作Online没有任何区别。这样避免对线上业务的干扰或资源争用。另外一方面,通过在备库上模拟操作变更,最终并不实际切换源表和目标表,然后对源表和目标做校验来持续验证可靠性。
当然,没有所有场景下的完美方案,无触发器的设计存在下面的问题:
1.代码复杂性
在触发器设计的描述中提到,它主要依赖触发器的同步和数据库内部操作,工具的作用相对较小。非触发器的设计基于binlog,有很大的自由度,但是复杂度会大幅增加。需要注册为一个SLAVE、订阅事件并转为SQL重新写入,异常处理相对简单的如处理连接失败、复制延迟以及数据类型等,其他程序的异常诸如程序负载、不可控异常等都要在代码上进行关注。同时逻辑中需要包含大量的代码以及更复杂的并发控制逻辑。
2.网络流量
相比触发器在数据库的内部处理,非触发器方案需要订阅事件流以及回写数据,这将使用到主机间的流量,占用MySQL的进程流量。代码的复杂性依赖缜密的算法逻辑,完善的测试用例集来保证健壮性和稳定性。但是相比之下,它带来了更多的好处,比如可以指定时间切表、拷贝或者增量流量控制等一些额外的功能。
无触发器设计方案对比
关于DMS与同样采用无触发器的gh-ost工具的优劣势对比信息,请参见DDL无锁变更的优势。