本文翻译自: gh-ost: GitHub’s online schema migration tool for MySQL 。已取得原作者 Shlomi Noach 授权。
今天 Github 正式宣布以开源的方式发布 gh-ost :GitHub 的 MySQL 无触发器在线更改表定义工具!
gh-ost 是 GitHub 最近几个月开发出来的,目的是解决一个经常碰到的问题:不断变化的产品需求会不断要求更改 MySQL 表结构。gh-ost 通过一种影响小、可控制、可审计、操作简单的方案来改变线上表结构。
MySQL 表定义修改是个众所周知的难题,从 2009 年开始大家都通过在线表定义修改工具来解决。快速增长和变化的业务需求经常会要求更改数据库表结构。增加、改变、删除字段和索引等操作在默认情况下都会堵塞住数据操作。Github 生产系统每天都会改好几张表定义,非常希望能把对用户的影响减少到最小。
在介绍 gh-ost 之前,请先了解一下各种现有方案,以及为什么要自己开发一个新工具。
已有的在线修改表定义方案
目前,在线修改表定义的任务主要是通过这三种途径完成的:
- 在从库上修改表定义,修改之后再提升为新的主库。
- 通过 MySQL 的 InnoDB 在线 DDL 功能。
- 使用修改表定义工具。现在最流行的是 Percona 公司的 pt-online-schema-change 和 Facebook 的 OSC ,也有人使用 LHM 或最早的 oak-online-alter-table 。
还有其它的比如 Galera Cluster 的 Rolling Schema Upgrade,或者非 InnoDB 引擎的表等。GitHub 的 MySQL 数据库用的都是主从复制架构,使用可靠的 InnoDB 引擎。
为什么 Github 决定去设计一个新解决方案,而不是直接从上面的几种方案中选一个用?现有的解决方案都有着自身的局限性,下面就对它们的不足之处做个简单分析,主要深入地分析基于触发器的在线修改表定义工具的不足之处。
- 在从库上修改表定义的方案需要付出许多运维代价,这需要更多的服务器、更长的完成时间和更复杂的管理工作。修改操作是直接应用在具体的某个从库或者整个拓扑架构的一些子树上。服务器宕机、从库数据不够新、新部署的服务器等各种问题都需要有非常严密的跟踪系统来跟进单个数据库上的操作。一个改变操作可能会需要多次反复,也就需要更长时间。而把一个从库升为主库也会导致短暂的停服。如果同时需要做多个更改就更难协调。由于每天都要改好几张表,所以在考虑解决方案时不希望有这样的管理开销。
- MySQL 的 InnoDB 在线 DDL 只能是在你敲命令的那个 MySQL 上才是“在线”修改的。二进制文件中的日志把修改操作序列化了,从库应用日志时会导致复制延迟。但如果尝试在每个从库上挨个去改的话又会导致上面分析的管理代价。而且 DDL 还是不可中断的,要是在修改时把操作杀掉的话还需要更长的时间去回滚,甚至导致数据字典崩溃。这种方案也不“友好”,在系统负载高时也不能限速或者暂停。这样的操作还有可能会耗尽你的系统资源。
- Github 用了 pt-online-schema-change 好几年了。可是,当生产系统数据增多、业务压力增大之后就碰到了越来越多的问题,甚至到了许多修改操作都被认为是“危险操作”的地步。有一些操作只敢在非业务高峰期或者周末才敢执行,其它的总是会导致 MySQL 停止服务。所有现有的在线修改表定义工具都是用 MySQL 触发器来迁移数据的,因此本身就存在着一些问题。
基于触发器的解决方案有什么不好?
所有在线修改表定义的工具运行原理都是相似的:创建一张与原始表定义相同的临时表,趁上面没有数据时先改好表定义,然后慢慢地、用增量方式把数据从原始表拷到临时表,同时不断的把进行中的原始表上的数据操作(所有应用在原始表上的插入、删除、更新操作)也应用过来。当工具把所有数据都拷贝完毕,两边数据同步了之后,它就用这张临时表来替代原始表。修改过程就结束了。
象 pt-online-schema-change、LHM 和 oak-online-alter-table 这些工具用的都是同步复制的方式,对表的每一条数据修改都会立刻在同一个事务里就应用到临时表上。Facebook 的工具用的则是异步模式,先把修改操作都记在一张修改日志表里,然后再取出来执行,把修改操作应用到临时表上。这些工具全都使用触发器来提取那些应用在目标表上的操作。
触发器都是存储过程,在表上有插入、删除、修改操作时就会被触发。触发器可能包括好多条语句,这些语句都是和引发触发器的那条操作在相同的事务空间内运行的,因此保证了这些操作的原子性。
一般意义上的触发器,尤其是基于触发器的表定义修改操作,都有如下问题:
- 触发器就是存储过程,都是解释型代码,MySQL 不会做预编译。把它们硬嵌入到业务操作的事务空间中,会给你要修改的表上执行的每条操作都增加命令分析和解释的开销。
- 锁:触发器与操作语句分享相同的事务空间,当操作语句释放了原始表上的锁之后,触发器再去释放另一张表上的锁。在同步模式下这样行为的后果尤其严重。主库上的锁竞争与写并发有直接关系。Github 在生产环境中曾经遇到过锁竞争导致的几乎乃至完全锁住的情况,完全无法访问表或者整个数据库。触发器导致的另一种锁是在创建或销毁触发器时对元数据的锁。在完成修改表定义之后从比较忙的表上删除触发器时,甚至曾经碰到几十秒甚至几分钟无法提供服务的情况。
- 无法暂停:当主库业务负载开始增高时,你可能会想要暂停或者取消还没完成的修改表定义的任务。可是基于触发器的方案没办法这么做。也许你可以暂停行拷贝的操作,但却不能暂停触发器,因为把触发器停掉会导致临时表中丢数据。所以,在整个过程中触发器都必须一直处于工作状态。在一些繁忙的服务器上,曾经出现过即使把在线操作全停掉,最后主库还是被触发器给拖死的情况。
- 并发修改:大家都希望能同时修改多张表的定义。考虑到上面分析的触发器的代价,在生产系统中 Github 并不敢以触发器的模式同时修改多张表的定义,事实上也没听说有哪家公司真的在线上这么干。
- 测试:大家也许想测试一下修改方案是否可行,评估一下负载。基于触发器的方案只能在从库上通过基于语句的复制来模拟一下,由于从库上的复制操作是单线程的(即使用了多线程复制的方案,大部分情况下也还是这样的),这样远不能模拟出在主库上修改过程中的真实情况。
gh-ost
gh-ost 是 gitHub’s Online Schema Transmogrifier/Transfigurator/Transformer/Thingy 的缩写,意思是 GitHub 的在线表定义转换器。
gh-ost 有以下特点:
- 无触发器
- 轻量级
- 可暂停
- 动态可控
- 可审计
- 可测试
- 可靠性高
无触发器
gh-ost 不使用触发器,它跟踪二进制日志文件,在对原始表的修改提交之后,用异步方式把这修改内容应用到临时表中去。
gh-ost 希望二进制文件使用基于行的日志格式,但这并不表示如果主库上使用的是基于语句的日志格式,就不能用它来在线修改表定义了。事实上,Github 常用的方式是用一个从库把日志的语句模式转成行模式,再从这个从库上去读日志。搭一个这样的从库并不复杂。
轻量级
因为不需要使用触发器,gh-ost 把修改表定义的负载和正常的业务负载解耦开了。它不需要考虑被修改的表上的并发操作和竞争等,这些在二进制日志中都被序列化了,gh-ost 只操作临时表,完全与原始表不相干。事实上,gh-ost 也把行拷贝的写操作与二进制日志的写操作序列化了,这样,对主库来说只是有一条连接在顺序的向临时表中不断写入数据,这样的行为与常见的 ETL 相当不同。
可暂停
因为所有写操作都是 gh-ost 生成的,而读取二进制文件本身就是一个异步操作,所以在暂停时,gh-ost 是完全可以把所有对主库的写操作全都暂停的。暂停就意味着对主库没有写入和更新。不过 gh-ost 也有一张内部状态跟踪表,即使在暂停状态下也会向那张表中不断写入心跳信息,写入量可以忽略不计。
gh-ost 提供了比简单的暂停更多的功能,除了暂停之外还可以做:
- 负载:与 pt-online-schema-change 相近的一个功能,用户可以设置 MySQL 指标的阈值,比如设置 Threads_running=30。
- 复制延迟:gh-ost 内置了心跳功能来检查复制延迟。用户可以指定查看哪个从库的延迟,gh-ost 默认是直接查看它连上的那个从库。
- 命令:用户可以写一些命令,根据输出结果来决定要不要开始操作。比如:SELECT HOUR(NOW()) BETWEEN 8 and 17.
上述所有指标即使在修改表定义的过程中也可以动态修改。
- 标志位文件:生成一个标志位文件,gh-ost 就会立刻暂停。删除文件,gh-ost 又会恢复工作。
- 用户命令:通过网络连上 gh-ost,通过命令让它暂停。
动态可控
如果别的工具在修改过程中产生了比较高的负载,DBA 只好把它停掉再修改配置,比如把一次拷贝的数据量改小些,然后再从头开始修改过程。这样的反复操作代价非常大。
gh-ost 通过监听 TCP 或者 unix socket 文件来获取命令。即使有正在进行中的修改工作,用户也可以向 gh-ost 发出命令修改配置,比如可以这样做:
- echo throttle | socat - /tmp/gh-ost.sock:这是暂停命令。也可以输入 no-throttle
- 修改运行参数,gh-ost 可以接受这样的修改方式来改变它的行为:chunk-size=1500, max-lag-millis=2000, max-load=Thread_running=30
可审计
用上面所说的相同接口也可以查看 gh-ost 的状态,查看当前任务进度、主要配置参数、相关 MySQL 实例的情况等。这些信息通过网络发送命令就可以得到,因此就给了运维人员极大的灵活性,如果是使用别的工具的话一般只能是通过共享屏幕或者不断跟踪日志文件最新内容。
可测试
读取二进制文件内容的操作完全不会增加主库的负载,在从库上做修改表结构的操作也和在主库上做是非常相象的(当然并不完全一样,但主要来说还是差不多的)。
gh-ost 自带了–test-on-replica 选项来支持测试功能,它允许你在从库上运行起修改表结构操作,在操作结束时会暂停主从复制,让两张表都处于同步、就绪状态,然后切换表、再切换回来。这样就可以让用户从容不迫地对两张表进行检查和对比。
在 GitHub 是这样在生产环境测试 gh-ost 的:有许多个指定的生产从库,在上面不提供服务,只是周而复始地不断地把所有表定义都改来改去。对于生产环境地每一张表,小到空表,大到几百 GB,都会通过修改存储引擎的方式来进行修改(engine=innodb), 这样并不会真正修改表结构。在每一次这样的修改操作最后都会停掉主从复制,再把原始表和临时表的全量数据都各做一次校验和,然后比较两个校验和,要求它们是一致的。然后恢复主从复制,再继续测试下一张表。Github 生产环境的每一张表都这样用 gh-ost 在从库上做过好多次修改测试。
可靠性高
所有上述讲到的和没讲到的内容,都是为了让你对 gh-ost 的能力建立信任。毕竟,大家在做这件事的时候已经使用类似工具做了好多年,而 gh-ost 只是一个新工具。
- Github 在从库上对 gh-ost 进行测试,在去主库上做第一次真正改动之前在从库上成功地试了几千次。所以,请你也在从库上开始测试,验证数据是完好无损的,然后再把它用到生产环境。希望你可以放手去试。
- 当你执行了 gh-ost 之后,也许你会看见主库的负载变高了,那你可以发出暂停命令。用 echo throttle 命令生成一个文件,看看主库的负载会不会又变得正常。试一下这些命令,你就可以知道你可以怎样控制它的行为,你的心里就会安定许多。
- 你发起了一次修改操作,然后估计完成时间是凌晨 2 点钟,可是你又非常关心最后的切换操作,非常想看着它切换,这可怎么办?只需要一个标志位文件就可以告诉 gh-ost 推迟切换了,这样 gh-ost 会只做完拷贝数据的操作,但不会切换表。它还会仍然继续同步数据,保持临时表的数据处于同步状态。等第二天早上你回到办公室之后,删除标志位文件或者向 gh-ost 发送命令 echo unpostpone,它就会做切换了。没有人希望软件强迫自己看着它做事情,它应该把大家解放出来,让人去做人该做的事。
- 谈到估计完成时间,–exact-rowcount 选项非常有用。在最开始时要在目标表上做个代价比较大的 SELECT COUNT(*) 操作查出具体要拷多少行数据,gh-ost 就会对它要做多少工作有了一个比较准确的估计。接下来在拷贝的过程中,它会不断地尝试更新这个估计值。因为预计完成的时间点总是会不断变化,所以已经完成的百分比就反而比较精确。如果你也曾经有过非常痛苦的经历,看着已经完成 99% 了可是剩下的一点操作却继续了一个小时也没完,你就会非常喜欢 gh-ost 提供的这个功能。
gh-ost 工作模式
gh-ost 工作时可以连上多个 MySQL 实例,同时也把自己以从库的方式连上其中一个实例来获取二进制日志事件。根据你的配置、数据库集群架构和你想在哪里执行修改操作,可以有许多种不同的工作模式。
1、连上从库,在主库上修改
这是 gh-ost 默认的工作模式,它会查看从库情况,找到集群的主库并且连接上去。修改操作的具体步骤是:
- 在主库上读写行数据;
- 在从库上读取二进制日志事件,将变更应用到主库上;
- 在从库上查看表格式、字段、主键、总行数等;
- 在从库上读取 gh-ost 内部事件日志(比如心跳);
- 在主库上完成表切换;
如果主库的二进制日志格式是 Statement,就可以使用这种模式。但从库就必须配成启用二进制日志(log_bin, log_slave_updates),还要设成 Row 格式(binlog_format=ROW),实际上 gh-ost 会在从库上帮你做这些设置。
事实上,即使把从库改成 Row 格式,这仍然是对主库侵入最少的工作模式。
2、连上主库
如果没有从库,或者不想在从库上操作,那直接用主库也是可以的。gh-ost 就会在主库上直接做所有的操作。仍然可以在上面查看主从复制延迟。
- 主库必须产生 Row 格式的二进制日志;
- 启动 gh-ost 时必须用–allow-on-master 选项来开启这种模式;
3、在从库上修改和测试
这种模式会在从库上做修改。gh-ost 仍然会连上主库,但所有操作都是在从库上做的,不会对主库产生任何影响。在操作过程中,gh-ost 也会不时地暂停,以便从库的数据可以保持最新。
- –migrate-on-replica 选项让 gh-ost 直接在从库上修改表。最终的切换过程也是在从库正常复制的状态下完成的。
- –test-on-replica 表明操作只是为了测试目的。在进行最终的切换操作之前,复制会被停止。原始表和临时表会相互切换,再切换回来,最终相当于原始表没被动过。主从复制暂停的状态下,你可以检查和对比这两张表中的数据。
gh-ost 在 GitHub 的应用
现在 Github 生产环境的表修改操作全都是用 gh-ost 完成的。每天只要有需求来了,技术人员就把它运行起来,有时候一天会做好多次。因为它有审计和控制功能,所以还可以把它和 Chatops 系统整合起来。技术人员可以对它的工作进度有非常清晰的了解,因此可以控制它的行为。在生产环境中各种指标和事件都被收集起来,让大家可以以图形化的方式看到操作情况。
开源
尽管现在已经稳定了,Github 还是有一些想要继续改进的方面,因此现在把它发布出来,希望能得到来自于社区的参与和贡献。Github 也会不断把社区提供的建议等公布出来。
Github 的技术团队还在积极维护 gh-ost。希望你能试用一下,他们花了很大的精力,相信它是非常可靠的。
鸣谢
gh-ost 由 GitHub 数据库基础架构组设计、开发、审查和测试:
@jonahberquist , @ggunson , @tomkrouper , @shlomi-noach
同时也要对 GitHub 其他提供了非常有价值信息和建议的同事表示感谢。也要感谢来自 MySQL 社区的朋友们,在试生产阶段就审阅了这个项目并提出了宝贵意见。
评论 1 条评论