前言
做过 MySQL 运维的朋友都知道,线上表结构变更(DDL)是一件让人头疼的事情。表小的时候无所谓,ALTER TABLE 几秒钟就完事了。但当表达到几千万、上亿行的时候,一个简单的加字段操作就可能锁表几十分钟甚至几个小时,业务直接就挂了。
长期以来,Percona 的 pt-online-schema-change(pt-osc)是大家的首选方案。它通过创建影子表 + 触发器的方式实现在线 DDL,确实解决了大部分问题。但触发器方案本身也有不少坑,尤其是在高并发场景下。
2016 年,GitHub 开源了 gh-ost(GitHub's Online Schema Transmogrifier),提出了一种完全不依赖触发器的在线 DDL 方案。经过多年的生产环境验证,gh-ost 已经成为很多公司的首选 DDL 工具。
本文将从原理到实战,带你全面掌握 gh-ost。
一、为什么需要 gh-ost?pt-osc 触发器方案的痛点
在聊 gh-ost 之前,我们先回顾一下 pt-osc 的工作方式以及它存在的问题。
1.1 pt-osc 的基本原理
pt-osc 的核心流程是:
- 创建一张与原表结构相同的影子表(
_tablename_new)
- 在影子表上执行
ALTER TABLE
- 在原表上创建 INSERT / UPDATE / DELETE 三个触发器
- 分批将原表数据拷贝到影子表
- 拷贝完成后,通过
RENAME TABLE 原子切换
1.2 触发器方案的问题
这个方案看起来很优雅,但触发器本身带来了不少麻烦:
性能影响大: 触发器是同步执行的。每一条对原表的 INSERT / UPDATE / DELETE 操作,都会同步触发对影子表的写入。在高并发场景下,这意味着每条 DML 的执行时间直接翻倍,RT(响应时间)显著上升。
锁竞争加剧: 触发器在同一个事务中执行,原表和影子表的锁会相互影响。当存在热点行时,锁等待和死锁的概率大幅增加。
无法暂停: pt-osc 一旦开始运行,触发器就已经装上了。即使你暂停了数据拷贝,触发器仍然在工作,仍然在影响线上性能。想要彻底停止,只能 kill 掉进程并手动清理触发器和影子表。
测试困难: 你没有办法在从库上先跑一遍测试,看看影响到底有多大。触发器必须在写入端(主库)创建。
binlog 膨胀: 触发器产生的额外 DML 会写入 binlog,导致主从延迟加大。
这些问题在 GitHub 这种级别的业务场景下尤为突出。GitHub 的 MySQL 集群承载着极高的写入并发,触发器方案带来的性能抖动是不可接受的。于是,他们开发了 gh-ost。
二、gh-ost 架构与工作原理
gh-ost 最核心的设计理念就是:用 binlog 流式解析替代触发器。
2.1 整体流程
gh-ost 的迁移过程可以概括为以下几步:
- 建立连接:连接到 MySQL 实例(主库或从库),检查表结构、binlog 格式等前置条件
- 创建 ghost 表:在主库上创建
_tablename_gho 影子表,并执行 DDL 变更
- 开启 binlog 监听:作为一个伪 MySQL 从库(fake replica),订阅 binlog 事件流
- 行拷贝(row-copy):分批将原表数据通过
INSERT ... SELECT 拷贝到 ghost 表
- 应用 binlog 事件:将监听到的原表 DML 变更,实时回放到 ghost 表
- 切换(cut-over):当行拷贝完成且 binlog 追平后,通过原子操作完成表名切换
2.2 Binlog 流式解析
这是 gh-ost 最关键的创新点。gh-ost 会注册为一个 MySQL 从库,通过 COM_BINLOG_DUMP 协议读取 binlog 事件流。它只关注目标表的 DML 事件(INSERT / UPDATE / DELETE),解析后将其转换为对 ghost 表的操作。
这种方式相比触发器有几个本质优势:
- 异步执行:binlog 解析和回放是异步的,不会增加原表 DML 的执行时间
- 可暂停:暂停行拷贝时,binlog 事件会持续积压但不影响原表性能
- 可控性强:gh-ost 可以随时调整速度、暂停、恢复,甚至回退
2.3 Cut-over(表切换)
表切换是整个迁移中最关键的一步,也是最短暂但最敏感的一步。gh-ost 使用的是原子切换方案,大致流程如下:
1-- 1. 对原表加锁(非常短暂)
2LOCK TABLES `original_table` WRITE, `_original_table_gho` WRITE;
3
4-- 2. 等待 binlog 完全追平
5
6-- 3. 执行 RENAME(原子操作)
7-- gh-ost 实际上使用了更复杂的双连接 cut-over 方案
8-- 通过 DROP + RENAME 的方式实现
9DROP TABLE IF EXISTS `_original_table_del`;
10RENAME TABLE `original_table` TO `_original_table_del`,
11 `_original_table_gho` TO `original_table`;
12
13-- 4. 释放锁
14UNLOCK TABLES;
整个锁表时间通常在毫秒级别,业务几乎无感知。
注意:gh-ost 实际使用的 cut-over 算法比上面展示的更加精巧,采用了双连接协作的方式来保证原子性和安全性。感兴趣的同学可以去看源码中的 Migrator.atomicCutOver() 方法。
三、三种迁移模式详解
gh-ost 支持三种运行模式,适用于不同场景。
3.1 模式一:连接从库,在主库迁移(推荐)
这是 gh-ost 官方推荐的默认模式。
工作方式:
- gh-ost 连接到从库,从从库的 binlog 中读取数据变更事件
- 实际的行拷贝和表切换操作在主库上执行
- 从库仅用于读取 binlog,不承担任何写入操作
优势:
- 读取 binlog 的开销不影响主库性能
- 可以利用从库验证数据一致性
- 如果从库出了问题,主库不受影响
1gh-ost \
2 --host=replica-host \
3 --port=3306 \
4 --user="ghost_user" \
5 --password="ghost_pass" \
6 --database="mydb" \
7 --table="orders" \
8 --alter="ADD COLUMN remark VARCHAR(255) DEFAULT NULL" \
9 --execute
3.2 模式二:直接连接主库
当没有可用的从库时,可以直接在主库上完成所有操作。
1gh-ost \
2 --host=master-host \
3 --port=3306 \
4 --user="ghost_user" \
5 --password="ghost_pass" \
6 --database="mydb" \
7 --table="orders" \
8 --alter="ADD COLUMN remark VARCHAR(255) DEFAULT NULL" \
9 --allow-on-master \
10 --execute
需要添加 --allow-on-master 参数。这种模式下,gh-ost 会从主库自身的 binlog 读取事件并在主库上执行所有操作。
注意事项:
- binlog 读取和行拷贝都在主库进行,资源开销更大
- 适用于没有从库的单实例环境
- 生产环境建议优先使用模式一
3.3 模式三:在从库上迁移(测试模式)
这种模式仅在从库上执行迁移,不涉及主库,主要用于测试和验证。
1gh-ost \
2 --host=replica-host \
3 --port=3306 \
4 --user="ghost_user" \
5 --password="ghost_pass" \
6 --database="mydb" \
7 --table="orders" \
8 --alter="ADD COLUMN remark VARCHAR(255) DEFAULT NULL" \
9 --test-on-replica \
10 --execute
特点:
- 迁移完成后,gh-ost 会暂停从库复制(
STOP SLAVE),执行 cut-over,然后将两张表都保留
- 你可以对比原表和 ghost 表的数据,验证一致性
- 完成后恢复复制(
START SLAVE),从库会自动追平
- 非常适合在上线前进行预演
四、安装与前置条件
4.1 安装 gh-ost
方式一:直接下载二进制
1# 下载最新版本(以 v1.1.6 为例)
2
3> **本章目标**:掌握本章核心知识点
4> **前置要求**:完成前序章节学习
5> **预计时长**:60 分钟
6
7wget https://github.com/github/gh-ost/releases/download/v1.1.6/gh-ost-binary-linux-amd64-20231207144046.tar.gz
8
9# 解压
10tar -xzf gh-ost-binary-linux-amd64-20231207144046.tar.gz
11
12# 移动到 PATH
13sudo mv gh-ost /usr/local/bin/
14chmod +x /usr/local/bin/gh-ost
15
16# 验证
17gh-ost --version
18# gh-ost 1.1.6
方式二:从源码编译
1git clone https://github.com/github/gh-ost.git
2cd gh-ost
3
4# 需要 Go 1.19+
5go build -o gh-ost ./cmd/gh-ost/
6sudo mv gh-ost /usr/local/bin/
4.2 MySQL 前置条件
gh-ost 对 MySQL 的配置有几个硬性要求:
4.2.1 binlog 格式必须为 ROW
1-- 检查当前配置
2SHOW GLOBAL VARIABLES LIKE 'binlog_format';
3+---------------+-------+
4| Variable_name | Value |
5+---------------+-------+
6| binlog_format | ROW |
7+---------------+-------+
8
9-- 如果不是 ROW,需要修改 my.cnf
10-- [mysqld]
11-- binlog_format = ROW
4.2.2 binlog_row_image 必须为 FULL
1SHOW GLOBAL VARIABLES LIKE 'binlog_row_image';
2+------------------+-------+
3| Variable_name | Value |
4+------------------+-------+
5| binlog_row_image | FULL |
6+------------------+-------+
7
8-- 如果是 MINIMAL 或 NOBLOB,gh-ost 无法正确解析变更
9-- [mysqld]
10-- binlog_row_image = FULL
4.2.3 开启 GTID(推荐但非必须)
1SHOW GLOBAL VARIABLES LIKE 'gtid_mode';
2+---------------+-------+
3| Variable_name | Value |
4+---------------+-------+
5| gtid_mode | ON |
6+---------------+-------+
4.2.4 创建专用账号
1CREATE USER 'ghost_user'@'%' IDENTIFIED BY 'GhostP@ss2026';
2
3GRANT ALTER, CREATE, DELETE, DROP, INDEX, INSERT, LOCK TABLES,
4 SELECT, TRIGGER, UPDATE
5 ON mydb.* TO 'ghost_user'@'%';
6
7GRANT SUPER, REPLICATION CLIENT, REPLICATION SLAVE
8 ON *.* TO 'ghost_user'@'%';
9
10FLUSH PRIVILEGES;
提示:MySQL 8.0+ 中 SUPER 权限已被逐步弃用,可以用更细粒度的权限替代,但为了兼容性,这里仍然使用 SUPER。
五、基本用法:添加一个字段
来看一个最常见的场景:给一张大表加一个字段。
5.1 场景描述
假设我们有一张 orders 表,大约 5000 万行数据,需要加一个 remark 字段:
1-- 原始表结构
2CREATE TABLE `orders` (
3 `id` bigint(20) NOT NULL AUTO_INCREMENT,
4 `user_id` bigint(20) NOT NULL,
5 `product_id` bigint(20) NOT NULL,
6 `amount` decimal(10,2) NOT NULL,
7 `status` tinyint(4) NOT NULL DEFAULT '0',
8 `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
9 `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
10 PRIMARY KEY (`id`),
11 KEY `idx_user_id` (`user_id`),
12 KEY `idx_created_at` (`created_at`)
13) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
5.2 干跑模式(dry-run)
正式执行之前,一定要先用干跑模式验证:
1gh-ost \
2 --host=replica-host \
3 --port=3306 \
4 --user="ghost_user" \
5 --password="GhostP@ss2026" \
6 --database="mydb" \
7 --table="orders" \
8 --alter="ADD COLUMN remark VARCHAR(255) DEFAULT NULL COMMENT '备注'" \
9 --verbose
注意:没有 --execute 参数就是干跑模式。gh-ost 会检查所有前置条件,但不会实际执行任何变更。
输出类似:
12026-05-06 10:30:15 INFO Starting gh-ost 1.1.6
22026-05-06 10:30:15 INFO Validating connection to replica-host:3306
32026-05-06 10:30:15 INFO Connection validated on replica-host:3306
42026-05-06 10:30:15 INFO Master found: master-host:3306
52026-05-06 10:30:15 INFO binlog_format: ROW ✓
62026-05-06 10:30:15 INFO binlog_row_image: FULL ✓
72026-05-06 10:30:15 INFO Table mydb.orders found, 49823156 rows estimated
82026-05-06 10:30:15 INFO Dry run complete. Rerun with --execute to apply changes
5.3 正式执行
确认没有问题后,加上 --execute 正式执行:
1gh-ost \
2 --host=replica-host \
3 --port=3306 \
4 --user="ghost_user" \
5 --password="GhostP@ss2026" \
6 --database="mydb" \
7 --table="orders" \
8 --alter="ADD COLUMN remark VARCHAR(255) DEFAULT NULL COMMENT '备注'" \
9 --chunk-size=1000 \
10 --max-lag-millis=1500 \
11 --cut-over=default \
12 --exact-rowcount \
13 --concurrent-rowcount \
14 --ok-to-drop-table \
15 --panic-flag-file=/tmp/ghost.panic.flag \
16 --postpone-cut-over-flag-file=/tmp/ghost.postpone.flag \
17 --serve-socket-file=/tmp/gh-ost.mydb.orders.sock \
18 --execute 2>&1 | tee /tmp/gh-ost-orders.log
执行过程中的输出:
12026-05-06 10:31:00 INFO Migration started
22026-05-06 10:31:00 INFO Creating ghost table mydb._orders_gho
32026-05-06 10:31:00 INFO ALTER TABLE `mydb`.`_orders_gho` ADD COLUMN remark VARCHAR(255) DEFAULT NULL COMMENT '备注'
42026-05-06 10:31:01 INFO Ghost table created and altered
52026-05-06 10:31:01 INFO Binlog streaming started from mysql-bin.000342:48291056
6Copy: 0/49823156 0.0%; Applied: 0; Backlog: 0/1000; Time: 0s(total), 0s(copy); streamer: mysql-bin.000342:48291200
7Copy: 125000/49823156 0.3%; Applied: 23; Backlog: 0/1000; Time: 12s(total), 12s(copy); streamer: mysql-bin.000342:48523410
8Copy: 250000/49823156 0.5%; Applied: 51; Backlog: 0/1000; Time: 24s(total), 24s(copy); streamer: mysql-bin.000342:48891023
9...
10Copy: 49750000/49823156 99.9%; Applied: 18923; Backlog: 0/1000; Time: 5765s(total), 5765s(copy); streamer: mysql-bin.000345:10234891
112026-05-06 12:07:15 INFO Row copy complete
122026-05-06 12:07:15 INFO Proceeding to cut-over
132026-05-06 12:07:15 INFO Cut-over phase: lock tables
142026-05-06 12:07:15 INFO Swapping tables
152026-05-06 12:07:15 INFO Cut-over done. Migration complete
162026-05-06 12:07:16 INFO Dropping old table mydb._orders_del
172026-05-06 12:07:16 INFO Done
整个过程大约 1.5 小时,期间业务完全正常运行。
六、高级参数与交互式控制
gh-ost 提供了非常丰富的运行时控制手段,这也是它相比 pt-osc 的一大优势。
6.1 核心调优参数
--chunk-size(每批拷贝行数)
1--chunk-size=1000 # 默认值
2--chunk-size=5000 # 磁盘 IO 充裕时可以调大
3--chunk-size=200 # 业务高峰期调小
每一批 INSERT ... SELECT 拷贝的行数。这个值越大,迁移越快,但对主库的瞬时压力也越大。建议根据实际监控动态调整。
--max-lag-millis(最大主从延迟)
1--max-lag-millis=1500 # 默认值,1.5 秒
gh-ost 会持续监控从库延迟。当延迟超过这个阈值时,自动暂停行拷贝,等延迟降下来再继续。这个机制非常有用,能有效避免迁移导致主从延迟过大。
--throttle-control-replicas(指定监控的从库)
1--throttle-control-replicas="replica1:3306,replica2:3306"
指定需要监控延迟的从库列表。gh-ost 会检查所有指定从库的延迟,只要有一个超过阈值就暂停。
--nice-ratio(节流比例)
每拷贝一个 chunk 后休息的时间比例。0.5 表示拷贝花了 100ms,就休息 50ms。在高峰期可以设置较大的值来降低影响。
6.2 控制文件(Flag Files)
gh-ost 支持通过文件系统来控制迁移行为,非常适合自动化运维。
--postpone-cut-over-flag-file(推迟切换)
1# 启动时指定
2--postpone-cut-over-flag-file=/tmp/ghost.postpone.flag
3
4# 创建这个文件,gh-ost 就不会自动执行 cut-over
5touch /tmp/ghost.postpone.flag
6
7# 行拷贝完成后,gh-ost 会等待
8# 2026-05-06 12:07:15 INFO Postponing cut-over: /tmp/ghost.postpone.flag exists
9
10# 确认可以切换时,删除文件
11rm /tmp/ghost.postpone.flag
12
13# gh-ost 自动开始 cut-over
这个功能非常实用! 你可以在业务低峰期(比如凌晨 3 点)再执行 cut-over,最大限度降低影响。
--panic-flag-file(紧急中止)
1--panic-flag-file=/tmp/ghost.panic.flag
2
3# 紧急情况下,创建这个文件
4touch /tmp/ghost.panic.flag
5
6# gh-ost 立即停止,保留现场(ghost 表不删除)
7# 2026-05-06 11:45:30 FATAL Panic flag file detected: /tmp/ghost.panic.flag
这相当于一个紧急刹车按钮。gh-ost 会立即退出,但不会清理 ghost 表,方便后续排查。
6.3 Unix Socket 交互式控制
这是 gh-ost 最酷的功能之一。迁移运行过程中,你可以通过 Unix socket 实时发送命令。
1# 启动时指定 socket 文件
2--serve-socket-file=/tmp/gh-ost.mydb.orders.sock
查看当前状态:
1echo "status" | nc -U /tmp/gh-ost.mydb.orders.sock
输出:
1Copy: 25000000/49823156 50.2%
2Applied: 9523
3Backlog: 12/1000
4Chunk-size: 1000
5Max-lag: 1500ms
6Current-lag: 320ms
7Throttle: no
8ETA: 2026-05-06 13:15:00
动态调整 chunk-size:
1echo "chunk-size=2000" | nc -U /tmp/gh-ost.mydb.orders.sock
2# 立即生效,无需重启
手动暂停和恢复:
1# 暂停
2echo "throttle" | nc -U /tmp/gh-ost.mydb.orders.sock
3
4# 恢复
5echo "no-throttle" | nc -U /tmp/gh-ost.mydb.orders.sock
动态调整最大延迟阈值:
1echo "max-lag-millis=3000" | nc -U /tmp/gh-ost.mydb.orders.sock
这种运行时动态调整能力,在面对突发情况时非常有价值。比如业务突然来了一波流量高峰,你可以立刻暂停行拷贝或者降低 chunk-size,等高峰过去了再恢复。
七、gh-ost vs pt-osc 对比
这是大家最关心的问题。下面用一张表来对比两个工具的核心差异:
| 对比维度 | gh-ost | pt-osc |
|---|
| 数据同步方式 | 解析 binlog(异步) | 触发器(同步) |
| 对线上 DML 的影响 | 几乎无影响 | 每条 DML 耗时增加 |
| 锁竞争 | 仅 cut-over 时短暂锁表 | 触发器 + 行拷贝全程有锁竞争 |
| 可暂停性 | 随时暂停/恢复,不影响原表 | 暂停数据拷贝后触发器仍在运行 |
| 运行时调整 | 支持通过 socket 动态调整参数 | 不支持 |
| 测试支持 | 支持从库测试模式 | 不支持 |
| 主从延迟控制 | 内置延迟监控和自动节流 | 支持但依赖触发器 |
| binlog 要求 | 必须 ROW 格式 | 无特殊要求 |
| 外键支持 | 不支持 | 支持(有限) |
| 触发器已存在 | 不受影响 | 冲突,无法创建 |
| 回滚方式 | 删除 ghost 表即可 | 删除影子表 + 触发器 |
| cut-over 时间 | 毫秒级 | 毫秒级(RENAME) |
| 成熟度 | 2016 年开源,GitHub 生产验证 | 2011 年发布,业界广泛使用 |
| 社区活跃度 | 活跃 | 活跃(Percona 维护) |
选择建议
优先选择 gh-ost 的场景:
- 高并发写入的表(QPS > 1000)
- 需要在低峰期手动控制 cut-over 的场景
- 表上已经有触发器
- 需要在从库上预演测试
- binlog 已经是 ROW 格式
仍然选择 pt-osc 的场景:
- binlog 不是 ROW 格式且无法修改
- 表有外键约束
- 团队对 pt-osc 更熟悉,且性能影响可接受
八、生产环境最佳实践
8.1 上线前检查清单(Pre-flight Checklist)
正式执行 gh-ost 之前,务必逐项确认:
1#!/bin/bash
2# gh-ost 上线前检查脚本
3
4MYSQL_HOST="master-host"
5MYSQL_PORT=3306
6MYSQL_USER="ghost_user"
7MYSQL_PASS="GhostP@ss2026"
8
9echo "=== gh-ost Pre-flight Checklist ==="
10
11# 1. 检查 binlog_format
12echo -n "[1] binlog_format: "
13mysql -h$MYSQL_HOST -P$MYSQL_PORT -u$MYSQL_USER -p$MYSQL_PASS -e \
14 "SELECT @@binlog_format" --skip-column-names 2>/dev/null
15
16# 2. 检查 binlog_row_image
17echo -n "[2] binlog_row_image: "
18mysql -h$MYSQL_HOST -P$MYSQL_PORT -u$MYSQL_USER -p$MYSQL_PASS -e \
19 "SELECT @@binlog_row_image" --skip-column-names 2>/dev/null
20
21# 3. 检查目标表行数
22echo -n "[3] Table row count (estimated): "
23mysql -h$MYSQL_HOST -P$MYSQL_PORT -u$MYSQL_USER -p$MYSQL_PASS -e \
24 "SELECT TABLE_ROWS FROM information_schema.TABLES WHERE TABLE_SCHEMA='mydb' AND TABLE_NAME='orders'" --skip-column-names 2>/dev/null
25
26# 4. 检查磁盘空间(需要原表 1.5 倍以上空间)
27echo -n "[4] Disk free space: "
28df -h /var/lib/mysql | tail -1 | awk '{print $4}'
29
30# 5. 检查主从延迟
31echo -n "[5] Replica lag: "
32mysql -hreplica-host -P$MYSQL_PORT -u$MYSQL_USER -p$MYSQL_PASS -e \
33 "SHOW SLAVE STATUS\G" 2>/dev/null | grep "Seconds_Behind_Master"
34
35# 6. 检查是否有长事务
36echo "[6] Long running transactions:"
37mysql -h$MYSQL_HOST -P$MYSQL_PORT -u$MYSQL_USER -p$MYSQL_PASS -e \
38 "SELECT trx_id, trx_started, trx_query FROM information_schema.innodb_trx WHERE trx_started < NOW() - INTERVAL 60 SECOND" 2>/dev/null
39
40echo "=== Checklist Complete ==="
8.2 迁移过程中的监控
在迁移运行期间,需要持续关注以下指标:
1. 主从延迟
1# 每 5 秒检查一次从库延迟
2watch -n 5 'mysql -hreplica-host -ughost_user -pGhostP@ss2026 -e "SHOW SLAVE STATUS\G" 2>/dev/null | grep Seconds_Behind_Master'
2. 主库负载
1# 关注 Threads_running 和 Threads_connected
2watch -n 5 'mysql -hmaster-host -ughost_user -pGhostP@ss2026 -e "SHOW GLOBAL STATUS LIKE '\''Threads_%'\''" 2>/dev/null'
3. gh-ost 进度
1# 通过 socket 查看实时状态
2watch -n 10 'echo "status" | nc -U /tmp/gh-ost.mydb.orders.sock 2>/dev/null'
4. 磁盘 IO
1# 关注磁盘写入量
2iostat -x 5 | grep -E "Device|sda|nvme"
8.3 处理超大表(1 亿行以上)
对于超大表,需要更加精细的控制策略:
1. 降低行拷贝速度
1gh-ost \
2 --chunk-size=500 \
3 --nice-ratio=1.0 \
4 --max-lag-millis=1000 \
5 ...
--nice-ratio=1.0 意味着拷贝和休息时间 1:1,可以有效控制对 IO 的压力。
2. 分时段执行
利用 --postpone-cut-over-flag-file 和交互式控制,可以实现"白天慢跑、晚上全速":
1# 白天(业务高峰)
2echo "chunk-size=200" | nc -U /tmp/gh-ost.mydb.orders.sock
3echo "nice-ratio=2.0" | nc -U /tmp/gh-ost.mydb.orders.sock
4
5# 晚上(业务低峰)
6echo "chunk-size=5000" | nc -U /tmp/gh-ost.mydb.orders.sock
7echo "nice-ratio=0" | nc -U /tmp/gh-ost.mydb.orders.sock
3. 关注 binlog 积压
超大表迁移时间可能长达数天,需要确保 binlog 不会因为积压太多而占满磁盘:
1# 检查 binlog 占用
2mysql -e "SHOW BINARY LOGS" | awk '{total += $2} END {printf "Total binlog size: %.2f GB\n", total/1024/1024/1024}'
4. 预估时间
一个粗略的经验公式:
1迁移时间 ≈ (表行数 / chunk_size) × (每个 chunk 耗时 + nice_ratio × 每个 chunk 耗时)
以 1 亿行、chunk_size=1000、每 chunk 50ms、nice_ratio=0.5 为例:
1100,000,000 / 1000 × (50ms + 25ms) = 100,000 × 75ms = 7,500s ≈ 2 小时
实际时间还取决于 binlog 应用量和网络延迟等因素,通常会比理论值高 20%-50%。
8.4 自动化运维脚本
在团队里推广 gh-ost 时,建议封装一个标准化的执行脚本:
1#!/bin/bash
2# gh-ost-run.sh - 标准化 gh-ost 执行脚本
3
4DB_NAME=$1
5TABLE_NAME=$2
6ALTER_SQL=$3
7
8if [ -z "$ALTER_SQL" ]; then
9 echo "Usage: $0 <database> <table> <alter_sql>"
10 echo "Example: $0 mydb orders 'ADD COLUMN remark VARCHAR(255)'"
11 exit 1
12fi
13
14REPLICA_HOST="replica-host"
15GHOST_USER="ghost_user"
16GHOST_PASS="GhostP@ss2026"
17
18SOCK_FILE="/tmp/gh-ost.${DB_NAME}.${TABLE_NAME}.sock"
19PANIC_FILE="/tmp/gh-ost.${DB_NAME}.${TABLE_NAME}.panic"
20POSTPONE_FILE="/tmp/gh-ost.${DB_NAME}.${TABLE_NAME}.postpone"
21LOG_FILE="/var/log/gh-ost/${DB_NAME}.${TABLE_NAME}.$(date +%Y%m%d%H%M%S).log"
22
23mkdir -p /var/log/gh-ost
24
25# 创建 postpone 文件,手动控制 cut-over
26touch "$POSTPONE_FILE"
27echo "Postpone file created: $POSTPONE_FILE"
28echo "Delete it to trigger cut-over when ready"
29
30gh-ost \
31 --host="$REPLICA_HOST" \
32 --port=3306 \
33 --user="$GHOST_USER" \
34 --password="$GHOST_PASS" \
35 --database="$DB_NAME" \
36 --table="$TABLE_NAME" \
37 --alter="$ALTER_SQL" \
38 --chunk-size=1000 \
39 --max-lag-millis=1500 \
40 --throttle-control-replicas="$REPLICA_HOST:3306" \
41 --exact-rowcount \
42 --concurrent-rowcount \
43 --ok-to-drop-table \
44 --panic-flag-file="$PANIC_FILE" \
45 --postpone-cut-over-flag-file="$POSTPONE_FILE" \
46 --serve-socket-file="$SOCK_FILE" \
47 --execute 2>&1 | tee "$LOG_FILE"
48
49echo "Log saved to: $LOG_FILE"
使用方式:
1chmod +x gh-ost-run.sh
2./gh-ost-run.sh mydb orders "ADD COLUMN remark VARCHAR(255) DEFAULT NULL"
3
4# 另一个终端,确认行拷贝完成后执行 cut-over
5rm /tmp/gh-ost.mydb.orders.postpone
九、常见错误与解决方案
1FATAL Error: only ROW binlog format is supported. Got: STATEMENT
解决: 修改 MySQL 配置并重启,或者在线修改(影响新的会话):
1SET GLOBAL binlog_format = 'ROW';
注意:在线修改只对新连接生效,已有连接仍然使用旧格式。建议写入 my.cnf 后重启。
9.2 binlog_row_image 不是 FULL
1FATAL Error: binlog_row_image is MINIMAL, require FULL
解决:
1SET GLOBAL binlog_row_image = 'FULL';
2-- 同样需要写入 my.cnf 持久化
9.3 表没有主键或唯一索引
1FATAL No PRIMARY KEY or unique index found on table `mydb`.`events`
gh-ost 必须要求表有主键或唯一索引,用来做行级别的数据对应。如果没有,需要先加上:
1-- 如果表有可以做唯一标识的字段
2ALTER TABLE events ADD PRIMARY KEY (id);
3
4-- 如果实在没有,考虑是否适合用 gh-ost
9.4 从库延迟导致长时间暂停
1Throttling on replica lag: 5230ms > 1500ms threshold
这不是错误,是正常的保护机制。如果暂停时间过长,可以:
- 排查从库延迟原因(大事务、IO 瓶颈等)
- 临时调高阈值:
echo "max-lag-millis=5000" | nc -U /tmp/gh-ost.mydb.orders.sock
- 检查从库是否有慢查询在消耗资源
9.5 磁盘空间不足
1FATAL Error: disk space below threshold
gh-ost 需要大约原表 1-1.5 倍的额外磁盘空间(ghost 表 + binlog 积压)。处理方式:
1# 检查磁盘使用
2df -h /var/lib/mysql
3
4# 清理不需要的 binlog(谨慎操作)
5mysql -e "PURGE BINARY LOGS BEFORE '2026-05-05 00:00:00'"
6
7# 清理无用的大表或临时文件
9.6 cut-over 时锁等待超时
1FATAL Error: cut-over lock timeout exceeded
通常是因为有长事务持有表锁。解决方案:
1-- 查看谁在持有锁
2SELECT * FROM information_schema.innodb_trx
3WHERE trx_started < NOW() - INTERVAL 30 SECOND\G
4
5-- 如果是可以 kill 的查询
6KILL <thread_id>;
建议在 cut-over 前检查并清理长事务。这也是使用 --postpone-cut-over-flag-file 手动控制切换时机的价值所在。
9.7 ghost 表已存在
1FATAL Error: ghost table `mydb`.`_orders_gho` already exists
说明上次迁移异常退出没有清理干净:
1-- 确认表内容无用后删除
2DROP TABLE IF EXISTS `mydb`.`_orders_gho`;
3DROP TABLE IF EXISTS `mydb`.`_orders_del`;
9.8 权限不足
1FATAL Error: user ghost_user lacks REPLICATION SLAVE privilege
检查并补齐权限:
1GRANT REPLICATION CLIENT, REPLICATION SLAVE ON *.* TO 'ghost_user'@'%';
2FLUSH PRIVILEGES;
总结
gh-ost 通过用 binlog 解析替代触发器这一核心设计,从根本上解决了 pt-osc 在高并发场景下的性能问题。它的可控性(暂停、恢复、动态调参)和可测试性(从库测试模式)也远超传统方案。
回顾本文的核心要点:
- 原理层面:gh-ost 伪装成 MySQL 从库读取 binlog,异步回放数据变更,避免了触发器的同步开销
- 模式选择:优先使用"连接从库、迁移主库"的推荐模式,正式上线前用"从库测试模式"预演
- 控制手段:善用 postpone-cut-over 文件控制切换时机,善用 socket 交互式调参
- 安全保障:panic 文件是你的紧急刹车,遇到任何异常都能立即中止
- 前置条件:binlog_format=ROW、binlog_row_image=FULL、表必须有主键
如果你的 MySQL 环境满足前置条件(ROW 格式 binlog),强烈建议切换到 gh-ost。它在 GitHub 内部已经执行了数十万次在线 DDL,稳定性经得起考验。
最后,别忘了在生产环境第一次使用时,先用 --test-on-replica 模式跑一遍,心里有底了再上主库。稳,才是 DBA 的核心竞争力。