在生产环境中对千万级乃至亿级数据量的MySQL大表执行DDL(Data Definition Language)变更,一直是DBA和后端工程师最头疼的难题之一。传统的
1 | ALTER TABLE |
语句会长时间持有表级元数据锁(MDL),阻塞业务读写,甚至导致连接池耗尽、数据库雪崩。MySQL 8.0虽然在Online DDL方面有了长足进步,但仍有许多场景需要借助
1 | gh-ost |
或
1 | pt-online-schema-change |
(pt-osc)这类第三方工具来完成无锁或低锁变更。本文将从原理出发,结合实战配置,系统讲解三种方案的适用场景、工作机制与选型策略。

一、为什么大表DDL是个难题
要理解在线DDL的难点,首先需要搞清楚MySQL执行DDL时发生了什么。在MySQL 5.6引入Online DDL之前,绝大多数
1 | ALTER TABLE |
操作都会采用COPY算法:创建一张临时新表、拷贝全量数据、替换旧表。整个过程表不可写,甚至不可读。
即使到了MySQL 8.0,Online DDL也不是万能的。以下几类操作依然可能引发长时间阻塞或大表拷贝:
- 修改列类型:例如把
1VARCHAR(50)
改为
1TEXT,只能走COPY算法
- 修改列字符集或排序规则:需要重建整张表
- 添加全文索引:耗时与数据量正相关
- 修改主键:涉及全表数据重组
- 大表添加二级索引:虽然INPLACE算法不拷贝数据,但构建索引的排序过程会消耗大量IO和临时空间
更隐蔽的风险是元数据锁(MDL)。DDL语句在执行开始时会请求MDL写锁,如果此时有长事务持有MDL读锁,DDL会被阻塞;而DDL一旦排队等待,后续所有对该表的查询和DML都会被堵在队列里,形成典型的DDL雪崩。这就是为什么在生产环境直接执行
1 | ALTER TABLE |
极其危险。
二、MySQL 8.0 Online DDL详解
2.1 INPLACE与INSTANT算法
MySQL 8.0引入了革命性的INSTANT算法(8.0.12+),对于在表尾添加列这类操作可以瞬间完成——只修改数据字典中的表定义,不修改数据行,也不重建表。这是真正意义上的瞬态DDL。
1
2
3
4
5
6
7
8
9
10
11
12 -- 8.0.12+ 添加列默认使用INSTANT算法,瞬间完成
ALTER TABLE orders ADD COLUMN remark VARCHAR(200) NULL;
-- 显式指定算法
ALTER TABLE orders
ADD COLUMN status TINYINT NOT NULL DEFAULT 0,
ALGORITHM=INSTANT;
-- 添加二级索引使用INPLACE算法(不拷贝数据但需构建索引)
ALTER TABLE orders
ADD INDEX idx_user_created (user_id, created_at),
ALGORITHM=INPLACE, LOCK=NONE;
LOCK子句控制并发度:
1 | LOCK=NONE |
允许DML并发执行;
1 | LOCK=SHARED |
允许读但不允许写;
1 | LOCK=EXCLUSIVE |
完全阻塞。推荐在生产DDL中显式声明
1 | LOCK=NONE |
,如果MySQL发现该操作无法以NONE方式执行,会直接报错而不是默默降级为阻塞模式——这比让数据库自作主张安全得多。
2.2 Online DDL的适用范围
下表总结了MySQL 8.0中常见DDL操作支持的算法,供选型参考:
| 操作类型 | INSTANT | INPLACE | COPY | 是否阻塞DML |
|---|---|---|---|---|
| 表尾添加列 | 支持 | 支持 | 支持 | INPLACE/INSTANT不阻塞 |
| 删除列 | 8.0.29+部分 | 重建 | 支持 | INPLACE不阻塞 |
| 重命名列 | 支持 | 支持 | 支持 | 不阻塞 |
| 修改列类型 | 不支持 | 不支持 | 支持 | 阻塞 |
| 添加二级索引 | 不支持 | 支持 | 支持 | INPLACE不阻塞 |
| 修改主键 | 不支持 | 部分 | 支持 | INPLACE部分阻塞 |
| 修改列默认值 | 支持 | 支持 | 支持 | 不阻塞 |
| 修改字符集 | 不支持 | 不支持 | 支持 | 阻塞 |
从表中可以看出,凡是涉及重建表或重写每行数据的操作,MySQL原生Online DDL都无能为力,这时就需要借助第三方工具。

三、gh-ost:GitHub开源的无触发器在线DDL工具
3.1 工作原理
gh-ost(GitHub Online Schema Change Migrator)是GitHub为了解决自身海量数据变更问题而开发的开源工具。与传统的pt-osc不同,gh-ost不依赖触发器,而是通过解析binlog来同步增量数据。这一设计避免了触发器带来的性能损耗和死锁风险。
其核心流程如下:
- 创建与原表结构一致的影子表(ghost table)
- 从原表拷贝数据到影子表(分批chunk拷贝)
- 同时作为binlog客户端,实时应用原表上的DML变更到影子表
- 校验数据一致性后,通过原子切换(RENAME TABLE)替换原表与影子表
- 清理旧表和临时文件
因为不使用触发器,gh-ost对源库的负载可控性更强——它自身就是一个独立的客户端进程,可以随时暂停、动态调整并发度。
3.2 实战命令示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 # 基础用法:给orders表添加列
gh-ost \
--host=127.0.0.1 \
--port=3306 \
--user=ghost-user \
--password='xxxx' \
--database=shop \
--table=orders \
--alter='ADD COLUMN channel VARCHAR(32) NOT NULL DEFAULT "web"' \
--allow-on-master \
--chunk-size=200 \
--max-load='Threads_running=100' \
--critical-load='Threads_running=300' \
--execute
# 动态限流:运行时通过socket文件下发命令
gh-ost ... --serve-socket-file=/tmp/gh-ost.sock &
echo 'throttle-chunk-size=50' | nc -U /tmp/gh-ost.sock
echo 'unthrottle' | nc -U /tmp/gh-ost.sock
几个关键参数值得重点关注:
- –max-load:软阈值,达到后gh-ost自动暂停拷贝,等待指标回落
- –critical-load:硬阈值,达到后立即中止整个迁移并回滚
- –chunk-size:每批拷贝的行数,越小越平滑但耗时越长
- –max-lag-millis:在主从架构下控制从库延迟上限
- –switch-to-rbr:自动将binlog格式切换为ROW(gh-ost强制要求ROW格式)
3.3 gh-ost的优势与限制
gh-ost的优势非常明显:无触发器设计避免了源表写入放大;支持动态暂停与限流;支持断点续传(通过
1 | --resume |
);提供throttle HTTP接口便于与监控系统联动。同时,它也存在一些限制:
- 要求binlog格式为ROW,且binlog_row_image=FULL
- 不支持无主键或唯一键的表
- 外键支持有限(需使用
1--alter-foreign-keys-method
)
- 对带触发器的源表支持不佳
四、pt-online-schema-change:Percona Toolkit经典工具
4.1 工作原理
pt-osc是Percona Toolkit套件中的在线表变更工具,基于触发器实现。其工作流程与gh-ost类似,但增量同步是通过在原表上创建三个触发器(INSERT、UPDATE、DELETE)来完成的。每当原表发生DML,触发器会把对应的变更同步到新表。
触发器方案的历史悠久、兼容性好,但也带来了固有的副作用:触发器会增加原表每次DML的执行成本,在高写入并发场景下可能导致性能下降、锁竞争甚至死锁。此外,一个表上不能同时存在多个pt-osc任务(因为触发器名固定)。
4.2 实战命令示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 # 给orders表添加索引,并自动检测从库延迟
pt-online-schema-change \
--alter='ADD INDEX idx_paytime (paid_at)' \
--host=127.0.0.1 \
--user=pt-user \
--password='xxxx' \
D=shop,t=orders \
--chunk-time=0.5 \
--max-load='Threads_running=80' \
--critical-load='Threads_running=200' \
--max-lag=10 \
--check-slave-lag='slave-host:3306' \
--execute
# 只执行dry-run,不真正变更
pt-online-schema-change \
--alter='MODIFY COLUMN name VARCHAR(128) NOT NULL' \
D=shop,t=users \
--dry-run
# 对于有外键的表,必须指定处理策略
pt-online-schema-change \
--alter='DROP COLUMN legacy_field' \
--alter-foreign-keys-method=auto \
D=shop,t=orders \
--execute
1 | --alter-foreign-keys-method |
有三种取值:
1 | auto |
让工具自行决定;
1 | rebuild_constraints |
在新表上重建外键约束;
1 | drop_swap |
先删原表再重命名新表(有短暂窗口期,不推荐生产使用)。

五、三种方案对比与选型矩阵
下表从多个维度对比MySQL原生Online DDL、gh-ost与pt-osc,帮助读者根据实际情况做出选择:
| 维度 | Online DDL(原生) | gh-ost | pt-osc |
|---|---|---|---|
| 实现机制 | 引擎内置 | binlog解析 | 触发器 |
| 对源库侵入 | 低(INPLACE) | 极低 | 中(触发器开销) |
| 可暂停/限流 | 不支持 | 支持 | 支持 |
| 断点续传 | 不支持 | 支持 | 支持 |
| 要求binlog ROW | 否 | 是 | 否 |
| 外键支持 | 原生支持 | 有限 | 支持(需指定策略) |
| 多任务并行 | 支持 | 支持(不同表) | 不支持(触发器冲突) |
| 对大表速度 | 中 | 快 | 中 |
| 社区维护 | Oracle官方 | GitHub活跃 | Perona活跃 |
5.1 推荐选型路径
实际工程中,可以按以下优先级顺序选型:
- 优先尝试MySQL原生Online DDL:对8.0+版本,能使用INSTANT或INPLACE+LOCK=NONE完成的操作,直接用原生DDL,最简单也最安全
- 大表且原生DDL不可用时首选gh-ost:高写入并发、对延迟敏感、binlog已为ROW格式的主库,gh-ost是最佳选择
- 兼容性优先时选pt-osc:旧版本MySQL、binlog为STATEMENT格式、或表上有外键且无法修改binlog格式的场景
- 极端大表可考虑组合方案:先在从库用gh-ost变更验证,再通过主从切换完成线上变更
六、生产环境实操注意事项
6.1 变更前的准备
无论使用哪种方案,变更前都应做足功课:
- 备份:务必先做一份完整备份(XtraBackup或mysqldump),变更失败时可以回滚
- 磁盘空间评估:gh-ost与pt-osc都需要约等于原表大小的额外空间存放影子表
- 从库延迟基线:记录当前从库延迟,变更过程中持续监控
- 变更窗口选择:尽量选业务低峰期,降低触发限流的概率
- Dry-run验证:pt-osc的
1--dry-run
和gh-ost的
1--test-on-replica可以先在从库演练
6.2 变更中的监控
使用以下SQL实时观察变更进度和数据库负载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 -- 查看gh-ost进度(通过影子表的行数估算)
SELECT
table_name, table_rows
FROM information_schema.tables
WHERE table_schema='shop' AND table_name LIKE '%_gho%';
-- 监控当前运行中的DDL线程
SELECT id, user, host, time, state, info
FROM information_schema.processlist
WHERE info LIKE 'ALTER%' OR info LIKE 'gh-ost%';
-- 观察Threads_running(pt-osc与gh-ost限流的核心指标)
SHOW GLOBAL STATUS LIKE 'Threads_running';
-- 监控从库延迟
SHOW SLAVE STATUS\G
-- 关注 Seconds_Behind_Master 字段
建议将
1 | Threads_running |
、
1 | Seconds_Behind_Master |
、
1 | Rows_examined |
接入告警系统,一旦超过阈值自动通知值班DBA。gh-ost的throttle接口也可以与Prometheus + Grafana联动,实现更精细的自动限流闭环。
6.3 变更后的校验
变更完成后不要立即开放全部流量,应做几项校验:
- 行数对比:原表与新表行数应一致(注意
1table_rows
是估算值,必要时用
1COUNT(*)精确比对)
- 索引检查:
1SHOW INDEX FROM tbl
确认新索引存在
- 表结构检查:
1SHOW CREATE TABLE tbl
确认列定义符合预期
- 抽样数据比对:随机选取若干行,比对关键字段值
- 慢查询日志观察:变更后一段时间内关注是否有新增慢查询
七、总结
大表在线DDL是MySQL运维中的高频难题,没有银弹方案。MySQL 8.0的Online DDL在加列、加索引等常见场景下已经足够高效,应作为首选;当遇到需要重建表的操作或对可控性要求更高时,gh-ost凭借无触发器设计和灵活的限流机制成为现代化场景的优选;pt-osc则在兼容性和外键处理上仍有不可替代的位置。理解三者的原理差异,掌握动态限流与监控手段,才能在面对亿级大表变更时胸有成竹、稳如磐石。
最后强调一点:任何大表变更都应先在测试环境或从库演练,并准备好完整的回滚预案。生产无小事,DDL需谨慎。
汤不热吧