案例 MySQL 大表加字段用 gh-ost:三场景命令、关键参数取舍与五处高发坑

2026-09-02 21:32:57

gh-ost 是 GitHub 开源的在线 DDL 工具(github/gh-ost,Go,MIT,约 13.5k star)。它执行大表 ALTER 时不用触发器,而是模拟一个从库拉取并解析 ROW 格式 binlog,把增量变更异步应用到镜像表,最后原子 RENAME 换表。相比 pt-osc 依赖触发器同步,gh-ost 没有触发器的性能开销,可以真正暂停、动态限流、可测试,中途宕机不影响原表。本文是生产实操指南。

一、执行前硬性前置条件(不满足会被拒绝执行)

  • binlog_format 必须为 ROW(5.6/5.7 都要确认,不能是 STATEMENT/MIXED)
  • binlog_row_image 必须为 FULL。MySQL 5.6 默认可能是 MINIMAL,需要手动改;5.7 默认已是 FULL
  • 表必须有主键或非空唯一键(gh-ost 靠它做 chunk 分块拷贝)
  • 表上不能有外键和触发器(有会直接拒绝,见第五节)
  • 字符集用 UTF8/UTF8MB4,不支持 latin1

二、典型生产命令(三场景)

场景 1:单机/无主从,直连主库

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.log 2>&1 &

场景 2:有主从,在主库执行、监控从库延迟(照顾读写分离)

--throttle-control-replicas="10.0.0.2:3306,10.0.0.3:3306" --max-lag-millis=1500 --critical-load-interval-millis=5000

场景 3:超大表/高并发,在从库执行(对主库零读压力)

--migrate-on-replica --assume-master-host="10.0.0.1:3306"

运行中干预(socket 动态控制):

SOCK=/tmp/gh-ost.xxx.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)

三、关键参数取舍

  • --execute:真正执行;不加则 Dry-Run(建幽灵表解析 binlog 但不拷贝数据)。首次或 DDL 没把握先 Dry-Run。
  • --allow-on-master:允许直连主库。gh-ost 默认出于安全拒绝直连主库。小架构可加,核心大表建议改从库模式。
  • --postpone-cut-over-flag-file:延迟切换标志文件。文件存在就一直追平 binlog 但不执行最后 RENAME。白天拷完数据,半夜低峰手动 rm 触发瞬间切换。建议配置。
  • --panic-flag-file:紧急刹车,touch 即安全退出。建议配置。
  • -timestamp-old-table:旧表加时间戳后缀,避免多次 DDL 旧表名冲突。
  • --max-load(软限/暂停阈值,建议 Threads_running=25 左右):达到暂停拷贝但继续追 binlog。
  • --critical-load(硬限/熔断,建议 Threads_running=80):达到直接 panic 退出,防主库雪崩。
  • --max-lag-millis:主从延迟超阈值暂停,建议 1500。
  • --throttle-control-replicas:指定要监控延迟的从库列表,建议全填。
  • --chunk-size:每次拷贝行数,默认 1000,I/O 压力大可用 socket 调小到 200。
  • --dml-batch-size:增量事件应用批次,默认 10。
  • --default-retries:可重试错误(死锁/超时)的重试次数,生产建议调大到 120。
  • --initially-drop-ghost-table / --initially-drop-old-table:启动时自动清理上次残留表。
  • --ok-to-drop-table:成功后自动删原表。生产不建议加,保留旧表观察几天,确认无问题再手动 DROP,留物理回滚机会。
  • --serve-socket-file + --verbose:socket 交互 + 详细日志,都建议开。

四、高频翻车点

坑 1:多行命令续行失败(-bash: --allow-on-master: 未找到命令)。反斜杠后多了空格,或行里插了 # 注释。确保 \ 是该行绝对最后字符,多行里不写注释。

坑 2:环境变量密码失效(using password: NO)。Go 写的 gh-ost 配 nohup 有时不继承环境变量。直接用 --password='密码',用单引号防止 ! $ * 被 shell 解析。

坑 3:用了不存在的参数(退出码 2,满屏 help)。gh-ost 自带时间戳,别加 pt-osc 的 --timestamp;要旧表时间戳用 -timestamp-old-table

坑 4:alter 解析为空(statement must not be empty)。配置文件里 alter= 别加引号;多行命令该行 \ 后别带空格。

坑 5:磁盘空间。镜像表含全表数据多占约一倍磁盘,binlog_row_image=FULL 又放大 binlog,先 df -h 确认。

坑 6:主从延迟。务必配 --max-lag-millis--throttle-control-replicas,防从库跟不上读旧数据。

五、为什么禁用外键与触发器

原表是子表:gh-ost 建幽灵表时已剥离外键,切换后新表丢外键约束。

原表是父表:把原表 RENAME 成 _old 时 MySQL 直接报 ERROR 1217,其他子表外键咬着它。

触发器:业务写原表 → 触发器执行;gh-ost 从 binlog 写幽灵表 → 幽灵表没触发器 → 关联数据丢失。若强行保留触发器,应用 binlog 时触发器二次执行,逻辑被双倍执行(如扣库存两次)。

替代方案:这类表用 MySQL 原生 Online DDL(ALGORITHM=INPLACE, LOCK=NONE);原生不支持(如改字段类型)就安排停机维护。

gh-ost 适合有主键、无外键触发器、binlog 为 ROW 的互联网高并发大表。小表低峰用原生 ALTER 最省事,带外键触发器的表用原生 INPLACE 或停机,几十 GB 高并发核心表才值得上 gh-ost,并配齐 postpone / panic / 限流参数。

复制全文 生成海报 gh-ost MySQL 在线DDL 运维 表结构变更 binlog

推荐文章

程序员茄子在线接单