本文内容较多,篇幅较长,若不想了解ghost原理,几种模式的介绍以及具体的验证过程,可直接跳到‘四 gh-ost使用总结’查看简洁版使用说明。
一 gh-ost使用场景
生产环境当有关于一个大表的大操作时(比如select count一个大表),此时对大表做DDL会被阻塞,提示等待table metadata lock。如果业务比较频繁的话,该DDL会阻塞关于该表的后续select sql。
如果生产库cpu使用率打满,想通过创建索引优化导致cpu使用率打满的慢sql时,被阻塞了,则无法快速处理该故障。
使用gh-ost工具可以在不锁表的情况下在线修改表结构,仅仅在最后切换表名(rename table)时锁表,但很快,就一两秒左右。
1.1 模拟锁阻塞
会话1执行:
select count(*) from zcx.my_sensor_state;#该表有14177038条数据
会话2执行:
create index idx_record_id on zcx.my_sensor_state(record_id); #被阻塞
会话3就能查看到元数据锁:
show processlist;
DDL操作被大select阻塞了。
此时,在会话4也查询下这个表,普通的select也被阻塞了:
select* from zcx.my_sensor_state limit 2; #查询卡住
会话5查看进程信息:
show processlist;
只执行select count(*)不会阻塞select,但是如果select count(*)期间,执行了关于这个表的DDL,再执行这个表的select,就会被阻塞,这样影响范围是比较大的。如果大表DDL比较慢,影响时间就比较长。
二 gh-ost理论知识
2.1 gh-ost介绍
gh-ost是MySQL没有使用触发器的在线迁移解决方案,它可以进行动态控制(以被指示推迟最关键的步骤:交换表),重新配置(即使迁移仍在运行,您也可以交互式地重新配置gh-ost)、审计等。
2.2 gh-ost原理
所有现有的在线模式更改工具都以类似的方式运行:它们创建一个与原始表相似的幽灵(ghost)表,修改表结构(加字段或者建索引),缓慢增量地将数据从原始表复制到幽灵表,同时将正在进行的更改(应用于表的任何INSERT、DELETE、UPDATE)传播到幽灵表。最后,在适当的时候,他们会用幽灵表替换原来的表。
gh-ost也是使用相同的模式,它与所有现有工具的不同之处在于不使用触发器。gh-ost使用binlog日志流来捕获表更改,并将其异步应用于幽灵表。gh-ost伪装为一个 MySQL 从副本:它连接到 MySQL 服务器并开始请求 binlog 事件,就好像它是一个真正的从副本一样。
gh-ost对迁移过程有更大的控制权;可以真正暂停它;
2.3 要求和限制
gh-ost/doc/requirements-and-limitations.md at master · github/gh-ost · GitHub
要求
- gh-ost目前需要MySQL 5.7及更高版本。
- binlog_format=row
- log_slave_updates=on
- 要求用户有如下权限:
- 对表所属的库有ALTER, CREATE, DELETE, DROP, INDEX, INSERT, LOCK TABLES, SELECT, TRIGGER, UPDATE权限;
- SUPER, REPLICATION SLAVE,REPLICATION CLIENT on *.*
stop slave,start slave需要super权限,这些用于:
- 将binlog_format切换到ROW,如果它不是ROW并且您明确指定了--switch-to-rbr
- 如果你的复制已经在RBR中(binlog_format=ROW),你可以指定--assume-rbr 可以避免STOP SLAVE/START SLAVE操作,因此不需要SUPER。
- gh-ost对所有MySQL连接使用REPEATABLE_READ事务隔离级别,而不管服务器默认设置如何。
- 在切换阶段之前运行--test-on-replica,gh-ost会停止复制,以便您可以比较这两个表并确信迁移是合理的。
限制
- 不支持外键约束;
- 不支持触发器;
- MySQL 5.7支持JSON列,但不作为PRIMARY KEY的一部分;
- 如果存在另一个同名且大小写不同的表,则不允许迁移该表,比如存在一个MYTable ,则不能迁移MyTable;
- 不支持多源复制;
- 仅支持active-passive模式 的MASTER-MASTER主主模式,不支持 active-active模式 的MASTER-MASTER主主模式;
- 如果您的迁移key(通常是PRIMARY key)中有一个枚举字段,迁移性能将降低,并可能很差;
- 不支持FEDERATED 的表;
- 不支持Encrypted binary logs;
2.4 gh-ost三种模式介绍
2.4.1 连接到从库,在主库做迁移(默认模式)
特点:在此模式下,gh-ost首先连接到从库以查看其状态,并找到主库进行连接。然后,它在主库上创建幽灵表(与原表结构一致)和变更日志表,同时修改幽灵表的结构以符合迁移要求。接着,gh-ost在从库上读取二进制日志事件,并将这些变更应用到主库上的幽灵表。最后,在主库上完成表切换。
优点:对主库的侵入最小
缺点:如果主库的binlog没有完全在从库执行,可能会导致数据不一致的风险。
2.4.2 连接到主库,迁移过程所有操作都在主库上执行
特点:在此模式下,gh-ost直接连接到主库,并在主库上创建幽灵表和变更日志表,同时修改幽灵表的结构。然后,它读取主库的二进制日志事件,并将这些变更应用到主库上的幽灵表。最后,在主库上完成表切换。
优点:简化了迁移过程,因为所有的操作都在主库上进行,无需考虑从库的同步状态。
缺点:可能会对主库的负载造成影响,因为gh-ost需要在主库上执行读取和写入操作。但是,可以通过调整参数来降低这种影响。
2.4.3 在从库做迁移测试
特点:此模式主要用于测试目的。gh-ost连接到从库,并在从库上执行迁移操作。在迁移过程中,它会在从库上创建幽灵表和变更日志表,并读取从库的二进制日志事件来更新幽灵表。加--migrate-on-replica参数能实现这一功能。如果没加execute参数,最终的表切换不会在实际的生产环境中执行,而是用于测试迁移的可行性和正确性,加上execute参数,会真正执行。
优点:提供了一个安全的测试环境,允许用户在不影响生产环境的情况下测试迁移操作。
缺点:由于迁移操作是在从库上进行的,因此无法直接反映主库的实际负载和性能。此外,在从库上执行的迁移操作可能会对主从复制造成一定的影响,而且需要在从库关闭只读参数。
2.5 gh-ost重要参数介绍
--host
MySQL hostname (preferably a replica, not the master) (default "127.0.0.1")
-host指定连哪个库,优先推荐连从库。如果没指定,默认是127.0.0.1。
实践证明,这个参数指定哪个库的ip,就会从哪个库获取gh-ost增量同步的binlog
--allow-on-master
-allow-on-master
allow this migration to run directly on master. Preferably it would run on a replica
gh-ost默认就是在主库上建gho幽灵表的,这里说允许直接在主库上执行,经验证,意思是允许从主库读取binlog,如果想从主库读取binlog,则需要加上该参数,但是推荐从从库读取binlog。
--assume-master-host
(optional) explicitly tell gh-ost the identity of the master. Format: some.host.com[:port] This is useful in master-master setups where you wish to pick an explicit master, or in a tungsten-replicator where gh-ost is unable to determine the master
显式告诉gh-ost master地址,适用于双主复制。如果双主复制没加这个参数,会有报错:
FATAL There seems to be a master-master setup at ip:端口. This is unsupported. Bailing out
--chunk-size
amount of rows to handle in each iteration (allowed range: 10-100,000) (default 1000)
说明:每次迭代处理的行数量,默认是1000
调整方法:增大--chunk-size的值可以提高并发度,但也可能增加主库的工作负载。建议根据主库的负载能力和迁移表的规模来适当调整。
--dml-batch-size
batch size for DML events to apply in a single transaction (range 1-100) (default 10)
说明:在单个事务中应用DML事件的批处理大小,默认为10。
较大的--dml-batch-size值可以加快数据变更的速度,因为它减少了事务的数量,从而降低了事务提交的开销。然而,过大的值也可能导致事务过大,增加锁的竞争和死锁的风险,甚至可能超出数据库的限制,导致操作失败。
相反,较小的--dml-batch-size值可以降低锁的竞争和死锁的风险,因为它减少了每次事务处理的数据量。但是,这也会增加事务的数量和提交的开销,从而可能降低数据变更的整体速度
--max-load 和 --critical-load
说明:这两个参数用于设置gh-ost在迁移过程中对MySQL服务器负载的监控阈值。
-max-load string
Comma delimited status-name=threshold. e.g: 'Threads_running=100,Threads_connected=500'. When status exceeds threshold, app throttles writes
-critical-load string
Comma delimited status-name=threshold, same format as --max-load. When status exceeds threshold, app panics and quits
调整方法:通过调整这些参数,可以控制gh-ost在负载较高时是否进行节流(throttle)或退出。这间接地影响了并发度,因为当负载超过阈值时,gh-ost可能会降低迁移速度或停止迁移。
与-max-load参数相比,-critical-load更为严格。-max-load允许数据库在较高负载下继续运行,只是会采取节流措施(如减缓数据变更操作的速度)来避免对数据库性能造成过大影响。而-critical-load则是一种紧急停止机制,当负载达到或超过临界值时,gh-ost会立即停止所有数据变更操作并退出,以防止数据库负载进一步升高,保护数据库的稳定性和安全性。
因此,在设置-critical-load时,需要确保指定的阈值足够低,以便在数据库负载达到危险水平之前触发恐慌机制。同时,也需要根据数据库的性能特点和实际需求来合理设置这些阈值,以避免误触发或漏触发。请注意,触发恐慌机制后,gh-ost会立即停止运行,并且不会自动恢复。因此,在使用-critical-load参数时,需要确保有相应的监控和恢复机制来及时发现并处理gh-ost的退出事件,以确保数据变更操作的连续性和完整性。
2.6 怎样控制gh-ost
暂停
echo throttle | socat - /tmp/gh-ost.库名.表名.sock
如果执行该命令提示:
-bash: socat: 未找到命令
则需要先安装socat:
yum install socat
恢复
对应上面的暂停措施,让其继续执行。
echo no-throttle | socat - /tmp/gh-ost.库名.表名.sock
终止
touch /tmp/ghost.panic.flag
touch的文件对应panic-flag-file参数所指定的文件,当tmp目录存在该文件立即停止。
需要在执行gh-ost的时候指定panic-flag-file参数,如果没指定panic-flag-file参数,则创建flag文件的不起作用,无法终止该进程。
延迟切换(cut-over阶段)
--postpone-cut-over-flag-file=/tmp/ghost.postpone.flag
当设置该参数时cut-over一直延迟切换,直到你删除该文件才进行切换
动态调整性能参数
echo参数=新值 | socat - /tmp/gh-ost.库名.表名.sock
示例:
echo chunk-size=4000 | socat - /tmp/gh-ost.库名.表名.sock
三 gh-ost实践
3.1 下载安装
下载
#官网下载地址
Release GA release v1.1.6 · github/gh-ost · GitHub
#我csdn资源地址
https://download.csdn.net/download/YABIGNSHI/89915675
安装
rpm -ivh gh-ost-1.1.6-1.x86_64.rpm
3.2 三种模式验证
我这里的实验环境是:
A>B->C 级联复制(A同步到B,B同步到C)。
A和B是双主复制(A也指向B进行同步)。
A是主库,B,C只读(read_only=on)
3.2.1 连接到从库,在主库做迁移(默认模式)
想实现默认模式’连接到从库,在主库做迁移’有两种方法:
方法一:
登录到从服务器,不用加host参数,默认就是连的本地127.0.0.1。
方法二:
登录主服务器,加host参数,指定从库的ip。
建议用方法一,将gh-ost进程运行到从库上,以减轻主库的负载。
我这里登录从库B服务器,用方法一为t2表加个ename4的字段
gh-ost --user="root" -ask-pass --port="3306" --database="baidd" --table="t2" --alter="ADD COLUMN ename4 varchar(30)" --initially-drop-old-table --initially-drop-ghost-table -assume-master-host=主库ip:端口 --execute
输出日志:
Password:
[2024/10/17 16:41:32] [info] binlogsyncer.go:148 create BinlogSyncer with config {99999 mysql 127.0.0.1 3306 root false false <nil> false UTC true 0 0s 0s 0 false false 0 <nil>}
[2024/10/17 16:41:32] [info] binlogsyncer.go:374 begin to sync binlog from position (mysql-bin.000161, 300097463)
[2024/10/17 16:41:32] [info] binlogsyncer.go:791 rotate to (mysql-bin.000161, 300097463)
# Migrating `baidd`.`t2`; Ghost table is `baidd`.`_t2_gho`
# Migrating host01:3306; inspecting host01:3306; executing on host02
# Migration started at Thu Oct 17 16:41:32 +0800 2024
# chunk-size: 1000; max-lag-millis: 1500ms; dml-batch-size: 10; max-load: ; critical-load: ; nice-ratio: 0.000000
# throttle-additional-flag-file: /tmp/gh-ost.throttle
# Serving on unix socket: /tmp/gh-ost.baidd.t2.sock
Copy: 0/4 0.0%; Applied: 0; Backlog: 0/1000; Time: 0s(total), 0s(copy); streamer: mysql-bin.000161:300100541; Lag: 0.07s, HeartbeatLag: 0.07s, State: migrating; ETA: N/A
Copy: 4/4 100.0%; Applied: 0; Backlog: 0/1000; Time: 1s(total), 1s(copy); streamer: mysql-bin.000161:300105330; Lag: 0.06s, HeartbeatLag: 0.07s, State: migrating; ETA: due
Copy: 0/4 0.0%; Applied: 0; Backlog: 0/1000; Time: 1s(total), 1s(copy); streamer: mysql-bin.000161:300105330; Lag: 0.06s, HeartbeatLag: 0.07s, State: migrating; ETA: N/A
# Migrating `baidd`.`t2`; Ghost table is `baidd`.`_t2_gho`
# Migrating host01:3306; inspecting host01:3306; executing on host02
# Migration started at Thu Oct 17 16:41:32 +0800 2024
# chunk-size: 1000; max-lag-millis: 1500ms; dml-batch-size: 10; max-load: ; critical-load: ; nice-ratio: 0.000000
# throttle-additional-flag-file: /tmp/gh-ost.throttle
# Serving on unix socket: /tmp/gh-ost.baidd.t2.sock
Copy: 4/4 100.0%; Applied: 0; Backlog: 1/1000; Time: 2s(total), 1s(copy); streamer: mysql-bin.000161:300111505; Lag: 0.06s, HeartbeatLag: 0.07s, State: migrating; ETA: due
Copy: 4/4 100.0%; Applied: 0; Backlog: 0/1000; Time: 2s(total), 1s(copy); streamer: mysql-bin.000161:300111505; Lag: 0.06s, HeartbeatLag: 0.07s, State: migrating; ETA: due
[2024/10/17 16:41:35] [info] binlogsyncer.go:180 syncer is closing...
[2024/10/17 16:41:35] [info] binlogsyncer.go:864 kill last connection id 5521
[2024/10/17 16:41:35] [info] binlogsyncer.go:210 syncer is closed
# Done
总结:
从binlog位置来看,获取的是B这个从库的binlog。
从binlog日志(server_id)来看,建gho表,加字段,为gho表插入数据,切表的sql都是在主库A上执行的:
3.2.2 连接到主库,迁移过程所有操作都在主库上执行
#需要加上--allow-on-master参数
登录A服务器,执行:
gh-ost --allow-on-master --user="root" -ask-pass --port="3306" --database="baidd" --table="t2" --alter="ADD COLUMN ename6 varchar(30)" --initially-drop-old-table --initially-drop-ghost-table -assume-master-host=主库ip:端口 --execute
经验证,获取的是主库的binlog,并在主库创建gho表,rename gho表。
如果在主库服务器执行该命令,没加--allow-on-master参数,会报错:
2024-10-17 15:59:57 FATAL It seems like this migration attempt to run directly on master. Preferably it would be executed on a replica (and this reduces load from the master). To proceed please provide --allow-on-master. Inspector config=127.0.0.1:3306, user=root, usingTLS=false, applier config=127.0.0.1:3306, user=root, usingTLS=false
3.2.3 在从库做迁移
想实现模式三“在从库做迁移测试”,需要确保:
- 从库没有开启只读(read_only,super_read_only=off)
- 配置双向复制,否则从库上执行的建gho表,ddl变更等操作无法同步的主库。
3、需要加参数migrate-on-replica
在B服务器执行:
[root@host01 ~]# gh-ost --user="root" -ask-pass --port="3306" --database="baidd" --table="t2" --alter="ADD COLUMN ename7 varchar(30)" --initially-drop-old-table --initially-drop-ghost-table --migrate-on-replica -assume-master-host=主库ip:端口 --execute
Password:
[2024/10/17 18:38:04] [info] binlogsyncer.go:148 create BinlogSyncer with config {99999 mysql 127.0.0.1 3306 root false false <nil> false UTC true 0 0s 0s 0 false false 0 <nil>}
[2024/10/17 18:38:04] [info] binlogsyncer.go:374 begin to sync binlog from position (mysql-bin.000161, 300155403)
[2024/10/17 18:38:04] [info] binlogsyncer.go:791 rotate to (mysql-bin.000161, 300155403)
2024-10-17 18:38:04 ERROR Error 1290: The MySQL server is running with the --super-read-only option so it cannot execute this statement
2024-10-17 18:38:04 FATAL Error 1290: The MySQL server is running with the --super-read-only option so it cannot execute this statement
提示该数据库只读,无法执行。说明是真的在从库上建幽灵表。
把从库改为可写,就可以执行了。但是还得改参数,建议还是用模式一。
3.3 大表DDL实践
上面为了验证基本语法与三种模式,用的都是小表,这里对大表做下DDL。
这里用到了几个性能参数:
gh-ost --user="root" -ask-pass --port="3306" --database="zcx" --table="my_sensor_state" --alter="add index idx_record_id(record_id)" --chunk-size=3000 --dml-batch-size=50 --max-load=Threads_running=100 --critical-load=Threads_running=200 --initially-drop-old-table --initially-drop-ghost-table -assume-master-host=主库ip:端口 --execute
在执行期间,人工关注下cpu,io使用率,若太高了,则手动调整下相关参数。
我这里的实验环境是4核cpu,为一千四百万条数据的表加索引期间,cpu使用率50%-70%,历时20分钟左右。
发现获取binlog耗费不了多少cpu,主要是在哪个库建gho表,哪个库cpu使用率高。我这里是主库负载高,从库负载不高。
经验证,在ghost执行DDL期间,查询源表my_sensor_state没有锁表。理论上,只有在cut over的时候才会锁表,但只锁定很短时间(一秒左右)
可以根据输出查看进度:
也可以手动查询下影子表的数量,以查看进度:
select count(*) from `zcx`.`_my_sensor_state_gho`
#验证索引是否添加成功
show indexes from 表名;
3.4 gh-ost暂停、恢复、终止、延迟切换,动态修改
#执行DDL
gh-ost --user="root" -ask-pass --port="3306" --database="zcx" --table="my_sensor_state" --alter="add index idx_record_id_2(record_id)" --chunk-size=3000 --dml-batch-size=50 --max-load=Threads_running=100 --critical-load=Threads_running=200 --initially-drop-old-table --initially-drop-ghost-table -assume-master-host=主库ip:端口 --execute
暂停
echo throttle | socat - /tmp/gh-ost.zcx.my_sensor_state.sock
发现将其暂停后,gh-ost进程还在,sock文件也还在。
左列行数就没再更新了:
恢复-继续DDL
echo no-throttle | socat - /tmp/gh-ost.zcx.my_sensor_state.sock
终止
gh-ost --user="root" -ask-pass --port="3306" --database="zcx" --table="my_sensor_state" --alter="add index idx_record_id_3(record_id)" --chunk-size=3000 --dml-batch-size=50 --max-load=Threads_running=100 --critical-load=Threads_running=200 --initially-drop-old-table --initially-drop-ghost-table --panic-flag-file=/tmp/ghost.panic.flag -assume-master-host=主库ip:端口 --execute
在开始ddl后,若想终止,则执行:
touch /tmp/ghost.panic.flag
若想重新执行,则需要删除该flag文件及sock文件,重新执行gh-ost命令。
动态调参
这里将chunk-size改为4000:
延迟切换
gh-ost --user="root" -ask-pass --port="3306" --database="zcx" --table="my_sensor_state" --alter="add index idx_record_id(record_id)" --chunk-size=3000 --dml-batch-size=50 --max-load=Threads_running=100 --critical-load=Threads_running=200 --initially-drop-old-table --initially-drop-ghost-table --panic-flag-file=/tmp/ghost.panic.flag --postpone-cut-over-flag-file=/tmp/ghost.postpone.flag -assume-master-host=主库ip:端口 --execute
可以看到自动生成了该文件:
在数据同步(从my_sensor_state到_my_sensor_state_gho)完10分钟后,验证gho表是否还存在,验证该表数据量:
select count(*) from `zcx`.`_my_sensor_state_gho`
表还存在,说明没发生切表,如果切表了,gho表就已经不存在了。
rm /tmp/ghost.postpone.flag
删完该文件后,发现DDL立马就完成了:
四 gh-ost使用总结
4.1 下载安装
下载
#官网下载地址
Release GA release v1.1.6 · github/gh-ost · GitHub
#我csdn资源地址
https://download.csdn.net/download/YABIGNSHI/89915675
安装
rpm -ivh gh-ost-1.1.6-1.x86_64.rpm
4.2 推荐使用模式
推荐使用默认模式-连接到从库,在主库做迁移。
4.3 gh-ost常用参数
推荐登录到从库服务器,执行带如下参数的命令执行DDL:
gh-ost --user="root" -ask-pass --port="3306" --database="zcx" --table="my_sensor_state" --alter="add index idx_record_id(record_id)" --chunk-size=3000 --dml-batch-size=50 --max-load=Threads_running=100 --critical-load=Threads_running=200 --initially-drop-old-table --initially-drop-ghost-table --panic-flag-file=/tmp/ghost.panic.flag --postpone-cut-over-flag-file=/tmp/ghost.postpone.flag -assume-master-host=主库ip:端口 --execute
4.4 gh-ost动态控制
暂停
echo throttle | socat - /tmp/gh-ost.库名.表名.sock
如果执行该命令提示:
-bash: socat: 未找到命令
则需要先安装socat:
yum install socat
恢复
对应上面的暂停措施,让其继续执行。
echo no-throttle | socat - /tmp/gh-ost.库名.表名.sock
终止
touch /tmp/ghost.panic.flag
touch的文件对应panic-flag-file参数所指定的文件,当tmp目录存在该文件立即停止。
需要在执行gh-ost的时候指定panic-flag-file参数,如果没指定panic-flag-file参数,则创建flag文件的不起作用,无法终止该进程。
延迟切换(cut-over阶段)
--postpone-cut-over-flag-file=/tmp/ghost.postpone.flag
当设置该参数时cut-over一直延迟切换,直到你删除该文件才进行切换
动态调整性能参数
echo参数=新值 | socat - /tmp/gh-ost.库名.表名.sock
示例:
echo chunk-size=4000 | socat - /tmp/gh-ost.库名.表名.sock
--本篇文章主要参考自:
GitHub - github/gh-ost: GitHub's Online Schema-migration Tool for MySQL