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 / 限流参数。