编程 gh-ost 改表不停写:binlog 追增量,两个连接抢 rename 完成 cut-over

2026-10-15 01:39:38

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 上。

迁移流程

  1. 校验:检查外键、触发器、主键/唯一键信息,预估行数,判断主库还是从库,检查 log_slave_updates 与 binlog 配置,确认 _gho/_del 临时表不存在。
  2. 创建 _ghc 表记录迁移进度与 binlog 位点/心跳;按 --alter 语句创建 _gho 并完成改结构。
  3. 初始化 binlog stream 连接,开始监听。
  4. 迁移:开事务按主键 id 分 chunk 把原表数据写入 _gho,提交;同时异步 apply binlog。以 binlog 为权威:insert 若 copy 在前用 replace into、copy 在后用 insert ignore;update/delete 对已 copy 的行整列覆盖,对未 copy 的行先空操作等 copy 追上。
  5. 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 对主库影响更小,迁移可暂停、可限流,切换时间可以设定。

推荐文章

程序员茄子在线接单