运维管理25 分钟阅读
gh-ost 在线 DDL 实战:GitHub 出品的 MySQL 无触发器变更方案
gh-ost 是 GitHub 开源的 MySQL 在线 DDL 工具,与 pt-osc 最大的区别是不使用触发器,而是通过解析 binlog 实现数据同步。本文详解 gh-ost 的工作原理、操作流程、与 pt-osc 的对比,以及生产环境最佳实践。
2026年5月6日阅读—点赞—收藏—
mysql
在知识库中专注阅读,并随时返回相关工具与课程
gh-ost 是 GitHub 开源的 MySQL 在线 DDL 工具,与 pt-osc 最大的区别是不使用触发器,而是通过解析 binlog 实现数据同步。本文详解 gh-ost 的工作原理、操作流程、与 pt-osc 的对比,以及生产环境最佳实践。
做过 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 的工作方式以及它存在的问题。
pt-osc 的核心流程是:
_tablename_new)ALTER TABLERENAME TABLE 原子切换这个方案看起来很优雅,但触发器本身带来了不少麻烦:
性能影响大: 触发器是同步执行的。每一条对原表的 INSERT / UPDATE / DELETE 操作,都会同步触发对影子表的写入。在高并发场景下,这意味着每条 DML 的执行时间直接翻倍,RT(响应时间)显著上升。
锁竞争加剧: 触发器在同一个事务中执行,原表和影子表的锁会相互影响。当存在热点行时,锁等待和死锁的概率大幅增加。
无法暂停: pt-osc 一旦开始运行,触发器就已经装上了。即使你暂停了数据拷贝,触发器仍然在工作,仍然在影响线上性能。想要彻底停止,只能 kill 掉进程并手动清理触发器和影子表。
测试困难: 你没有办法在从库上先跑一遍测试,看看影响到底有多大。触发器必须在写入端(主库)创建。
binlog 膨胀: 触发器产生的额外 DML 会写入 binlog,导致主从延迟加大。
这些问题在 GitHub 这种级别的业务场景下尤为突出。GitHub 的 MySQL 集群承载着极高的写入并发,触发器方案带来的性能抖动是不可接受的。于是,他们开发了 gh-ost。
gh-ost 最核心的设计理念就是:用 binlog 流式解析替代触发器。
gh-ost 的迁移过程可以概括为以下几步:
_tablename_gho 影子表,并执行 DDL 变更INSERT ... SELECT 拷贝到 ghost 表这是 gh-ost 最关键的创新点。gh-ost 会注册为一个 MySQL 从库,通过 COM_BINLOG_DUMP 协议读取 binlog 事件流。它只关注目标表的 DML 事件(INSERT / UPDATE / DELETE),解析后将其转换为对 ghost 表的操作。
gh-ost 解析 binlog 并回放到 ghost 表
这种方式相比触发器有几个本质优势:
表切换是整个迁移中最关键的一步,也是最短暂但最敏感的一步。gh-ost 使用的是原子切换方案,大致流程如下:
1-- 1. 对原表加锁(非常短暂)2LOCK TABLES `original_table` WRITE, `_original_table_gho` WRITE;34-- 2. 等待 binlog 完全追平56-- 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`;1213-- 4. 释放锁14UNLOCK TABLES;整个锁表时间通常在毫秒级别,业务几乎无感知。
注意
注意:gh-ost 实际使用的 cut-over 算法比上面展示的更加精巧,采用了双连接协作的方式来保证原子性和安全性。感兴趣的同学可以去看源码中的
Migrator.atomicCutOver()方法。
gh-ost 支持三种运行模式,适用于不同场景。
这是 gh-ost 官方推荐的默认模式。
gh-ost 从副库读取 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当没有可用的从库时,可以直接在主库上完成所有操作。
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 读取事件并在主库上执行所有操作。
注意事项:
这种模式仅在从库上执行迁移,不涉及主库,主要用于测试和验证。
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特点:
STOP SLAVE),执行 cut-over,然后将两张表都保留START SLAVE),从库会自动追平方式一:直接下载二进制
1# 下载最新版本(以 v1.1.6 为例)23> **本章目标**:掌握本章核心知识点4> **前置要求**:完成前序章节学习5> **预计时长**:60 分钟67wget https://github.com/github/gh-ost/releases/download/v1.1.6/gh-ost-binary-linux-amd64-20231207144046.tar.gz89# 解压10tar -xzf gh-ost-binary-linux-amd64-20231207144046.tar.gz1112# 移动到 PATH13sudo mv gh-ost /usr/local/bin/14chmod +x /usr/local/bin/gh-ost1516# 验证17gh-ost --version18# gh-ost 1.1.6方式二:从源码编译
1git clone https://github.com/github/gh-ost.git2cd gh-ost34# 需要 Go 1.19+5go build -o gh-ost ./cmd/gh-ost/6sudo mv gh-ost /usr/local/bin/gh-ost 对 MySQL 的配置有几个硬性要求:
1-- 检查当前配置2SHOW GLOBAL VARIABLES LIKE 'binlog_format';3+---------------+-------+4| Variable_name | Value |5+---------------+-------+6| binlog_format | ROW |7+---------------+-------+89-- 如果不是 ROW,需要修改 my.cnf10-- [mysqld]11-- binlog_format = ROW1SHOW GLOBAL VARIABLES LIKE 'binlog_row_image';2+------------------+-------+3| Variable_name | Value |4+------------------+-------+5| binlog_row_image | FULL |6+------------------+-------+78-- 如果是 MINIMAL 或 NOBLOB,gh-ost 无法正确解析变更9-- [mysqld]10-- binlog_row_image = FULL1SHOW GLOBAL VARIABLES LIKE 'gtid_mode';2+---------------+-------+3| Variable_name | Value |4+---------------+-------+5| gtid_mode | ON |6+---------------+-------+1CREATE USER 'ghost_user'@'%' IDENTIFIED BY 'GhostP@ss2026';23GRANT ALTER, CREATE, DELETE, DROP, INDEX, INSERT, LOCK TABLES,4 SELECT, TRIGGER, UPDATE5 ON mydb.* TO 'ghost_user'@'%';67GRANT SUPER, REPLICATION CLIENT, REPLICATION SLAVE8 ON *.* TO 'ghost_user'@'%';910FLUSH PRIVILEGES;提示
提示:MySQL 8.0+ 中
SUPER权限已被逐步弃用,可以用更细粒度的权限替代,但为了兼容性,这里仍然使用SUPER。
来看一个最常见的场景:给一张大表加一个字段。
假设我们有一张 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;正式执行之前,一定要先用干跑模式验证:
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.622026-05-06 10:30:15 INFO Validating connection to replica-host:330632026-05-06 10:30:15 INFO Connection validated on replica-host:330642026-05-06 10:30:15 INFO Master found: master-host:330652026-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 estimated82026-05-06 10:30:15 INFO Dry run complete. Rerun with --execute to apply changes确认没有问题后,加上 --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 started22026-05-06 10:31:00 INFO Creating ghost table mydb._orders_gho32026-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 altered52026-05-06 10:31:01 INFO Binlog streaming started from mysql-bin.000342:482910566Copy: 0/49823156 0.0%; Applied: 0; Backlog: 0/1000; Time: 0s(total), 0s(copy); streamer: mysql-bin.000342:482912007Copy: 125000/49823156 0.3%; Applied: 23; Backlog: 0/1000; Time: 12s(total), 12s(copy); streamer: mysql-bin.000342:485234108Copy: 250000/49823156 0.5%; Applied: 51; Backlog: 0/1000; Time: 24s(total), 24s(copy); streamer: mysql-bin.000342:488910239...10Copy: 49750000/49823156 99.9%; Applied: 18923; Backlog: 0/1000; Time: 5765s(total), 5765s(copy); streamer: mysql-bin.000345:10234891112026-05-06 12:07:15 INFO Row copy complete122026-05-06 12:07:15 INFO Proceeding to cut-over132026-05-06 12:07:15 INFO Cut-over phase: lock tables142026-05-06 12:07:15 INFO Swapping tables152026-05-06 12:07:15 INFO Cut-over done. Migration complete162026-05-06 12:07:16 INFO Dropping old table mydb._orders_del172026-05-06 12:07:16 INFO Done整个过程大约 1.5 小时,期间业务完全正常运行。
gh-ost 提供了非常丰富的运行时控制手段,这也是它相比 pt-osc 的一大优势。
1--chunk-size=1000 # 默认值2--chunk-size=5000 # 磁盘 IO 充裕时可以调大3--chunk-size=200 # 业务高峰期调小每一批 INSERT ... SELECT 拷贝的行数。这个值越大,迁移越快,但对主库的瞬时压力也越大。建议根据实际监控动态调整。
1--max-lag-millis=1500 # 默认值,1.5 秒gh-ost 会持续监控从库延迟。当延迟超过这个阈值时,自动暂停行拷贝,等延迟降下来再继续。这个机制非常有用,能有效避免迁移导致主从延迟过大。
1--throttle-control-replicas="replica1:3306,replica2:3306"指定需要监控延迟的从库列表。gh-ost 会检查所有指定从库的延迟,只要有一个超过阈值就暂停。
1--nice-ratio=0.5每拷贝一个 chunk 后休息的时间比例。0.5 表示拷贝花了 100ms,就休息 50ms。在高峰期可以设置较大的值来降低影响。
gh-ost 支持通过文件系统来控制迁移行为,非常适合自动化运维。
1# 启动时指定2--postpone-cut-over-flag-file=/tmp/ghost.postpone.flag34# 创建这个文件,gh-ost 就不会自动执行 cut-over5touch /tmp/ghost.postpone.flag67# 行拷贝完成后,gh-ost 会等待8# 2026-05-06 12:07:15 INFO Postponing cut-over: /tmp/ghost.postpone.flag exists910# 确认可以切换时,删除文件11rm /tmp/ghost.postpone.flag1213# gh-ost 自动开始 cut-over这个功能非常实用! 你可以在业务低峰期(比如凌晨 3 点)再执行 cut-over,最大限度降低影响。
1--panic-flag-file=/tmp/ghost.panic.flag23# 紧急情况下,创建这个文件4touch /tmp/ghost.panic.flag56# gh-ost 立即停止,保留现场(ghost 表不删除)7# 2026-05-06 11:45:30 FATAL Panic flag file detected: /tmp/ghost.panic.flag这相当于一个紧急刹车按钮。gh-ost 会立即退出,但不会清理 ghost 表,方便后续排查。
这是 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: 95233Backlog: 12/10004Chunk-size: 10005Max-lag: 1500ms6Current-lag: 320ms7Throttle: no8ETA: 2026-05-06 13:15:00动态调整 chunk-size:
1echo "chunk-size=2000" | nc -U /tmp/gh-ost.mydb.orders.sock2# 立即生效,无需重启手动暂停和恢复:
1# 暂停2echo "throttle" | nc -U /tmp/gh-ost.mydb.orders.sock34# 恢复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 的场景:
仍然选择 pt-osc 的场景:
正式执行 gh-ost 之前,务必逐项确认:
1#!/bin/bash2# gh-ost 上线前检查脚本34MYSQL_HOST="master-host"5MYSQL_PORT=33066MYSQL_USER="ghost_user"7MYSQL_PASS="GhostP@ss2026"89echo "=== gh-ost Pre-flight Checklist ==="1011# 1. 检查 binlog_format12echo -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/null1516# 2. 检查 binlog_row_image17echo -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/null2021# 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/null2526# 4. 检查磁盘空间(需要原表 1.5 倍以上空间)27echo -n "[4] Disk free space: "28df -h /var/lib/mysql | tail -1 | awk '{print $4}'2930# 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"3435# 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/null3940echo "=== Checklist Complete ==="在迁移运行期间,需要持续关注以下指标:
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_connected2watch -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"对于超大表,需要更加精细的控制策略:
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.sock3echo "nice-ratio=2.0" | nc -U /tmp/gh-ost.mydb.orders.sock45# 晚上(业务低峰)6echo "chunk-size=5000" | nc -U /tmp/gh-ost.mydb.orders.sock7echo "nice-ratio=0" | nc -U /tmp/gh-ost.mydb.orders.sock3. 关注 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%。
在团队里推广 gh-ost 时,建议封装一个标准化的执行脚本:
1#!/bin/bash2# gh-ost-run.sh - 标准化 gh-ost 执行脚本34DB_NAME=$15TABLE_NAME=$26ALTER_SQL=$378if [ -z "$ALTER_SQL" ]; then9 echo "Usage: $0 <database> <table> <alter_sql>"10 echo "Example: $0 mydb orders 'ADD COLUMN remark VARCHAR(255)'"11 exit 112fi1314REPLICA_HOST="replica-host"15GHOST_USER="ghost_user"16GHOST_PASS="GhostP@ss2026"1718SOCK_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"2223mkdir -p /var/log/gh-ost2425# 创建 postpone 文件,手动控制 cut-over26touch "$POSTPONE_FILE"27echo "Postpone file created: $POSTPONE_FILE"28echo "Delete it to trigger cut-over when ready"2930gh-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"4849echo "Log saved to: $LOG_FILE"使用方式:
1chmod +x gh-ost-run.sh2./gh-ost-run.sh mydb orders "ADD COLUMN remark VARCHAR(255) DEFAULT NULL"34# 另一个终端,确认行拷贝完成后执行 cut-over5rm /tmp/gh-ost.mydb.orders.postpone1FATAL Error: only ROW binlog format is supported. Got: STATEMENT解决: 修改 MySQL 配置并重启,或者在线修改(影响新的会话):
1SET GLOBAL binlog_format = 'ROW';注意
注意:在线修改只对新连接生效,已有连接仍然使用旧格式。建议写入
my.cnf后重启。
1FATAL Error: binlog_row_image is MINIMAL, require FULL解决:
1SET GLOBAL binlog_row_image = 'FULL';2-- 同样需要写入 my.cnf 持久化1FATAL No PRIMARY KEY or unique index found on table `mydb`.`events`gh-ost 必须要求表有主键或唯一索引,用来做行级别的数据对应。如果没有,需要先加上:
1-- 如果表有可以做唯一标识的字段2ALTER TABLE events ADD PRIMARY KEY (id);34-- 如果实在没有,考虑是否适合用 gh-ost1Throttling on replica lag: 5230ms > 1500ms threshold这不是错误,是正常的保护机制。如果暂停时间过长,可以:
echo "max-lag-millis=5000" | nc -U /tmp/gh-ost.mydb.orders.sock1FATAL Error: disk space below thresholdgh-ost 需要大约原表 1-1.5 倍的额外磁盘空间(ghost 表 + binlog 积压)。处理方式:
1# 检查磁盘使用2df -h /var/lib/mysql34# 清理不需要的 binlog(谨慎操作)5mysql -e "PURGE BINARY LOGS BEFORE '2026-05-05 00:00:00'"67# 清理无用的大表或临时文件1FATAL Error: cut-over lock timeout exceeded通常是因为有长事务持有表锁。解决方案:
1-- 查看谁在持有锁2SELECT * FROM information_schema.innodb_trx3WHERE trx_started < NOW() - INTERVAL 30 SECOND\G45-- 如果是可以 kill 的查询6KILL <thread_id>;建议在 cut-over 前检查并清理长事务。这也是使用 --postpone-cut-over-flag-file 手动控制切换时机的价值所在。
1FATAL Error: ghost table `mydb`.`_orders_gho` already exists说明上次迁移异常退出没有清理干净:
1-- 确认表内容无用后删除2DROP TABLE IF EXISTS `mydb`.`_orders_gho`;3DROP TABLE IF EXISTS `mydb`.`_orders_del`;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 在高并发场景下的性能问题。它的可控性(暂停、恢复、动态调参)和可测试性(从库测试模式)也远超传统方案。
回顾本文的核心要点:
如果你的 MySQL 环境满足前置条件(ROW 格式 binlog),强烈建议切换到 gh-ost。它在 GitHub 内部已经执行了数十万次在线 DDL,稳定性经得起考验。
最后,别忘了在生产环境第一次使用时,先用 --test-on-replica 模式跑一遍,心里有底了再上主库。稳,才是 DBA 的核心竞争力。