MySQL gh-ost 线上表结构变更 (Online DDL) 生产实战全指南
🚀 gh-ost 线上表结构变更 (Online DDL) 生产实战全指南
涵盖核心前置条件、4大实战场景命令、全量参数深度解析及血泪避坑宝典
📑 目录导航
- 一、 战前检查:gh-ost 核心前置条件
- 二、 生产实战命令模板 (4大典型场景)
- 三、 核心参数深度解析 (全量详细版)
- 四、 实战避坑指南 (血泪经验总结)
- 五、 架构禁区:为何禁用外键与触发器?
一、 战前检查:gh-ost 核心前置条件 (MySQL 5.6 / 5.7)
gh-ost 是一款无触发器的在线 DDL 工具。在执行前,必须确保环境满足以下硬性条件,否则工具会直接拒绝执行:
| 检查项 | 要求 | MySQL 5.6 与 5.7 的差异注意 |
|---|---|---|
| Binlog 格式 | 必须为 ROW |
5.6/5.7 均需确认,不能是 STATEMENT 或 MIXED。 |
| Binlog Row Image | 必须为 FULL |
5.6 默认可能是 MINIMAL,必须手动改为 FULL! 5.7 默认已是 FULL。 |
| 主键/唯一键 | 必须有主键或非空唯一键 | 无差异。gh-ost 强依赖主键进行数据分块(Chunk)拷贝。 |
| 外键与触发器 | 绝对不能有 | 无差异。有外键或触发器的表 gh-ost 会直接拒绝执行(详见第五章)。 |
| 字符集 | UTF8 或 UTF8MB4 | 不支持 latin1 等老旧字符集。 |
二、 生产实战命令模板 (4大典型场景)
场景 1:无从库/单节点环境,直连主库操作 真实跑通案例
适用场景: 单机 MySQL,或没有配置主从复制的环境。所有读写压力都在主库上,必须严格限制 gh-ost 的资源占用。
--password='...' 单引号传参解决环境变量失效问题;加入 -timestamp-old-table 防止多次 DDL 旧表名冲突。nohup gh-ost \ --user="ghost" \ --host="192.168.98.11" \ --password='YourRealPassword' \ --port=3306 \ --database="scts" \ --table="scts_etl_data_record_log" \ --alter="ADD COLUMN new_col INT DEFAULT 0 COMMENT '新字段', ADD INDEX idx_new_col (new_col)" \ --allow-on-master \ --execute \ --postpone-cut-over-flag-file="/tmp/gh-ost.postpone.flag" \ --panic-flag-file="/tmp/gh-ost.panic.flag" \ --max-load="Threads_running=30,Threads_connected=500" \ --critical-load="Threads_running=100,Threads_connected=1000" \ --chunk-size=1000 \ --dml-batch-size=10 \ --default-retries=120 \ --heartbeat-interval-millis=2000 \ --cut-over-lock-timeout-seconds=3 \ --initially-drop-ghost-table \ --initially-drop-old-table \ --serve-socket-file="/tmp/gh-ost.scts.scts_etl_data_record_log.sock" \ --verbose \ -timestamp-old-table \ > /var/log/gh-ost_scts_$(date +%Y%m%d%H%M).log 2>&1 &
场景 2:有主从架构,在主库执行,监控从库延迟 (兼顾便利与安全)
适用场景: 有从库,主库性能较好。在主库执行拷贝,但必须监控从库延迟,防止主从脱节影响读写分离业务。
nohup gh-ost \ --user="ghost" --password='YourRealPassword' \ --host="10.0.0.2" --port=3306 \ --database="mydb" --table="mytable" \ --alter="ADD INDEX idx_status (status)" \ --execute \ --postpone-cut-over-flag-file="/tmp/gh-ost.postpone.flag" \ --panic-flag-file="/tmp/gh-ost.panic.flag" \ --throttle-control-replicas="10.0.0.2:3306,10.0.0.3:3306" \ --max-lag-millis=1500 \ --max-load="Threads_running=30,Threads_connected=500" \ --critical-load="Threads_running=100,Threads_connected=1000" \ --critical-load-interval-millis=5000 \ --chunk-size=1000 --dml-batch-size=10 --default-retries=120 \ --heartbeat-interval-millis=2000 --cut-over-lock-timeout-seconds=3 \ --initially-drop-ghost-table --initially-drop-old-table --initially-drop-socket-file \ --serve-socket-file="/tmp/gh-ost.mydb.mytable.sock" \ --verbose -timestamp-old-table \ > /var/log/gh-ost_master_discovery.log 2>&1 &
场景 3:有主从架构,在从库执行 (大表/高并发终极方案)
适用场景: 表非常大(几十GB以上),或主库已处于高负载。将数据拷贝压力转移到从库,对主库几乎零读压力。
nohup gh-ost \ --user="ghost" --password='YourRealPassword' \ --host="10.0.0.2" --port=3306 \ --database="mydb" --table="mytable" \ --alter="ADD COLUMN age INT" \ --migrate-on-replica \ --assume-master-host="10.0.0.1:3306" \ --execute \ --postpone-cut-over-flag-file="/tmp/gh-ost.postpone.flag" \ --panic-flag-file="/tmp/gh-ost.panic.flag" \ --max-load="Threads_running=30,Threads_connected=500" \ --critical-load="Threads_running=100,Threads_connected=1000" \ --critical-load-interval-millis=5000 \ --chunk-size=1000 --dml-batch-size=10 --default-retries=120 \ --heartbeat-interval-millis=2000 --cut-over-lock-timeout-seconds=3 \ --initially-drop-ghost-table --initially-drop-old-table --initially-drop-socket-file \ --serve-socket-file="/tmp/gh-ost.mydb.mytable.sock" \ --verbose -timestamp-old-table \ > /var/log/gh-ost_replica_mode.log 2>&1 &
场景 4:开发/测试环境 (狂飙模式)
适用场景: 测试环境,不关心负载,不需要延迟切换,只求最快跑完。
gh-ost \ --user="root" --password='123456' \ --host="127.0.0.1" --port=3306 \ --database="testdb" --table="testtable" \ --alter="ADD COLUMN test_col VARCHAR(50)" \ --allow-on-master --execute \ --chunk-size=5000 --dml-batch-size=50 \ --ok-to-drop-table --initially-drop-socket-file --verbose
战中干预与战后收尾
# 战中干预 (通过 Socket 动态调整) SOCK="/tmp/gh-ost.scts.scts_etl_data_record_log.sock" echo "status" | nc -U $SOCK # 查看进度 echo "sup" | nc -U $SOCK # 暂停数据拷贝 (高峰期使用) echo "chunk-size=200" | nc -U $SOCK # 动态调小批次 # 战后收尾 (最终切换) rm -f /tmp/gh-ost.postpone.flag # 删除延迟文件触发 RENAME
三、 核心参数深度解析 (全量详细版)
1. 核心执行与切换控制
| 参数 | 作用解释 | 生产实战建议 |
|---|---|---|
--execute |
真正执行变更。如果不加此参数,gh-ost 会进行 Dry-Run(试运行),只创建幽灵表并解析 Binlog,但不拷贝数据。 | 必须加。 但在第一次跑或者对 DDL 语句没把握时,先去掉它跑一次 Dry-Run。 |
--allow-on-master |
允许直接在主库上执行。gh-ost 默认出于安全考虑拒绝直连主库。 | 如果架构较小或主库性能过剩,加上此参数直连主库最方便。核心大表建议不加,改用从库执行模式。 |
--postpone-cut-over-flag-file |
延迟切换标志文件。只要这个文件存在,gh-ost 就会一直追平 Binlog,但绝对不执行最后的 RENAME TABLE 换表动作。 | 强烈建议配置。 让 gh-ost 在白天把数据拷完,等半夜低峰期手动 rm 删除这个文件,让它瞬间完成切换。 |
--panic-flag-file |
紧急刹车文件。在运行过程中,只需 touch 创建这个文件,gh-ost 会立刻安全退出,不进行任何破坏性操作。 | 强烈建议配置。 这是你的“后悔药”和“紧急停止按钮”。 |
-timestamp-old-table |
给备份的旧表加上时间戳后缀(如 _20260525_del)。 |
强烈建议。 避免同一张表多次 DDL 时,因旧表名冲突导致任务失败。 |
--migrate-on-replica |
在从库上执行迁移。告诉 gh-ost 当前连接的是从库,所有数据拷贝在从库完成。 | 配合 --assume-master-host 使用。适用于超大表,将对主库的读 I/O 影响降到最低。 |
--assume-master-host |
指定主库地址。当使用从库执行模式时,告诉 gh-ost 主库的 IP 和端口。 | 从库模式下必填。格式如 10.0.0.1:3306。 |
2. 安全保护与限流 (Throttling)
| 参数 | 作用解释 | 生产实战建议 |
|---|---|---|
--max-load |
软限制(暂停阈值)。当主库负载达到此阈值时,gh-ost 会暂停数据拷贝(但继续追 Binlog),直到负载下降。 | 格式:指标=阈值。建议:Threads_running=25。根据主库日常峰值设置,略高于日常峰值即可。 |
--critical-load |
硬限制(熔断阈值)。当负载达到此阈值时,gh-ost 认为数据库处于危险边缘,直接触发 Panic 中止并退出。 | 建议:Threads_running=80。这是保命参数,防止 DDL 导致主库雪崩。 |
--max-lag-millis |
主从延迟限制。如果监控到从库的延迟超过设定的毫秒数,暂停数据拷贝。 | 建议:1500(1.5秒)。防止因为 DDL 导致主从延迟过大。 |
--throttle-control-replicas |
指定要监控延迟的从库列表。如果不指定,gh-ost 可能只监控直连的那个从库。 | 建议填入所有核心从库地址,用逗号分隔:10.0.0.2:3306,10.0.0.3:3306。 |
3. 性能与拷贝微调
| 参数 | 作用解释 | 生产实战建议 |
|---|---|---|
--chunk-size |
每次拷贝的行数。gh-ost 每次从原表 SELECT 多少行,然后 INSERT 到幽灵表。 | 默认 1000。如果主库 I/O 压力大,可通过 Socket 动态调小至 200;系统闲时可调大至 2000。 |
--dml-batch-size |
增量事件应用批次。每次从 Binlog 中读取并应用到幽灵表的 DML 事件数量。 | 默认 10。通常保持默认,增量极大且 CPU 充足时可适当调大。 |
--default-retries |
默认重试次数。遇到可重试错误(如死锁、锁等待超时)时的重试次数。 | 默认 60。生产环境建议调大至 120,防止偶尔的业务长事务导致任务失败。 |
4. 容错与清理机制
| 参数 | 作用解释 | 生产实战建议 |
|---|---|---|
--initially-drop-ghost-table |
自动清理幽灵表。如果上次异常退出留下了 _xxx_gho 表,启动时自动 Drop。 |
建议加上。 省去手动清理残留表的麻烦。 |
--initially-drop-old-table |
自动清理旧表。同上,针对上次留下的 _xxx_old 或 _xxx_del 表。 |
建议加上。 |
--ok-to-drop-table |
成功后自动删除原表。切换成功后,原表会被重命名为 _xxx_del,加此参数会自动 Drop。 |
⚠️ 生产环境强烈建议不加! 保留旧表观察几天,确认无 Bug 后再手动 DROP,保留物理回滚机会。 |
5. 交互与日志
| 参数 | 作用解释 | 生产实战建议 |
|---|---|---|
--serve-socket-file |
开启 Unix Socket 交互。允许在运行期间通过 nc 命令发送指令(如 status, sup)。 | 必须配置。 这是 gh-ost 的灵魂,让你能动态调优。 |
--verbose |
输出详细日志。打印详细的执行步骤、拷贝进度等。 | 必须加上。 排错时全靠它。 |
四、 实战避坑指南 (血泪经验总结) 高危预警
坑 1:多行命令续行失败 (报“未找到命令”)
-bash: --allow-on-master: 未找到命令
根本原因:在多行命令中插入了 # 注释,或者反斜杠 \ 后面不小心多敲了一个空格。Bash 认为上一行已经结束,把参数当成了独立命令。
解决办法:确保 \ 必须是该行的绝对最后一个字符;多行命令中绝对不要写 # 注释。
坑 2:环境变量密码失效 (报 using password: NO)
FATAL Error 1045: Access denied... (using password: NO),即使 echo $MYSQL_PWD 有值。
根本原因:Go 语言编写的 gh-ost 在配合 nohup 后台运行时,有时无法正确继承 Shell 的环境变量。
解决办法:放弃环境变量,直接在命令中使用 --password='你的密码'。务必使用单引号,防止密码中的 ! $ * 等特殊字符被 Shell 提前解析。
坑 3:使用了不存在的参数 (打印满屏 Help 文档)
根本原因:使用了 gh-ost 不支持的参数(例如误用了 pt-osc 的 --timestamp 参数)。
解决办法:gh-ost 日志默认自带时间戳,不需要额外加时间戳参数;若要给旧表加时间戳,请使用 -timestamp-old-table。
坑 4:alter 语句解析为空 (报 statement must not be empty)
FATAL --alter must be provided and statement must not be empty
根本原因:如果使用配置文件(.conf),在 alter= 后面加了双引号;或者多行命令中 --alter="..." 这一行的 \ 后面有空格导致截断。
解决办法:配置文件中 alter=ADD COLUMN... 不要加引号;或者直接改用标准多行命令传递。
五、 架构禁区:为何 gh-ost 绝对禁用外键与触发器?
gh-ost 的核心原理(幽灵表 + Binlog 增量同步 + RENAME 切换)与 MySQL 外键/触发器的底层元数据管理机制是天生互斥的。
🚫 为什么不能有“外键”?
- 如果原表是“子表”: 幽灵表在创建时为了顺利拷贝数据,gh-ost 已经主动剥离了外键定义。切换完成后,新表将丢失外键约束,破坏参照完整性。
- 如果原表是“父表”: 当尝试把原表 RENAME 为
_old时,MySQL 引擎会直接报错拦截(ERROR 1217),因为其他子表的外键正“咬”着这张表。
🚫 为什么不能有“触发器”?
- 数据不一致: 业务写原表 -> 触发器执行。gh-ost 读 Binlog 写幽灵表 -> 幽灵表没触发器,不执行。导致依赖触发器产生的关联数据丢失。
- 双倍执行灾难: 如果强行让幽灵表保留触发器,gh-ost 应用 Binlog 时触发器会再次执行,导致逻辑被双倍执行(如库存扣减两次)。
ALGORITHM=INPLACE, LOCK=NONE)。如果原生 DDL 不支持该操作(如修改字段类型),则必须安排停机维护。

浙公网安备 33010602011771号