MySQL Online DDL 完全指南:INSTANT/INPLACE/COPY 三种算法、MDL 锁事故与 gh-ost 实战

个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <

文章目录

  • [MySQL Online DDL 完全指南:INSTANT/INPLACE/COPY 三种算法、MDL 锁事故与 gh-ost 实战](#MySQL Online DDL 完全指南:INSTANT/INPLACE/COPY 三种算法、MDL 锁事故与 gh-ost 实战)
    • 一、先把"Online"这个词掰清楚
    • 二、三种算法的执行过程
      • [2.1 COPY:最原始,也最危险](#2.1 COPY:最原始,也最危险)
      • [2.2 INPLACE:原地改,执行阶段放行 DML](#2.2 INPLACE:原地改,执行阶段放行 DML)
      • [2.3 INSTANT:只改元数据,8.0 的救命特性](#2.3 INSTANT:只改元数据,8.0 的救命特性)
    • [三、一张表看懂常见 DDL 走哪条路](#三、一张表看懂常见 DDL 走哪条路)
    • [四、MDL 锁:Online DDL 事故的第一杀手](#四、MDL 锁:Online DDL 事故的第一杀手)
      • [4.1 一次典型的雪崩](#4.1 一次典型的雪崩)
      • [4.2 执行前必做的检查](#4.2 执行前必做的检查)
      • [4.3 让 DDL 自己别变成塞子](#4.3 让 DDL 自己别变成塞子)
    • [五、INPLACE 期间的两个隐藏上限](#五、INPLACE 期间的两个隐藏上限)
      • [5.1 row log 溢出](#5.1 row log 溢出)
      • [5.2 磁盘与临时目录](#5.2 磁盘与临时目录)
      • [5.3 8.0 给大表 DDL 的性能开关](#5.3 8.0 给大表 DDL 的性能开关)
    • [六、什么时候该上 gh-ost / pt-osc](#六、什么时候该上 gh-ost / pt-osc)
      • [6.1 两个工具的原理差异](#6.1 两个工具的原理差异)
      • [6.2 对比选型](#6.2 对比选型)
      • [6.3 gh-ost 实战](#6.3 gh-ost 实战)
      • [6.4 pt-osc 的写法](#6.4 pt-osc 的写法)
    • [七、一套可复制的 DDL 操作流程](#七、一套可复制的 DDL 操作流程)
    • 八、常见误区
    • 小结

MySQL Online DDL 完全指南:INSTANT/INPLACE/COPY 三种算法、MDL 锁事故与 gh-ost 实战

"MySQL 不是支持 Online DDL 吗?为什么我加个字段,业务就雪崩了?"

这是 DBA 日常被问得最多的问题之一。真相是:

Online DDL 说的是"执行期间不阻塞 DML",但它没说"拿锁的时候不阻塞"。

而真正的生产事故,几乎都发生在那拿锁的几秒里。

这篇把三种算法、MDL 锁、inplace 的 row log 上限、以及 gh-ost / pt-osc 怎么选一次讲透。

一、先把"Online"这个词掰清楚

业界口语里的 "Online DDL" 其实混了三件事,必须先分开:

概念 含义 层级
ALGORITHM=COPY 建临时表、逐行拷数据、再 rename 执行方式
ALGORITHM=INPLACE 原地操作,可能重建表,执行期间允许 DML 执行方式
ALGORITHM=INSTANT 只改数据字典元数据,秒级完成(8.0.12+) 执行方式
LOCK=NONE / SHARED / EXCLUSIVE 执行期间允许什么级别的并发 并发度

INPLACE ≠ 不锁表。

INPLACE 只说明"不拷贝整表数据",它在准备阶段和提交阶段依然要拿排他 MDL 锁 。

真正决定"能不能写"的,是 LOCK 子句:LOCK=NONE 才表示全程可读写。

语法长这样:

sql 复制代码
ALTER TABLE orders
  ADD COLUMN channel VARCHAR(32) NOT NULL DEFAULT '' COMMENT '下单渠道',
  ALGORITHM = INPLACE,
  LOCK = NONE;

⚠️ 生产上不建议硬写 ALGORITHM 和 LOCK 。

MySQL 不指定时会按 INSTANT → INPLACE → COPY 自动挑最优的;

你一旦硬指定,本来能 INPLACE 的被你指定成 INSTANT 会直接报错,

指定成 COPY 就是一场事故。让 MySQL 自己选,才是默认正确的做法。

硬指定的价值在于验证 :写 ALGORITHM=INSTANT 能跑通,就说明它确实是 instant 的。

二、三种算法的执行过程

2.1 COPY:最原始,也最危险

复制代码
准备阶段 ─ 拿排他 MDL → 建临时表(server 层 create like)
执行阶段 ─ 逐行 copy 数据到临时表(全程持排他锁,DML 全部阻塞)
提交阶段 ─ rename 临时表为原表名 → 删原表 → 释放锁

特征:执行期间只读、写全阻塞 ,需要约 1 倍表大小的额外磁盘;8.0 里仍有一批操作只能走它。

2.2 INPLACE:原地改,执行阶段放行 DML

复制代码
准备阶段 ─ 共享锁升级为排他 MDL → 判断是否 rebuild → 若 rebuild 则申请 row log 空间
执行阶段 ─ 降级为共享 MDL(DML 放行)→ 扫描/重建数据 → 并发 DML 记入 row log
提交阶段 ─ 再次升级为排他 MDL → 回放 row log → rename → 释放锁

关键点:执行阶段不阻塞 DML ,但首尾各有一小段排他锁窗口;

rebuild 类操作 (加主键、删列、改数据类型)虽是 INPLACE 却要重建整表,耗时与 COPY 同量级;

no-rebuild 类操作(加/删二级索引、改默认值、改名)只动元数据或局部数据,很快。

判断是哪种:看 ALTER 返回的 rows affected。0 rows affected 说明没拷数据,有行数说明重建了表。

2.3 INSTANT:只改元数据,8.0 的救命特性

8.0.12 引入(由腾讯互娱 DBA 团队贡献),只修改数据字典,不动数据页,不加排他 MDL,秒级完成。

8.0.12 起支持:

  • 在表末尾追加列
  • 新增 / 删除虚拟列
  • 设置 / 删除列默认值
  • 修改 ENUM / SET 定义(往后追加值)
  • 修改索引类型、表重命名

8.0.29 起进一步增强 :支持在任意位置 instant 加列,以及 instant 删列。

限制要记住:

限制 说明
行格式 不支持 ROW_FORMAT=COMPRESSED 的表
全文索引 表上有 FULLTEXT 索引时不支持 instant 加列
累计次数 同一张表 instant 加列有累计上限,超了需要重建(看 INFORMATION_SCHEMA.INNODB_TABLES.INSTANT_COLS)
版本 8.0.12 之前完全没有这个能力;5.7 加列一定是 INPLACE rebuild,很慢
sql 复制代码
-- 验证是不是真的 instant
ALTER TABLE t ADD COLUMN ext JSON DEFAULT NULL, ALGORITHM = INSTANT;
-- 不报错就说明是 instant;报 ERROR 1846 就说明不支持,得换方案

三、一张表看懂常见 DDL 走哪条路

基于 MySQL 8.0(InnoDB):

操作 INSTANT INPLACE 重建表 可并行 DML 只改元数据
加二级索引 ❌ ✅ ❌ ✅ ❌
删二级索引 ❌ ✅ ❌ ✅ ✅
索引重命名 ❌ ✅ ❌ ✅ ✅
修改索引可见性 ❌ ✅ ❌ ✅ ✅
加主键 ❌ ✅ ✅ ✅ ❌
删主键 ❌ ❌ ✅ ❌ ❌
删并同时加主键 ❌ ✅ ✅ ✅ ❌
末尾加列 ✅ 8.0.12+ ✅ ❌ ✅ ✅
任意位置加列 ✅ 8.0.29+ ✅ ✅ ✅ ❌
删列 ✅ 8.0.29+ ✅ ✅ ✅ ❌
修改列数据类型 ❌ ❌ ✅ ❌ ❌
扩展 VARCHAR 长度 ❌ ✅ ❌ ✅ ✅(≤255 或同档位)
列重命名 ❌ ✅ ❌ ✅ ✅
改列默认值 ✅ ✅ ❌ ✅ ✅
加 STORED 虚拟列 ❌ ❌ ✅ ❌ ❌
加 VIRTUAL 虚拟列 ✅ ✅ ❌ ✅ ✅
转换表字符集 ❌ ❌ ✅ ❌ ❌
OPTIMIZE TABLE ❌ ✅ ✅ ✅ ❌
表重命名 ✅ ✅ ❌ ✅ ✅

几个值得单独拎出来的点:

  • VARCHAR 扩长度 :VARCHAR(50) → VARCHAR(100) 这种"255 以内"的变化是纯元数据操作,秒完;
    但 VARCHAR(100) → VARCHAR(300) 跨过了字节数档位(1 字节长度前缀 → 2 字节),就要重建表。
  • 加自增列 / 把列改成自增:会锁表并重建,属于高危操作。
  • 第一个全文索引 :5.6+ 会自动补 FTS_DOC_ID 从而避免 COPY,但仍会重建表,且阻塞写。
  • 带 FULLTEXT 索引的表做 OPTIMIZE TABLE / ALTER TABLE ... ENGINE=InnoDB:退化成 COPY 并阻塞写。

四、MDL 锁:Online DDL 事故的第一杀手

4.1 一次典型的雪崩

sql 复制代码
-- 会话 A:一个跑了 40 分钟还没提交的慢事务(或忘了 COMMIT 的手工操作)
BEGIN;
SELECT * FROM orders WHERE id = 1;    -- 事务没提交,一直持有 MDL 共享锁

-- 会话 B:DBA 加字段
ALTER TABLE orders ADD COLUMN channel VARCHAR(32) DEFAULT '';
-- INPLACE,看上去很安全,但准备阶段要拿排他 MDL → 卡住,等待 A 释放

-- 会话 C/D/E......:所有业务 SQL
SELECT * FROM orders WHERE user_id = 100;   -- 全部卡住!

为什么会连锁? MySQL 的 MDL 获取是排队 的。

B 的排他锁请求排在 A 后面,而 C 之后的共享锁请求又排在 B 后面。

于是明明只读的 SELECT 也被阻塞,连接数迅速打满,业务雪崩。

这才是"Online DDL 造成事故"的真实机制------不是 DDL 本身慢,而是它变成了队列里的一个塞子。

4.2 执行前必做的检查

sql 复制代码
-- 1. 有没有长事务(重点关注 trx_started 很早的)
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id AS conn_id, trx_query
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60 ORDER BY trx_started;

-- 2. 谁在等 MDL(需先开启 instrument)
UPDATE performance_schema.setup_instruments SET ENABLED='YES', TIMED='YES'
WHERE NAME = 'wait/lock/metadata/sql/mdl';

SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, OWNER_THREAD_ID
FROM performance_schema.metadata_locks WHERE OBJECT_TYPE = 'TABLE';

-- 3. 当前连接都在干什么
SELECT id, user, db, command, time, state, LEFT(info, 100) AS sql_text
FROM information_schema.PROCESSLIST
WHERE db = 'your_db' AND command <> 'Sleep' ORDER BY time DESC;

确认没有长事务和慢查询之后再动手。

4.3 让 DDL 自己别变成塞子

sql 复制代码
-- 给 DDL 设一个短的锁等待超时,拿不到锁就快速失败,不要堵住后面的人
SET SESSION lock_wait_timeout = 10;
ALTER TABLE orders ADD COLUMN channel VARCHAR(32) DEFAULT '';

-- MySQL 8.0 还可以直接写在语句上
ALTER TABLE orders NOWAIT ADD COLUMN channel VARCHAR(32) DEFAULT '';
ALTER TABLE orders WAIT 10 ADD COLUMN channel VARCHAR(32) DEFAULT '';

快速失败比默默等待安全得多:失败了你重试就行,等待会把整个库拖挂。

五、INPLACE 期间的两个隐藏上限

5.1 row log 溢出

INPLACE 执行期间,并发 DML 会记到一块叫 row log 的区域里,最后回放。

这块区域的大小由 innodb_online_alter_log_max_size 控制,默认 128MB。

如果表很大、写入很猛、DDL 又跑得久,就会撞上:

复制代码
ERROR 1799 (HY000): Creating index 'idx_xxx' required more than
'innodb_online_alter_log_max_size' bytes of modification log

后果是 DDL 直接失败回滚,前面跑的几个小时白费。

应对:

sql 复制代码
SET GLOBAL innodb_online_alter_log_max_size = 1073741824;   -- 临时调到 1G

但更根本的办法是在业务低峰做,或者直接用 gh-ost(它没有这个限制)。

5.2 磁盘与临时目录

  • INPLACE rebuild 需要约等于表大小的额外空间
  • 建二级索引时的排序会在临时目录产生大文件,tmpdir 所在分区要够大
    (8.0 可以用 innodb_tmpdir 单独给 DDL 指定临时目录)
sql 复制代码
SHOW VARIABLES LIKE 'innodb_tmpdir';
SHOW VARIABLES LIKE 'tmpdir';
SHOW VARIABLES LIKE 'innodb_online_alter_log_max_size';

5.3 8.0 给大表 DDL 的性能开关

参数 版本 作用
innodb_ddl_threads 8.0.27+ 并行创建二级索引的线程数
innodb_ddl_buffer_size 8.0.27+ 并行建索引时的 buffer 大小
innodb_parallel_read_threads 8.0.14+ 并行扫描聚簇索引,加速建表/校验
sql 复制代码
-- 大表加索引前临时调大(会话级即可)
SET SESSION innodb_ddl_threads = 8;
SET SESSION innodb_ddl_buffer_size = 8388608;
ALTER TABLE big_table ADD INDEX idx_created (created_at);

六、什么时候该上 gh-ost / pt-osc

先说结论:除了 8.0 上确认是 INSTANT 的那些操作,其余大表 DDL 都建议用工具做。

原因很简单,原生 Online DDL 有几个绕不过去的问题:

  1. 无法真正限速:一旦开始就是全速跑,IO 和 CPU 打满你只能 kill,而 kill 大 DDL 的回滚代价极高。
  2. 无法暂停:发现主库扛不住了,停不下来。
  3. 主从延迟:DDL 在主库跑完后,从库要串行回放同一个 DDL,大表会造成严重延迟(8.0 的并行复制对 DDL 帮助有限)。
  4. row log 溢出风险。

6.1 两个工具的原理差异

pt-online-schema-change(Percona):

复制代码
1. 建新表 _tbl_new(新结构)
2. 在原表上建 3 个触发器(INSERT / UPDATE / DELETE),把增量同步到 _tbl_new
3. 按 chunk 拷贝存量数据
4. RENAME TABLE 切换

gh-ost(GitHub):

复制代码
1. 建幽灵表 _tbl_gho(新结构)+ 变更记录表 _tbl_ghc
2. 伪装成一个 slave,拉取 ROW 格式的 binlog 获取增量
3. 按 chunk 拷贝存量数据(INSERT IGNORE ... SELECT)
4. 持续 apply binlog 增量
5. cut-over:短暂拿一次表锁,原子 RENAME 切换

核心区别就一句话:pt-osc 用触发器捕获增量,gh-ost 用 binlog 捕获增量。

6.2 对比选型

维度 pt-online-schema-change gh-ost
增量捕获 触发器 ROW 格式 binlog
对主库额外负载 触发器同步执行,开销大(实测约 12% 吞吐下降) 异步读 binlog,开销极小(接近 0)
能否限速 / 暂停 支持但粒度粗 ✅ 非常灵活(chunk-size、max-load、nice-ratio、throttle 文件)
已有触发器 ❌ 冲突,不支持 ✅ 不受影响
外键 需要 --alter-foreign-keys-method 额外处理 ❌ 不支持有外键的表
需要主键/唯一键 需要 需要(用于 chunk 切分和行定位)
binlog 要求 无特殊要求 必须 ROW 格式,且 binlog_row_image=FULL
追增量能力 触发器并行,能扛高写入 单线程 apply binlog,超高写入下可能永远追不上
可测试性 一般 ✅ --test-on-replica 先在从库演练
写入停顿 切换瞬间 cut-over 瞬间有毫秒到秒级锁等待
空负载下的速度 更快(约 gh-ost 的两倍) 稍慢

⚠️ gh-ost 那个"追不上"的坑是真实的 :

社区 benchmark 显示,在 25% 满负载写入下,gh-ost 因为单线程回放 binlog 而永远无法完成迁移。

所以高写入表用 gh-ost 前,先观察它的 Backlog 指标 ,如果持续打满 100/100 且 Applied 增长缓慢,就要调小 chunk-size 或改在低峰执行。

6.3 gh-ost 实战

bash 复制代码
# 最简执行:gh-ost --host=127.0.0.1 --user=dba --password='***' --database=shop \
#   --table=orders --alter="ADD COLUMN channel VARCHAR(32) NOT NULL DEFAULT ''" \
#   --allow-on-master --execute
# 生产推荐:带限速、带阈值、可随时暂停
gh-ost \
  --host=127.0.0.1 --user=dba --password='***' \
  --database=shop --table=orders \
  --alter="ADD INDEX idx_created (created_at)" \
  --allow-on-master \
  --chunk-size=1000 \
  --max-lag-millis=1500 \
  --max-load='Threads_running=50' \
  --critical-load='Threads_running=200' \
  --throttle-flag-file=/tmp/gh-ost.throttle \
  --throttle-query="SELECT IF(HOUR(NOW()) BETWEEN 9 AND 21, 1, 0)" \
  --nice-ratio=0.5 \
  --serve-socket-file=/tmp/gh-ost.orders.sock \
  --execute

运行时的输出要看懂:

复制代码
Copy: 3200000/9872432 32.4%; Applied: 15840; Backlog: 3/100;
Time: 12m30s(total), 12m20s(copy); streamer: mysql-bin.000018:641578191;
State: migrating; ETA: 26m10s
  • Copy --- 存量数据拷贝进度
  • Applied --- 已回放的 binlog 事件数
  • Backlog --- 待回放堆积量。持续 100/100 说明跟不上,要降速
  • State --- migrating 正常;throttled 说明被你限流了;postponing cut-over 说明在等切换时机

运行时可动态控制 :touch /tmp/gh-ost.throttle 暂停,rm 恢复;

也可以通过 socket 在线改参数:echo "chunk-size=500" | nc -U /tmp/gh-ost.orders.sock。

先演练再上线 :--test-on-replica 会在从库上完整跑一遍但不切换,用来验证 SQL 正确性和耗时。

6.4 pt-osc 的写法

bash 复制代码
pt-online-schema-change --host=127.0.0.1 --user=dba --password='***' \
  D=shop,t=orders \
  --alter "ADD COLUMN channel VARCHAR(32) NOT NULL DEFAULT ''" \
  --chunk-size=1000 --max-lag=1.5 --check-interval=1 \
  --recursion-method=processlist --no-check-replication-filters --execute

外键表要额外指定 --alter-foreign-keys-method=rebuild_constraints(或 drop_swap,更快但有短暂窗口)。

七、一套可复制的 DDL 操作流程

  1. 分类 :先用 ALGORITHM=INSTANT 在测试库试一下,判断它是 INSTANT / INPLACE / COPY 中的哪一类。
  2. INSTANT? → 直接执行,但仍避开业务高峰。
  3. 小表(< 100MB)? → 原生 Online DDL 直接跑,几秒钟的事。
  4. 大表? → 上 gh-ost / pt-osc。
  5. 执行前检查 :有没有长事务 / 慢查询、磁盘剩余空间够不够(至少 1 倍表大小)、
    主从延迟是否健康、innodb_online_alter_log_max_size 是否要临时调大。
  6. 执行中监控:主从延迟、Threads_running、磁盘 IO、gh-ost 的 Backlog。
  7. 执行后清理 :gh-ost 会留下 _tbl_old(旧表)和 _tbl_ghc,确认无误后再 DROP,并 ANALYZE TABLE 刷新统计信息。
sql 复制代码
ANALYZE TABLE orders;
DROP TABLE IF EXISTS _orders_ghc;
DROP TABLE IF EXISTS _orders_old;   -- 确认新结构没问题之后再删!

八、常见误区

误区 1:INPLACE 就不锁表。 ------INPLACE 在 prepare 和 commit 各要一次排他 MDL,真正的杀手是排队效应。

误区 2:加个字段而已,很快。 ------8.0.12 之前加列是 INPLACE + 重建整表,5000 万行要跑几十分钟到几小时。这就是 INSTANT ADD COLUMN 被称为"救命特性"的原因。

误区 3:DDL 卡住了就 kill 掉。 ------大 DDL 的回滚可能比执行还慢,且期间锁一直不释放。正确做法是先找源头长事务。

误区 4:gh-ost 比 pt-osc 快。 ------空负载下 pt-osc 通常更快;gh-ost 的优势是可控、低侵入、可暂停,不是绝对速度,高写入下甚至可能永远跑不完。

误区 5:Online DDL 可以在高峰期随便做。 ------黄金准则:DDL 永远不要在业务高峰期执行,哪怕它是 INSTANT。

误区 6:8.0 了就不需要 gh-ost。 ------INSTANT 只覆盖"加列 / 删列 / 改默认值 / 加虚拟列"这一小撮;改数据类型、转字符集、删主键、加全文索引照样是重活。

小结

  • Online DDL 不等于不锁表 :INPLACE 在准备和提交阶段各要一次排他 MDL,真正的风险是 MDL 排队导致的连锁阻塞
  • 三种算法:COPY (拷数据、阻塞写)、INPLACE (原地、执行期放行)、INSTANT(8.0.12+ 只改元数据,秒级)
  • 不要硬写 ALGORITHM / LOCK,让 MySQL 自动选;硬指定只用于验证
  • 8.0.29 起 instant 支持任意位置加列和删列;COMPRESSED 行格式、有全文索引的表不支持
  • 动手前必查 长事务和 MDL 等待 ,并给 DDL 设置 lock_wait_timeout 让它快速失败
  • INPLACE 的 row log 上限默认 128MB ,大表要调 innodb_online_alter_log_max_size
  • 8.0.27+ 可用 innodb_ddl_threads / innodb_ddl_buffer_size 加速建索引
  • 大表 DDL 用工具:pt-osc 靠触发器 (更快但侵入大),gh-ost 靠 binlog(低侵入、可限速可暂停,但单线程追增量有上限)
  • gh-ost 要求 ROW 格式 binlog + binlog_row_image=FULL,且不支持有外键的表
  • 上线前用 --test-on-replica 演练,运行时盯住 Backlog 指标
  • 最后一条:DDL 永远不要在业务高峰期执行

下一篇聊备份与恢复------DDL 能改回来的前提是,你有一份能用的备份。

相关推荐
for_ever_love__1 小时前
MySQL 子查询、派生表与 CTE 优化:半连接、物化和递归实战
数据库·mysql·优化·as·子查询·with·cte
DBA_G2 小时前
GBase 8c分布式数据库客户端工具 gsql
数据库·分布式
用户5611610232732 小时前
量化笔记程序代码分析,逐笔数据、五档快照和分钟行情代码分析
数据库
房开民2 小时前
通俗总结 Octree(八叉树)
java·服务器·数据库
星云低代码开发平台2 小时前
AI 连接器运行健康度:指标、异常恢复和联调清单
大数据·运维·网络·数据库·人工智能·mcp
鱼宵2 小时前
LangChain4j 生产化改造:记忆落 Redis、向量落 ES,重启再也不丢
数据库·redis·elasticsearch·langchain4j
星云API技术笔记3 小时前
企业微信外部群机器人实战:多人同时咨询时,如何准确匹配消息与回复对象
数据库
ClouGence3 小时前
开源数据库管理工具 CloudDM 4.4.0 发布,新增 Kafka、SAP HANA 数据源
数据库·kafka
瀚高PG实验室3 小时前
业务无法通过hgproxy访问数据库
数据库·postgresql·瀚高数据库