gh-ost 改表不停写:binlog 追增量,两个连接抢 rename 完成 cut-over
gh-ost 是 GitHub 开源的 online schema migration 工具,Go 编写,MIT 许可。仓库在 github/gh-ost,二进制从 releases 下载,文档都在仓库的 doc/ 目录:requirements-and-limitations.md、why-triggerless.md、triggerless-design.md、cheatsheet.md、interactive-commands.md、throttle.md、testing-on-replica.md、perks.md、rds.md、azure.md。它是 triggerless 方案,支持可暂停、动态控制与审计。
为什么需要 ghost 表
ALTER TABLE 会对表加 MDL 元数据写锁,大表上可能导致数小时内无法写入,直接阻塞业务。所有 online schema change 工具走的是同一套路:先建一张结构相同(或先改好结构)的幽灵表,按主键分批把原表数据 copy 过去,同时把进行中的 INSERT/DELETE/UPDATE 传播到幽灵表,最后用 rename 原子替换原表。
无触发器:binlog 替掉 trigger
pt-online-schema-change(pt-osc)和 Facebook OSC 都用触发器:在原表上建 INSERT/UPDATE/DELETE 三个触发器把增量同步到新表。触发器在原表事务内同步执行,等于把两张表的锁并集塞进同一个事务空间,锁竞争变重,业务高峰期会加重主库负载。
gh-ost 不建触发器,改为订阅并解析 binlog 捕获原表变更,异步 apply 到幽灵表 _gho。迁移的写入负载因此与主库业务负载解耦,整个过程可暂停、可动态控制/重配、可审计。它的做法是把自己伪装成一个 MySQL 从库,从主库或从库拉 binlog,过滤出目标表事件后重放到主库的 _gho 上。
迁移流程
- 校验:检查外键、触发器、主键/唯一键信息,预估行数,判断主库还是从库,检查
log_slave_updates与 binlog 配置,确认_gho/_del临时表不存在。 - 创建
_ghc表记录迁移进度与 binlog 位点/心跳;按--alter语句创建_gho并完成改结构。 - 初始化 binlog stream 连接,开始监听。
- 迁移:开事务按主键 id 分 chunk 把原表数据写入
_gho,提交;同时异步 apply binlog。以 binlog 为权威:insert 若 copy 在前用replace into、copy 在后用insert ignore;update/delete 对已 copy 的行整列覆盖,对未 copy 的行先空操作等 copy 追上。 - cut-over:lock 源表 → rename 源表为
_del、_gho为源表 → 清理_ghc、删除 socket 文件。
cut-over 怎么做到原子
MySQL 的 rename 是原子的,而且在所有被 blocked 的请求里 rename 优先级最高。gh-ost 用两个连接配合:一个连接对原表加锁,另一个连接发起 rename 并被阻塞;释放锁时 rename 第一个拿到执行权,其他被阻塞的请求随后落到新表上。--cut-over-lock-timeout-seconds 控制 cut-over 超时,也就是 MDL 锁的持有时长——这段时间就是切换窗口。
三种运行模式
- 默认:连从库拿信息、在主库执行(查从库情况、取主库信息后连到主库,对主库侵入最小)。
--allow-on-master:直连主库执行。担心从库延迟时可以直读主库 binlog。Statement Based Replication 下必须用从库模式。--test-on-replica:在从库上跑完整迁移,完成后 stop slave,源表与幽灵表交换后立刻换回,源表实际没被改,可对比两张表验证正确性。不想 stop slave 就加--test-on-replica-skip-replica-stop。GitHub 在生产环境就是持续在从库上跑这个来验证。
硬约束与前置条件
- 表必须有主键或唯一索引。
- 不支持有外键引用的表,也不支持带触发器的表。
- 要求
binlog_format=ROW、binlog_row_image=FULL(或 NOBLOB);MariaDB 需要对应设置。 - 迁移期间数据量翻倍,需要额外存储空间。
- 存在同名但大小写不同的表时无法修改。
- 使用 DTS 等同步可能导致同步失败。
关键参数与实操
改表语句由 --alter 指定,加 --execute 才真正执行,不加就是只做校验的 noop dry-run。先跑 dry-run:
gh-ost --host=... --database=... --table=t1 \
--alter="add column c2 int" \
--allow-on-master
确认输出无误后再加 --execute:
gh-ost --host=... --database=... --table=t1 \
--alter="add column c2 int" \
--allow-on-master --execute
常用参数:
--exact-rowcount:准确进度--postpone-cut-over-flag-file:把 cut-over 推迟到指定时间/文件--cut-over-lock-timeout-seconds:cut-over 超时--chunk-size:默认 1000,范围 1–100000--max-load/--critical-load:限流阈值--switch-to-rbr:把 binlog 转成 row 格式--allow-on-master--initially-drop-old-table--approve-renamed-columns--test-on-replica
另外支持 unix socket / TCP 查询状态、交互式重配与 hooks。
中断恢复与 pt-osc 的差异
pt-osc 中断后重跑会丢弃之前所有操作,从头开始。gh-ost 中断后重跑会读 _ghc 心跳表,从未完成的位点继续。
增量同步方式也是两者分界:pt-osc 用触发器,gh-ost 用 binlog。从从库拉 binlog 对主库影响更小,迁移可暂停、可限流,切换时间可以设定。