一、背景
GitHub 正式宣布以开源的方式发布 gh-ost,GitHub 的 MySQL 无触发器在线更改表定义工具!下面是官方给出 gh-ost 产生的背景。
gh-ost 是 GitHub 在 2016 年 5 月份开源的,目的是解决一个经常碰到的问题:不断变化的产品需求会不断要求更改 MySQL 表结构,gh-ost 通过一种影响小、可控制、可审计、操作简单的方案来改变线上表结构。
在介绍 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 引擎。
为什么我们决定去设计一个新解决方案,而不是直接从上面的几种方案中选一个用?现有的解决方案都有着自身的局限性,下面就对它们的不足之处做个简单分析。我们会主要深入地分析基于触发器的在线修改表定义工具的不足之处。
- 在从库上修改表定义的方案需要付出许多运维代价,这需要更多的服务器、更长的完成时间和更复杂的管理工作。修改操作是直接应用在具体的某个从库或者整个拓扑架构的一些子树上。服务器宕机、从库数据不够新、新部署的服务器等各种问题都需要有非常严密的跟踪系统来跟进单个数据库上的操作。一个改变操作可能会需要多次反复,也就需要更长时间。而把一个从库升为主库也会导致短暂的停服。如果同时需要做多个更改就更难协调。我们每天都要改好几张表,所以在考虑解决方案时,我们不希望有这样的管理开销。
- MySQL 的 InnoDB 在线 DDL 只能是在你敲命令的那个 MySQL 上才是“在线”修改的。二进制文件中的日志把修改操作序列化了,从库应用日志时会导致复制延迟。但如果尝试在每个从库上挨个去改的话又会导致上面分析的管理代价。而且 DDL 还是不可中断的,要是在修改时把操作杀掉的话还需要更长的时间去回滚,甚至导致数据字典崩溃。这种方案也不“友好”,在系统负载高时也不能限速或者暂停。这样的操作还有可能会耗尽你的系统资源。
- 我们用了 pt-online-schema-change 好几年了。可是,当我们的数据增多、业务压力增大之后,我们就碰到了越来越多的问题,甚至到了许多修改操作都被认为是“危险操作”的地步。有一些操作只敢在非业务高峰期或者周末才敢执行,其它的总是会导致 MySQL 停止服务。所有现有的在线修改表定义工具都是用 MySQL 触发器来迁移数据的,因此本身就存在着一些问题。
基于触发器的解决方案有什么不好?
所有在线修改表定义的工具运行原理都是相似的:创建一张与原始表定义相同的临时表,趁上面没有数据时先改好表定义,然后慢慢地、用增量方式把数据从原始表拷到临时表,同时不断的把进行中的原始表上的数据操作(所有应用在原始表上的插入、删除、更新操作)也应用过来。当工具把所有数据都拷贝完毕,两边数据同步了之后,它就用这张临时表来替代原始表。修改过程就结束了。
像 pt-online-schema-change、LHM 和 oak-online-alter-table 这些工具用的都是同步复制的方式,对表的每一条数据修改都会立刻在同一个事务里就应用到临时表上。Facebook 的工具用的则是异步模式,先把修改操作都记在一张修改日志表里,然后再取出来执行,把修改操作应用到临时表上。这些工具全都使用触发器来提取那些应用在目标表上的操作。
触发器都是存储过程,在表上有插入、删除、修改操作时就会被触发。触发器可能包括好多条语句,这些语句都是和引发触发器的那条操作在相同的事务空间内运行的,因此保证了这些操作的原子性。
一般意义上的触发器,尤其是基于触发器的表定义修改操作,都有如下问题:
- 触发器就是存储过程,都是解释型代码,MySQL 不会做预编译。把它们硬嵌入到业务操作的事务空间中,会给你要修改的表上执行的每条操作都增加命令分析和解释的开销。
- 锁:触发器与操作语句分享相同的事务空间,当操作语句释放了原始表上的锁之后,触发器再去释放另一张表上的锁。在同步模式下这样行为的后果尤其严重。主库上的锁竞争与写并发有直接关系。我们在生产环境中曾经遇到过锁竞争导致的几乎乃至完全锁住的情况,完全无法访问表或者整个数据库。触发器导致的另一种锁是在创建或销毁触发器时对元数据的锁。在完成修改表定义之后从比较忙的表上删除触发器时,我们曾经碰到几十秒甚至几分钟无法提供服务的情况。
- 无法暂停:当主库业务负载开始增高时,你可能会想要暂停或者取消还没完成的修改表定义的任务。可是基于触发器的方案没办法这么做。也许你可以暂停行拷贝的操作,但却不能暂停触发器,因为把触发器停掉会导致临时表中丢数据。所以,在整个过程中触发器都必须一直处于工作状态。在一些繁忙的服务器上,我们曾经见过即使把在线操作全停掉,最后主库还是被触发器给拖死的情况。
- 并发修改:大家都希望能同时修改多张表的定义。考虑到上面分析的触发器的代价,我们并不敢以触发器的模式同时修改多张表的定义,我们也没听说有哪家公司真的在线上这么干。
- 测试:大家也许想测试一下修改方案是否可行,评估一下负载。基于触发器的方案只能在从库上通过基于语句的复制来模拟一下,由于从库上的复制操作是单线程的(即使用了多线程复制的方案,大部分情况下也还是这样的),这样远不能模拟出在主库上修改过程中的真实情况。
二、gh-ost 介绍
gh-ost 是 GitHub’s Online Schema Transmogrifier/Transfigurator/Transformer/Thingy 的缩写,意思是 GitHub 的在线表定义转换器。抛弃了 pt-online-schema-change 使用 trigger 来同步增量数据的方法,而通过模拟 slave 获取 row 格式的 binlog 的方式来获取增量数据。思路也很新颖,作者很厉害,也是是openark kit工具集的作者(主要是用 Python 写的一套工具集)。具体的数据流图可以看下图。
因为 binlog 中记录的是 full image,所以 binlog 中的数据是最权威的,而且读取的 binlog 在应用的时候做了如下转化,而且 copy old data 是 insert ignore,因为会以 binlog 的优先级为最高,因此不会有问题。
源类型
|
目标类型
|
insert
|
replace
|
update
|
update
|
delete
|
delete
|
对与 insert 和 update 是没有问题的,因为无论 copy old row 和 apply binlog 的先后顺序,如果 apply binlog 在后,会覆盖掉 copy old row,如果 apply binlog 在前面,copy old row 因为使用 insert ignore,因此会被 ignore 掉。
对与 delete 数据,我们可以演算一下,abc 三个操作,可能存在三种情况(b 肯定在 a 的后面):
a. delete old row
b. delete binlog apply
c. copy old row
那么操作顺序只有下面几种可能性:
1. cab,c 会将数据 copy 到 ghost 表,最后 b 会把 ghost 表中的数据 delete 掉;
2. acb,c 空操作,b 也是空操作;
3. abc,b 空操作,c 也是空操作;
大概看完了 gh-ost 工作原理后,既然牵扯到了 replace 语句,自然表就必须要有一个主键或唯一键。如果没有的情况下执行 gh-ost,自然会得到 gh-ost 的一个错误提供:FATAL No PRIMARY nor UNIQUE key found in table! Bailing out.
gh-ost 有以下特点:
- 无触发器
gh-ost 不使用触发器,它跟踪二进制日志文件,在对原始表的修改提交之后,用异步方式把这修改内容应用到临时表中去。
gh-ost 希望二进制文件使用基于行的日志格式,但这并不表示如果主库上使用的是基于语句的日志格式,就不能用它来在线修改表定义了。事实上,我们常用的方式是用一个从库把日志的语句模式转成行模式,再从这个从库上去读日志。搭一个这样的从库并不复杂。
- 轻量级
因为不需要使用触发器,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 | nc -U /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),这样并不会真正修改表结构。在每一次这样的修改操作最后我们都会停掉主从复制,再把原始表和临时表的全量数据都各做一次校验和,然后比较两个校验和,要求它们是一致的。然后我们恢复主从复制,再继续测试下一张表。我们生产环境的每一张表都这样用 gh-ost 在从库上做过好多次修改测试。
- 可靠的
所有上述讲到的和没讲到的内容,都是为了让你对 gh-ost 的能力建立信任。毕竟,大家在做这件事的时候已经使用类似工具做了好多年,而 gh-ost 只是一个新工具。
我们在从库上对 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 工作时可以连上多个 MySQL 实例,同时也把自己以从库的方式连上其中一个实例来获取二进制日志事件。根据你的配置、数据库集群架构和你想在哪里执行修改操作,可以有许多种不同的工作模式。
1、连上从库,在主库上修改
这是 gh-ost 默认的工作模式,它会查看从库情况,找到集群的主库并且连接上去。修改操作的具体步骤是:
A. 在主库上读写行数据;
B. 在从库上读取二进制日志事件,将变更应用到主库上;
C. 在从库上查看表格式、字段、主键、总行数等;
D. 在从库上读取 gh-ost 内部事件日志(比如心跳);
E. 在主库上完成表切换;
如果主库的二进制日志格式是 Statement,就可以使用这种模式。但从库就必须配成启用二进制日志(log_bin,log_slave_updates),还要设成 Row 格式(binlog_format=ROW),实际上 gh-ost 会在从库上帮你做这些设置。
事实上,即使把从库改成 Row 格式,这仍然是对主库侵入最少的工作模式。
2、连上主库
如果没有从库,或者不想在从库上操作,那直接用主库也是可以的。gh-ost 就会在主库上直接做所有的操作。仍然可以在上面查看主从复制延迟。
A. 主库必须产生 Row 格式的二进制日志;
B. 启动 gh-ost 时必须用--allow-on-master
选项来开启这种模式;
3、在从库上修改和测试
这种模式会在从库上做修改。gh-ost 仍然会连上主库,但所有操作都是在从库上做的,不会对主库产生任何影响。在操作过程中,gh-ost 也会不时地暂停,以便从库的数据可以保持最新。
A.--migrate-on-replica
选项让 gh-ost 直接在从库上修改表。最终的切换过程也是在从库正常复制的状态下完成的。
B.--test-on-replica
选表明操作只是为了测试目的。在进行最终的切换操作之前,复制会被停止。原始表和临时表会相互切换,再切换回来,最终相当于原始表没被动过。主从复制暂停的状态下,你可以检查和对比这两张表中的数据。
三种方法各有优缺点,这里只说缺点,先说第一种的缺点,由于会在从上面读取 binlog,但有可能主库的 binlog 没有完全在从库执行,所以个人感觉第一种方法有丢失数据的风险。第二种方法任何操作都会再主库操作,或多或少会对主库负载造成影响,但是可以通过调整一些参数降低和时刻关注这些影响,所以个人推荐使用第二种方法。至于第三种方法是偏向测试用的,这里不做过多介绍,但是第三种方法里有一个细节,cut-over 阶段有会 stop slave 一个操作,其实这个操作风险特别高,有时 stop slave 时间会很长,务必会对线上数据库使用造成影响,所以如果使用第三种方法做测试也要在线下数据库。
需求
- 将需要一台服务器提供基于行式复制(RBR)格式的二进制日志。现在仅支持 FULL 镜像,MINIMAL 镜像将来会获得支持。
- gh-ost 用户需要具有这些权限在你迁移的数据库上:ALTER, CREATE, DELETE, DROP, INDEX, INSERT, LOCK TABLES, SELECT, TRIGGER, UPDATE。或 SUPER, REPLICATION SLAVE on *.*,或 REPLICATION CLIENT, REPLICATION SLAVE on *.*。SUPER 权限是 STOP SLAVE,START SLAVE 操作所必需的,这些用于将你的 binlog_format 切换到 ROW 格式(如果它不是 ROW 格式,并且你明确指定了
--switch-to-rbr
选项)。如果你的复制从库已经是 RBR(binlog_format = ROW)模式,你可以指定--assume-rbr
以避免 STOP SLAVE/START SLAVE 操作,因此不需要 SUPER。 - 运行
--test-on-replica
模式时,在切换阶段之前,gh-ost 会停止复制,以便你可以比较这两个表并确保迁移是正确的(仅测试使用)。
限制
- 不支持外键,在未来可能会得到支持。
- 触发器不受支持,在未来可能会得到支持。
- MySQL 5.7 JSON 列受支持,但不能作为 PRIMARY KEY 的一部分。
- 表必须有一个 PRIMARY KEY 或其他 UNIQUE KEY。
- Amazon RDS 可以工作,但有其自身的局限性(阿里云已经支持)。
- 通过从库迁移时不支持多源复制。
- 主 – 主模式下,设置仅在主动 – 被动中受支持。在主动 – 主动(表中同时写入两个主动实例的表)不受支持。它可能在未来得到支持。
- 如果将 enum 字段作为迁移 Key(通常是 PRIMARY KEY)的一部分,则迁移性能会降低并且可能出现不好的情况。
- ALTER TABLE … RENAME to some_other_name 不受支持,并且不应该使用 gh-ost 来执行这样的小操作。
五、gh-ost参数介绍
常用参数介绍
--host
Inspector 的数据库实例地址,gh-ost 默认当做从库处理。
--port
Inspector 的数据库实例端口,gh-ost 默认当做从库处理。
--user
Inspector 的数据库实例用户名,gh-ost 默认当做从库处理。
--password
Inspector 的数据库实例密码,gh-ost 默认当做从库处理。
--mysql-timeout
连接、读、写的超时时间。
--database
数据库名称,不填写则默认在--alter
语句中提取。
--table
表名称,不填写则默认在--alter
语句中提取。
--alter
ALTER语句的body部分,如“ALTER TABLE wing ADD COLUMN id int not null default 0”,使用 gh-ost 的--alter
参数时,写成--alter ADD COLUMN id int not null default 0
即可。
当然,也可以直接写完整的语句,如果你没有给定--database
或--table
参数,则默认从你的语句中提取,但不建议这么使用。
--allow-on-master
默认情况下 gh-ost 希望你连接一个从库进行 binlog 获取。如果你想连接主库进行整个迁移操作,需要加上此选项即可。gh-ost 提供了三种连接方案。
--max-load
迁移过程中,gh-ost 会时刻关注负载情况,负载阀值是使用者自己定义,比如数据库的最大连接数,如果超过阀值,gh-ost 不会退出,会等待到负载在阀值以下继续执行。
--critical-load
这个指的是 gh-ost 退出阀值,当负载超过这个阀值,gh-ost 会停止并退出。
--max-lag-millis
会监控从库的主从延迟情况,如果延迟秒数超过这个阀值,迁移不会退出,等待延迟秒数低于这个阀值继续迁移。这个是迁移中很大的一个问题,特别是在从库迁移时。
gh-ost 监控复制延迟是通过检查 gh-ost 本身在实用程序更新日志表中注入的心跳事件来衡量的。也就是说,为了测量这个复制延迟,gh-ost 不需要发出show slave status
命令,也没有任何外部心跳机制。
当提供--throttle-control-replicas
时,限流还会考虑指定主机上的延迟。通过查询 gh-ost 的更新日志表(其中 gh-ost 注入心跳)完成列出的主机上的延迟时间测量。
gh-ost 能够利用毫秒测量复制延迟,当--max-lag-millis
小于 1000,即小于1秒时,gh-ost 将进行限流。
--throttle-control-replicas
和--max-lag-millis
参数相结合,这个参数指定主从延迟的数据库实例。
--initially-drop-ghost-table
gh-ost 在迁移时会创建两张表,表名分别是 _xx_ghc 和 _xx_gho,gh-ost 非正常退出时不会清理相关表。如果这两张表存在,且加上了这个参数,那么在执行 gh-ost 时会自动删除原 _xx_gho 表,重新创建,否则退出。_xx_gho 表相当于老表的全量备份,也被称之为影子表;_xx_ghc 表数据是数据更改日志(Changelog)。
--initially-drop-old-table
gh-ost 执行完成后默认不会删除 _xx_del 表(加上--ok-to-drop-table
参数会在执行完成后删除原表),此表是由 gh-ost 原子切换时产生(rename table `sbtest`.`xx` to `sbtest`.`_xx_del`, `sbtest`.`_xx_gho` to `sbtest`.`xx`
),是存储原表数据。加上--initially-drop-old-table
参数时 gh-ost 会在执行前如果检测到 _xx_del 表存在则进行删除操作(默认不启用该参数,gh-ost 直接退出操作),该参数不建议使用,请手动处理原来存在的原数据表。
--initially-drop-socket-file
gh-ost 执行时会创建 socket 文件,非正常退出时不会删除 socket 文件,下次执行 gh-ost 时会报错,加上这个参数,gh-ost 会强制删除已经存在的 socket 文件。该参数不建议使用,可能会删除一个正在运行的 gh-ost 程序,导致 DDL 失败。
--ok-to-drop-table
gh-ost 执行完以后是否删除 _xx_del 表,此表是由 gh-ost 原子切换时产生(rename table `sbtest`.`xx` to `sbtest`.`_xx_del`, `sbtest`.`_xx_gho` to `sbtest`.`xx`),是存储原表数据。加上此参数会执行完成后自动删除,但是非正常完成退出时无法删除此表,可能就需要借助--initially-drop-old-table
参数在执行前删除已经存在的 _xx_del 表。该参数不建议使用,请手动处理原表。
--cut-over
自动执行 rename 操作,选择 cut-over 类型:atomic/two-step,atomic(默认)类型的 cut-over 是 Github 的算法,two-step 采用的是 facebook-OSC 的算法。
--cut-over-lock-timeout-seconds
gh-ost 在 cut-over 阶段最大的锁等待时间,当锁超时时,gh-ost 的 cut-over 将重试。(默认值:3)
--switch-to-rbr
让 gh-ost 自动将从库的 binlog_format 转换为 ROW 格式。
--assume-rbr
确认 gh-ost 连接的数据库实例的 binlog_format=ROW 的情况下,可以指定--assume-rbr
,这样可以禁止从库上运行 stop slave,start slave;执行 gh-ost 用户也不需要 SUPER 权限。
--panic-flag-file
这个文件被创建,迁移操作会被立即终止退出。
--throttle-flag-file
此文件存在时操作暂停,删除文件操作会继续。
--postpone-cut-over-flag-file
当指定的文件存在的时候,gh-ost 的 cut-over 阶段将会被推迟,直到该文件被手动删除则会立马进行 cut-over 操作。如果文件不存在,gh-ost 默认会自动创建该文件。
--concurrent-rowcount
该参数如果为 True(默认值),则进行 row-copy 之后,进行实际行数统计,并调整 ETA 时间;否则,gh-ost 首先实际统计表行数,然后开始 row-copy。
--exact-rowcount
准确统计表行数(使用 select count(*) 的方式),得到更准确的预估时间。
--execute
如果确定执行,加上这个参数。
可选参数介绍
--default-retries
各种操作在 panic 前重试次数。(默认为60)
--chunk-size
原表 insert…select 到目标表的迁移过程是一步步分批次完成的,这个参数是指事务每次提交的行数,默认是 1000。
--timestamp-old-table
使旧表包含时间戳值,旧表是在成功迁移结束时将原始表重新命名的内容。例如,如果表是 gh_ost_test,那么旧表通常是 _gh_ost_test_del。使用--timestamp-old-table
后,它将是用_gh_ost_test_20170221103147_del
。
--throttle-http
提供一个 HTTP 端点,gh-ost 将在给定的 URL 上发出 HEAD 请求,并在响应状态码不是 200 时进行限流。URL 可以通过交互式命令动态查询和更新,空的 URL 表示禁用 HTTP 检查。
--approve-renamed-columns
当做(change old_name new_name …)动作时,gh-ost 分析语句以尝试将旧列名称与新列名称相关联,如果它检测到确实是重命名操作,默认情况下将会打印出信息并退出。但除非你提供--approve-renamed-columns
,强制发出迁移操作。
如果你认为 gh-ost 解析错误,并且实际上并且没有重命名,你可以改为传入--skip-renamed-columns
,这将导致 gh-ost 取消关联列值,数据将不会在这些列之间复制,这样的话,列数据可能会丢失。
--skip-foreign-key-checks
默认情况下,gh-ost 会验证迁移表中存不存在外键,如果存在就会报错并退出;在具有大量表的服务器上,此检查可能需要很长时间。如果你确定没有外键存在(表没有引用其他表,也没有被其他表引用)并希望保存检查时间,可以使用--skip-foreign-key-checks
。但如果表上有外键,使用这个参数则会清除外键,千万注意。
--discard-foreign-keys
该操作很危险,意味着将默默丢弃表上存在的任何外键。目前,gh-ost 不支持迁移表上的外键(当它在迁移表上注意到外键时,它会保留)。但是,它能够支持通过此标志删除外键,如果你想这么干,这是一个有用的选项。使用下来感觉跟--skip-foreign-key-checks
参数作用一样。
--replica-server-id
gh-ost 原理是通过模拟 slave 从而获得 binlog,其默认 server-id 为 99999,如果你运行多个迁移,那么你必须为每个 gh-ost 进程提供一个不同的,唯一的 server-id。也可以使用进程 ID 当做 server-id,例如:--replica-server-id=$((1000000000+$$))
。
--migrate-on-replica
通常,gh-ost 用于在主服务器上迁移表。如果你只希望在从库上执行全部迁移,使用--migrate-on-replica
参数将 gh-ost 连接到从库进行迁移。
--assume-master-host
默认情况下,gh-ost 更倾向连接从库来进行迁移。gh-ost 通过爬取复制拓扑来推断主服务器的身份,你可以通过--assume-master-host=the.master.com
明确告诉 gh-ost 主服务器的身份(对阿里云高可用版 RDS 的操作就必须加这个参数,因为其是双主架构,gh-ost 会推断出主服务器的身份,但是你的 gh-ost 是无法连接过去的,所以会报错)。
这在以下方面很有用:主 – 主拓扑结构(与--allow-master-master
一起使用),其中 gh-ost 可以随意选择其中一个主协同者,这种情况你可以选择一个特定的主库。tungsten 复制器拓扑结构(与--tungsten
一起使用),其中 gh-ost 无法抓取并检测主节点。
--master-user
当明确主服务器的身份后,默认使用--user
给定的用户进行连接,你也可以使用--master-user
指定主服务器的用户。
--master-password
当明确主服务器的身份后,默认使用--password
给定的用户进行连接,你也可以使用--master-password
指定主服务器的用户密码。
--dml-batch-size
gh-ost 从二进制日志读取事件,并将它们应用到 ghost 表上。它采用的方式是将多个事件分组应用于单个事务中。这可以提供更好的写入吞吐量,因为我们不需要将每个事务日志同步到的磁盘。
此选项就是控制批量写入的大小,允许的值是 1-100,其中1表示不分组处理(二进制日志中的每个事件在其自己的事务中应用到 ghost 表上)。默认值是10。
--heartbeat-interval-millis
用来控制注入心跳事件的频率(就是 _xx_ghc 表),用来测量主从延迟。你应该设置 heartbeat-interval-millis <= max-lag-millis。否则,将失去粒度和效果。默认值 100。其--max-lag-millis
值应该在 300-500 之间。
--conf
指定 gh-ost 凭据的文件,如下格式。
1 2 3 |
[client] user=gromit password=123456 |
--debug
输出详细日志。
--verbose
执行过程输出日志。
六、安装使用gh-ost
直接使用二进制安装即可
1 2 3 |
$ tar xvf /tmp/gh-ost-release/gh-ost-binary-linux-20180601054532.tar.gz -C /tmp $ mv /tmp/gh-ost /usr/bin $ gh-ost -version |
如果你安装有什么问题直接借助搜索引擎应该就可以解决。
然后在使用上,下面是一些通用的配置(采取在主库进行 DDL 操作模式),你也可以尝试不同的配置根据上面配置的介绍来使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
$ gh-ost \ --max-load=Threads_running=25 \ --critical-load=Threads_running=64 \ --chunk-size=1000 \ --throttle-control-replicas="10.10.0.110:3306" \ --max-lag-millis=1000 \ --initially-drop-old-table \ --initially-drop-ghost-table \ --initially-drop-socket-file \ --ok-to-drop-table \ --assume-master-host="10.10.0.109:3306" \ --host="10.10.0.109" \ --port=3306 \ --user="root" \ --password="123456" \ --database="test" \ --table="test" \ --verbose \ --alter="add index idx1_k(k)" \ --switch-to-rbr \ --allow-on-master \ --cut-over=default \ --default-retries=120 \ --concurrent-rowcount \ --execute |
实际上变更的表如果有超长事务,依然存在 mdl 锁问题导致失败,因为在最后的新表旧表交替阶段,会有一个短暂的加锁操作,而长事务会阻塞这个加锁操作,所以还得在 ddl 前确认无长事务。
七、gh-ost 交互式命令
gh-ost 被设计为操作友好。为此,它允许用户即使在运行时也可以控制其行为。gh-ost 提供两种方式来进行动态控制。
- Unix 套接字文件
通过--serve-socket-file
提供或由 gh-ost 默认设置,该接口总是处于启动状态。当 gh-ost 自我设定时,gh-ost 将在启动时和整个迁移过程中公布套接字文件的标识文件。默认在 /tmp 下面,由gh-ost.库名.表明.sock
组成。
- TCP
由--serve-tcp-port
选项提供,默认没有。
两个接口可以同时使用,两者都响应简单的文本命令,这使得通过 shell 进行交互变得容易。
命令介绍:
help
显示可用命令的简要列表。
status
返回迁移进度和配置的详细状态摘要。
sup
返回迁移进度的简要状态摘要。
coordinates
返回检查服务器的最近(尽管不是最新的)二进制日志位置。
chunk-size=<newsize>
修改chunk大小,适用于下一刻数据复制。
dml-batch-size=<newsize>
适用于下一次应用二进制日志事件数量。
max-lag-millis=<max-lag>
修改最大复制延迟阈值(毫秒,最小值是 100,即 0.1 秒)。
max-load=<max-load-thresholds>
修改最大负载配置,适用于下一刻数据复制。如“max-load=Threads_running=50,threads_connected=1000”。
critical-load=<critical-load-thresholds>
修改临界负载配置(超过这些阈值会中止操作)。
nice-ratio=<ratio>
修改 nice 比例,0 表示不设置 nice 就是不睡眠线程。如果为1则表示 gh-ost 检测到复制行花费了 1ms 后将进行 1*1ms 睡眠;如果花费 100ms,nice 值为 0.5,则会睡眠 50ms,以此类推。默认为 0。
throttle-http
改变 HTTP 端点。
throttle-query
改变查询。
throttle-control-replicas='replica1,replica2'
更改副本的列表,这些副本是 gh-ost 会检查的,这需要用逗号分隔的副本列表来检查并替换先前的列表。
throttle
强制迁移暂停。
no-throttle
取消强制迁移暂停。
unpostpone
在 gh-ost 推迟 cut-over 阶段,指示 gh-ost 停止推迟并立即进行切换。
panic
理解放弃操作,意味着 gh-ost 将中断所有操作。
下面给一些样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
$ echo "status" | nc -U /tmp/gh-ost.sbtest.sbtest1.sock # Migrating `sbtest`.`sbtest1`; Ghost table is `sbtest`.`_sbtest1_gho` # Migrating localhost.localdomain:3306; inspecting localhost.localdomain:3306; executing on localhost.localdomain # Migration started at Thu Jun 07 03:37:12 -0400 2018 # chunk-size: 250; max-lag-millis: 1500ms; dml-batch-size: 10; max-load: Threads_running=25; critical-load: Threads_running=64; nice-ratio: 0.000000 # throttle-additional-flag-file: /tmp/gh-ost.throttle # throttle-control-replicas count: 1 # Serving on unix socket: /tmp/gh-ost.sbtest.sbtest1.sock Copy: 1612500/2607971 61.8%; Applied: 0; Backlog: 0/1000; Time: 4m24s(total), 4m24s(copy); streamer: mysql-bin.000033:180729376; State: migrating; ETA: 2m43s $ echo "sup" | nc -U /tmp/gh-ost.sbtest.sbtest1.sock Copy: 1655750/2607971 63.5%; Applied: 0; Backlog: 0/1000; Time: 4m36s(total), 4m36s(copy); streamer: mysql-bin.000033:189291849; State: migrating; ETA: 2m38s $ echo "coordinates" | nc -U /tmp/gh-ost.sbtest.sbtest1.sock mysql-bin.000033:140582919 $ echo "chunk-size=500" | nc -U /tmp/gh-ost.sbtest.sbtest1.sock $ echo "chunk-size=?" | nc -U /tmp/gh-ost.sbtest.sbtest1.sock 500 $ echo "dml-batch-size=?" | nc -U /tmp/gh-ost.sbtest.sbtest1.sock 10 $ echo "max-lag-millis=?" | nc -U /tmp/gh-ost.sbtest.sbtest1.sock 1000 |
八、gh-ost 使用风险点
1. 加锁时长
原表迁移数据到目标表是使用 insert…select 语句,根据--chunk-size
大小分批次进行的,为了保证数据一致性,对原表加了 lock in share mode。所以一次事务执行时间越久,原表对查询范围的锁定就越久。
1 2 3 |
insert /* gh-ost `sbtest`.`sbteset1` */ ignore into `sbtest`.`_sbtest1_gho` (`id`, `field`) (select `id`, `field` from `sbtest`.`sbteset1` force index (`PRIMARY`) where (((`id` > _binary'463677712')) and ((`id` < _binary'463678012') or ((`id` = _binary'463678012')))) lock in share mode |
另外,如果表事务操作非常频繁,会出现拿不到锁的情况,gh-ost会执行失败。
2. 高频次事务操作
gh-ost 使用串行方式来执行增量数据和 Insert…select,默认应用增量数据优先级更高,只有当没有增量数据在某个瞬间没有时才会应用一次 Insert…select,所以如果事务操作非常频繁,gh-ost 就会处于一直在处理增量数据过程中,导致 Insert…select 应用很慢,拉长整个执行时间。
3. 添加唯一索引
注意点,对于使用gh-ost添加唯一约束时,gh-ost并没有给什么参数来控制,默认会去掉重复数据,并且不会报什么问题。意味着你如果添加唯一索引可能会造成数据丢失,这是你需要注意的。
4. default null 变更为 not null
当我们需要把列的 DEFAULT NULL 转换为 NOT NULL 时,如果增量过程(应用 binlog 事件)原表插入 NULL 值,在影子表插入会在重试多次后失败,报错 cannot be null,导致 gh-ost 退出。
上面只是强调增量过程,在 Insert … select 全量过程中是不会有问题,MySQL 对原生 SQL 会做处理,会把原表 Null 值替换为影子表的默认值。
5. datetime 类型转换为 timestamp 类型
在把列的 datetime 类型转换为 timestamp 类型时,如果同时更改了列名会导致增量数据不一致,比如当前 datetime 类型的列时间为北京时间(+08:00),变更为 timestamp 类型时,增量数据会被转为 UTC 时间。
但在把列的 datetime 类型转换为 timestamp 类型时,如果列名不变时,应用增量数据时会获取数据库时区使用 convert_tz() 函数进行时间转换,此时数据一致。
6. 创建 ghost 表没有带自增值
九、阿里云 RDS 的支持
目前用户使用的话,记得加上以下几个参数:
--allow-on-master
--assume-rbr
--assume-master-host
--aliyun-rds
参数具体什么意思,可以参考上面的说明。阿里云也有专门的文章阐述“gh-ost支持阿里云RDS”。
<参考>