MySQL gh-ost 线上表结构变更 (Online DDL) 生产实战全指南

🚀 gh-ost 线上表结构变更 (Online DDL) 生产实战全指南

涵盖核心前置条件、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 文档)

报错现象:进程瞬间退出(退出码 2),日志里全是 gh-ost 的帮助文档。

根本原因:使用了 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 时触发器会再次执行,导致逻辑被双倍执行(如库存扣减两次)。
🛑 替代方案:对于带有外键或触发器的表,请使用MySQL 原生 Online DDL(ALGORITHM=INPLACE, LOCK=NONE)。如果原生 DDL 不支持该操作(如修改字段类型),则必须安排停机维护

 

posted @ 2026-05-25 17:54  VicLW  阅读(15)  评论(0)    收藏  举报