Online-DDL与大表变更
改一列类型、加一个索引,在千行表上是一秒的小事,在 5000 万行表上就是一场需要彩排、监控、随时可回滚的变更。本章讲清 MySQL 8.4 下三种 DDL 算法(COPY/INPLACE/INSTANT)的边界、为什么一条 DDL 能把整个实例拖垮(MDL 排队雪崩),以及生产大表变更的标准打法:原生 Online DDL 的显式声明、pt-online-schema-change 与 gh-ost 的原理与选型、变更前检查清单和两个完整实战流程。
前置:MDL 与事务的关系在 05-事务与锁.md,复制线程行为在 10-主从复制与高可用.md,本章直接引用结论。
1. 开场:为什么改表结构是高危操作
先看一次真实形状的事故时间线(orders 表,5000 万行,业务高峰):
T+0s 事务 A(某个报表查询)BEGIN,SELECT 了 orders —— 持有 orders 的 MDL 共享锁,但一直没提交
T+30s DBA 执行 ALTER TABLE orders ADD COLUMN remark ... —— 需要 MDL 独占锁,进入等待(State: Waiting for table metadata lock)
T+31s 一条再普通不过的 SELECT * FROM orders WHERE id=1 到达 —— 它也要申请 MDL 共享锁
但 MDL 队列里已有排队的独占请求,后来者按序等待 → 这条主键点查也被挂起
T+60s 应用连接池被挂住的查询占满,健康检查超时,整条业务链路雪崩
T+65s DBA 发现异常 KILL 掉事务 A,DDL 瞬间获得锁开始执行,积压查询涌出两个认知修正:
- DDL 的锁风险不在"执行那几十分钟",而在"拿锁那一刻"。MDL(元数据锁)是表级读写意向锁:任何在途事务只要碰过这张表(哪怕只是一条未提交的 SELECT),DDL 就得排队;而 DDL 一旦排队,它身后所有查询跟着排队——这就是"雪崩"的全部机理(MDL 细节回指
05-事务与锁.md)。 - 报警不是"表被锁了",而是大量线程卡在
Waiting for table metadata lock。发现与处置工具:
-- 谁拿着 MDL、谁在等(8.0,需 performance_schema 开启 metadata_locks 采集,默认开)
SELECT object_schema, object_name, lock_type, lock_status, owner_thread_id
FROM performance_schema.metadata_locks WHERE object_type='TABLE' AND object_name='orders';
-- 直接给"杀掉谁"建议的视图
SELECT * FROM sys.schema_table_lock_waits;
-- 执行 DDL 前,先看有没有长事务在碰目标表
SELECT trx_id, trx_started, trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx
WHERE trx_started < NOW() - INTERVAL 60 SECOND;保命参数——给 DDL 会话设一个短的 MDL 等待上限(默认值是 31536000 秒,即一年,等于不设防):
SET SESSION lock_wait_timeout = 10; -- 拿不到锁 10 秒就失败退出,不拖垮别人,稍后重试
ALTER TABLE orders ADD COLUMN remark VARCHAR(200) NULL, ALGORITHM=INSTANT;注意它与行锁的 innodb_lock_wait_timeout(默认 50s)是两个参数,管 DDL 的是 lock_wait_timeout。“短超时 + 有限重试"是比"长时间干等"安全得多的执行姿势,可以包装成变更脚本的标准动作:
# 抢不到 MDL 就退避重试,最多 10 次;每次失败都确认过没有把自己排成雪崩的源头
for i in $(seq 1 10); do
mysql -h db1 shop -e "SET SESSION lock_wait_timeout=10; ALTER TABLE orders ADD INDEX idx_status (status), ALGORITHM=INPLACE, LOCK=NONE;" && break
echo "MDL busy, retry $i after 30s"; sleep 30
done还有一个时序细节值得记住:MDL 队列对新来的共享锁请求排在已等待的独占请求之后(写优先),这正是"第 31 秒的主键点查也被挂起"的原因;也因此"先杀 DDL 还是先杀长事务"的处置顺序是——先解除 DDL 的等待源(长事务),再决定是否放弃 DDL,反过来只是把队列排空一次、业务继续堵在下一个 DDL 上。
2. 三种算法,三代演进
ALTER TABLE 底层按代价从大到小分三档,语法上可以(也应该)显式声明,防止"本以为是秒级操作,实际偷偷降级成全表拷贝”。先给一张决策树,后文都是它的展开:
要改表结构
├─ 加/删列、改默认值、改自增值、(8.0.29+)任意位置加列?
│ └─ 是 → 原生 ALGORITHM=INSTANT(秒级,复制不延迟,失败即报错)
├─ 加/删二级索引、索引改名、列改名、(同侧)varchar 加长?
│ ├─ 表 ≤ 数百万行 且 能容忍从库整段 DDL 延迟 → 原生 INPLACE, LOCK=NONE
│ └─ 千万行以上 / 延迟敏感 → pt-osc 或 gh-ost
├─ 改列类型、跨长度前缀分界的 varchar、转字符集?
│ └─ 必重建 → 一律工具化(gh-ost 优先);无工具则窗口期 INPLACE 重建
└─ 表上有外键/触发器/无主键?
└─ 工具受限 → 先治理(补主键、盘点外键),再回上面分支;实在不行安排停机窗口 COPY显式声明的语法如下:
ALTER TABLE t ..., ALGORITHM=INSTANT; -- 只改元数据;若该操作不支持 INSTANT 会直接报错,绝不静默降级
ALTER TABLE t ..., ALGORITHM=INPLACE; -- 原地执行;不支持时报错而不是退回 COPY
ALTER TABLE t ..., LOCK=NONE; -- 声明"必须允许并发 DML";做不到就报错
-- ALGORITHM 与 LOCK 可同时给出,取两者交集;都不写则由优化器按最宽松方式自行决定2.1 COPY(第一代)
把数据逐行拷进新结构的临时表,再原子换名。代价:拷贝期间按语义只允许读(LOCK=SHARED)或读写全禁,且大表拷贝时长直接决定服务不可用窗口。8.0 里它已退化为兜底通道:只有少数操作(如某些存储引擎转换、全文索引的部分变更)必须走它。执行前务必留档 SHOW CREATE TABLE/SHOW TRIGGERS——涉及表重命名的路径上,触发器、权限绑定的历史坑很多(各版本行为差异大,疑点见本章末清单),变更前后逐项 diff 复核。
2.2 INPLACE(第二代,Online DDL 的主体)
不新建"逻辑上的另一张表",在当前表空间上操作,分两类:
- 非重建(Only Modifies Metadata: 部分为 Yes):加二级索引、删索引、改索引名、改表名、改默认值等。加二级索引要在 B+ 树旁构建新索引树并回放期间并发 DML 的"在线变更日志"(row log,容量由
innodb_online_alter_log_max_size限制,默认 128M——大表 + 高峰并发 DML 下 row log 写满是经典报错,解法是错峰或调大后重试,DDL 失败不会损坏原表)。row log 满的典型报错形态(文案以小版本为准):
ERROR 1799 (HY000): Creating index 'idx_status_created' required
reorganizing the table. Error: 'Online log of changed rows' exceeded
the maximum size specified by innodb_online_alter_log_max_size
(确切文案以官方手册/实测为准,认准 1799 与 online log 字样即可定位)- 重建表(Rebuilds Table: Yes):改列类型、转字符集、加主键等。本质上仍是"建新聚簇索引再切换",但相比 COPY 全程允许并发读写(
LOCK=NONE的操作),代价是临时空间与时间。
2.3 INSTANT(第三代,8.0 招牌)
只改数据字典(.ibd 里的行格式版本标记),秒级返回、不碰任何一行数据、不需要空间:
- 8.0.12:加列——但只能加在表末尾。
- 8.0.29:加列可以任意位置;并支持删列(逻辑删除:列定义打标记,数据仍占行内空间,空间要等表自然重建才回收)。
- 其他可 INSTANT 的常见操作:改列默认值、改自增计数起点。表改名不在其列(属 INPLACE 的瞬时元数据操作,见第 3 节)。
注意 INSTANT 不是免费的午餐延续体:每次 INSTANT 变更会在行格式里累积一个"行版本",单表可累积的 INSTANT 次数存在上限(与元数据行版本容量相关,具体数字随版本演进,量级在几十次)。超限时新 INSTANT 报错,需要走一次重建型 DDL(如
ALTER TABLE t ENGINE=InnoDB或 OPTIMIZE 语义的重建)归零。长期靠 INSTANT 加加减减的表,要把"重建归零"排进年度维护计划。
选型心智:能 INSTANT 绝不用 INPLACE,能 INPLACE 绝不 COPY;写不出 ALGORITHM= 的变更单,不算写好了变更单。
2.4 高频报错速查
| 报错(关键词) | 含义 | 处置 |
|---|---|---|
ALGORITHM=INSTANT is not supported for this operation. Try ALGORITHM=COPY/INPLACE.(1845,8.0.28 实测) |
该操作没有 INSTANT 资格 | 按支持度表降级到 INPLACE/工具,别删掉声明硬跑 |
ALGORITHM=INPLACE is not supported. Reason: ... Try ALGORITHM=COPY.(1846) |
INPLACE 做不到(多数是必须 COPY 的冷门操作) | 安排窗口或工具;这报错本身就是"降级被拦住"的胜利 |
Lock wait timeout exceeded(1205/MDL) |
抢不到元数据锁/行锁 | 查 information_schema.innodb_trx 与 sys.schema_table_lock_waits,先解长事务再重试 |
Online log of changed rows is full |
row log 打满,并发 DML 太猛 | 错峰重跑;或临时调大 innodb_online_alter_log_max_size(有 OOM 风险,按可用内存算) |
| gh-ost:privileges 类报错 | 账号缺 REPLICATION SLAVE/CLIENT 等 |
在监听节点补权限;确认 log_slave_updates/binlog_format=ROW |
| pt-osc:refuses 表上有触发器/外键 | 工具的硬限制 | 触发器用 --preserve-triggers 评估;外键显式选 --alter-foreign-keys-method,或换 gh-ost/窗口 |
Row version ... too many instant ALTER(文案随版本) |
单表 INSTANT 次数撞行版本上限 | 安排一次重建归零(见 2.3 注意块) |
3. 支持度速查
8.4 基线下核心操作(口径:能否 INPLACE、是否重建表、并发 DML;个别项在不同行格式/场景有例外,执行前用 8.0 手册的 alter-table-online-dtl 表或"影子库试跑 ALGORITHM=INPLACE, LOCK=NONE 看是否报错"复核):
| 操作 | 最优算法 | 重建表 | 并发 DML | 备注 |
|---|---|---|---|---|
| 尾部加列 / 任意位置加列 / 删列 | INSTANT | 否 | 是 | 8.0.12 / 8.0.29 |
| 改默认值 | INSTANT | 否 | 是 | |
| 加二级索引 | INPLACE | 否 | 是 | 耗时随表大小线性增长,空间约需索引本身 |
| 删除/重命名索引 | INPLACE | 否 | 是 | |
| 重命名表 | INPLACE | 否 | 否 | 瞬间完成,但仍是 MDL 独占操作,风险见第 1 节 |
| 改列类型(int→bigint、varchar(50)→varchar(100)) | INPLACE | 是 | 是 | 大表首选工具化(第 6 节) |
| 转字符集/排序规则(如 utf8→utf8mb4) | INPLACE | 是 | 是 | 全表重建且列宽膨胀,空间要按峰值预算 |
| 加主键 | INPLACE | 是 | 否 | 期间禁 DML;通常安排窗口 |
| 列改名 | INPLACE | 否 | 是 | RENAME COLUMN / 8.0 的 CHANGE 语义,不重建 |
| 增删外键约束 | INPLACE/COPY | 视场景 | 受限 | 外键相关变更并发能力弱,大表建议走工具或窗口 |
| 全文索引的多数变更 | INPLACE | 部分重建 | 部分禁 | 细节多,直接查手册对应行 |
两个高频澄清:
- 改 varchar 长度:
utf8mb4下总字节 ≤255 与 >255 是 1/2 字节长度前缀的分界(见08-EXPLAIN与查询优化.md3.5),跨越分界的变更必须重建;同侧之内(如 VARCHAR(50)→VARCHAR(60))8.0 可 INPLACE 不重建(并发 DML 允许)。 - 加索引"卡住业务"通常不是索引构建本身,而是开头/结尾那一瞬的 MDL 抢占——第 1 节时序就是它。
4. 空间与时间账
重建类 DDL 的空间峰值远大于直觉:旧表 + 新表(含新索引) + 期间并发 DML 的 row log/binlog 缓冲,粗略按原表(数据+索引)体积的 2~3 倍预留 datadir 空闲;pt-osc/gh-ost 的影子表同样吃这一份。以 5000 万行、行均 200B、索引 2GB 的 orders 为例:数据 ≈10GB + 索引 ≈2GB,重建类变更前确认 datadir 空闲 ≥ 40GB(含余量)不是夸张。
时间估算给一条工程口径:重建速度大致随"数据+索引总字节数 / 有效写盘带宽"线性,机械盘顺序写 150-300MB/s、SSD 更高但混着随机读写与复制压力,实际常按 **1GB ≈ 15 分钟** 粗估,再乘 2 做计划。5000 万行 ×200B≈10GB + 索引 → 计划窗口按 30~90 分钟,影子库实测为准。
空间核算的标准动作(主从每台都跑一遍,从库常常比主库先满):
df -h /var/lib/mysql
mysql -h db1 -e "SELECT table_name,
ROUND(data_length/1073741824,1) AS data_gb,
ROUND(index_length/1073741824,1) AS idx_gb,
ROUND((data_length+index_length)/1073741824,1) AS total_gb
FROM information_schema.tables
WHERE table_schema='shop' AND table_name IN ('orders','order_items');"由此得出排期三原则:挑低峰(row log 压力小、回放大)、先备盘(空间不足会让 DDL 中途失败,失败清理同样耗时)、估算时长写入变更单并与备份窗口对齐(备份恢复指 11-备份恢复与性能调优.md——变更前先有一份能用的备份,这是纪律不是建议)。
5. 主从复制下的 DDL
原生 DDL 是一条语句复制到从库、由复制 apply 线程从头执行到尾。后果:从库要重复付出与主库同量级的执行时间,这期间该库后续所有变更在回放队列里堆积——Seconds_Behind_Source 直接飙到几十分钟,读切换、缓存一致性全部受牵连(复制线程模型指 10-主从复制与高可用.md)。并行复制(多 worker)也救不了:同库/同表的 DDL 与后续 DML 存在依赖,照样串行。
结论:
- 中小表、可容忍延迟窗口:原生 DDL 可以,但要盯从库(变更单里写"主从延迟恢复前禁止业务切换")。
- 大表(千万行级起步)或延迟敏感:用 pt-osc / gh-ost。它们把"一次大 DDL"拆成成千上万次"小事务 + 小 DML",从库可以边收边并行回放,延迟可控(两者都有专门的限 lag 机制,见第 6 节)。
- INSTANT 例外:它本身就是改元数据的瞬间动作,复制过去同样瞬间执行,不产生延迟——这也是"能 INSTANT 全走原生"的核心理由。
6. 第三方工具:pt-online-schema-change 与 gh-ost
6.1 pt-online-schema-change(Percona Toolkit)
原理四步:
1) 影子表:按"目标结构"建 _orders_new(空表)
2) 触发器增量:在 orders 上挂 INSERT/UPDATE/DELETE 三个触发器,把变更同步进 _orders_new
3) 分块拷贝:INSERT INTO _orders_new SELECT ... 按主键分块搬旧数据;
自适应 chunk(--chunk-time=0.5s 让每块耗时稳定在 0.5s 附近),
每块前检查限流条件:--max-load Threads_running=50(超了暂停)、
--critical-load(超了直接终止)、--check-slave-lag=h22(指定从库 lag 超阈值即暂停)
4) 原子切换:确认增量追平后,rename 原子交换 orders ↔ _orders_new,旧表留作 _orders_old限制要背熟:需要表有主键或唯一索引(分块依据);原表已有触发器时默认拒绝(它要自己创建三个触发器,触发器与"表随 rename 走"的关系是最大历史坑,需要 --preserve-triggers 或人工处理);外键默认拒绝,--alter-foreign-keys-method(rebuild-constraints / drop-swap / auto)各有代价,要显式决策;行拷贝会产生大量 row 格式 binlog,带宽和从库回放都要算账。
两个使用要点:先不带 --execute 跑一次(默认即演练模式),它会把将执行的完整动作打印出来供审;正式执行时cut-over 前会短暂锁写(内部 LOCK/UNLOCK 排空触发器积压),这一瞬同样受第 1 节 MDL 规律支配,所以 --lock-wait-timeout 必须设——这也是"工具方案也要过检查清单"的原因。
6.2 gh-ost(GitHub 开源)
原理差异一句话:不建触发器,自己伪装成从库去拉 binlog。
1) gh-ost 连接目标实例(或其从库),以 REPLICATION 协议订阅 row 格式 binlog
→ 前置:binlog_format=ROW、被监听节点要开 log_slave_updates、账号需 REPLICATION SLAVE/CLIENT 权限
2) 同样建影子表 _orders_gho + 变更日志表 _orders_ghc
3) 旧数据分块拷贝(--chunk 及自适应);期间增量变更从 binlog 流里重放到影子表
4) throttle:--max-lag-millis=1500 盯任一从库延迟、--critical-load 盯 Threads_running,
还可 --throttle-query 挂自定义 SQL(比如"订单高峰时段 SELECT 结果>阈值就暂停")
运行中可用 --throttle-additional-flag-file(touch 出现该文件即暂停拷贝、rm 即恢复)人工控速;
另有 Unix socket 交互命令(子命令集合随版本演进,使用前 gh-ost --help 核对)
5) cut-over:追平后原子改名切换,影子表转正,原表被改名为 _orders_del
——默认保留、不自动删除(除非 --ok-to-drop-table),它就是回滚锚点限制:不支持带外键的表(默认拒绝);表上已有触发器会拒绝;需要额外一份影子表空间(与 pt-osc 同)。
值守时看它的状态行(周期打印,含进度与应用到的增量位点):
Copy: 21340000/50000000 42.7%; Applied: 891230; Backlog: 100/1000;
Time: 01:12:05(total), 00:58:11(copied), 0:00:03(throttled; lag)Backlog 持续涨说明增量跟不上拷贝(该降速或查从库),throttled(lag) 出现是正常的自我克制。同 pt-osc 一样,正式跑之前用不带 --execute 的预检模式验证连通性、权限与 binlog 前置条件。
6.3 选型结论
| 维度 | pt-osc | gh-ost |
|---|---|---|
| 增量捕获 | 触发器(每次 DML 多三笔写) | binlog 监听(无触发器开销) |
| 对业务写入的额外影响 | 略高 | 略低 |
| 从库延迟控制 | –check-slave-lag | –max-lag-millis,交互暂停更顺手 |
| 成熟度/生态 | Percona 系,文档久、用法稳 | 大表变更口碑集中在超大表、高并发写场景 |
| 共同短板 | 需主键/唯一键、外键处理别扭、空间×2、row binlog 量涨 | 同左 |
超大表(千万~亿行)与高并发写优先 gh-ost;能走原生 INSTANT/INPLACE 的不必上工具——工具解决的是"大表重建 + 并发 DML 不能停",不是魔法,它同样要空间、同样要盯延迟。
7. 变更前检查清单
逐条过,缺一不可(可打印随工单):
- 版本与算法可行性:目标操作在 8.4 是否 INSTANT/INPLACE?影子库用
ALGORITHM=INSTANT、ALGORITHM=INPLACE, LOCK=NONE试跑,报错即降级方案(窗口 COPY 或工具)。 - 空间:datadir 空闲 ≥ 表(数据+索引)体积 ×2.5;主从每一台都查,从库更先满。
- 备份:最近一次全备可用 + binlog 备份链完整(
11-备份恢复与性能调优.md);工具方案额外依赖保留的旧表快照(pt-osc 的_old/ gh-ost 的_del)作为回滚锚点。 - 从库延迟与复制拓扑:当前 Seconds_Behind_Source 基线;工具方案配好 –check-slave-lag/–max-lag-millis 的监控对象。
- 长事务与 MDL:
information_schema.innodb_trx无在途长事务碰目标表;DDL 会话显式lock_wait_timeout(10s 级),工具则配--lock-wait-timeout。 - 业务低峰窗口 + 变更期间禁发同表 DDL/大查询。
- 回滚方案:INSTANT 加列 →
DROP COLUMN(8.0.29+ 也 INSTANT)即可反向;重建/工具方案 → 保留旧表快照(_old/_del),观察期内 rename 切回即为回滚;更晚发现数据错误走 binlog 闪回(指11)。反向语句也写进工单并实测过。 - 灰度:先在影子库/从库完整走一遍(gh-ost
--test-on-replica),计时、验空间、验延迟曲线。 - 触发器/外键/权限盘点:
SHOW CREATE TABLE、SHOW TRIGGERS、INFORMATION_SCHEMA.KEY_COLUMN_USAGE留档,变更后 diff。 - 通知与值守:开始/结束/回滚决策人齐备,监控大盘盯 Threads_running、复制延迟、磁盘水位三项。
8. 实战案例
8.1 案例 A:给 5000 万行 orders 加 (status, created_at) 复合索引
加二级索引属 INPLACE 非重建,可以原生做,但从库会整段时间被这一条 DDL 卡住——先确认第 5 节的延迟预算可接受。可接受则:
-- 0) 检查清单:长事务 / 空间 / 从库
SELECT COUNT(*) FROM information_schema.innodb_trx WHERE trx_started < NOW() - INTERVAL 60 SECOND; -- 期望 0
SELECT ROUND(data_length/1024/1024/1024,1) AS data_gb, ROUND(index_length/1024/1024/1024,1) AS idx_gb
FROM information_schema.tables WHERE table_schema='shop' AND table_name='orders';
SHOW REPLICA STATUS\G -- 各从库 Seconds_Behind_Source 基线
-- 1) 执行(显式声明,失败即退出,绝不静默降级成 COPY)
SET SESSION lock_wait_timeout = 10;
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at), ALGORITHM=INPLACE, LOCK=NONE;
-- 若报 "ALGORITHM=INPLACE is not supported":查手册该行为何降级;若报 row log 满:错峰或调大 innodb_online_alter_log_max_size 重试
-- 2) 验证
SHOW INDEX FROM orders WHERE Key_name='idx_status_created';
EXPLAIN SELECT id FROM orders WHERE status='PAID' ORDER BY created_at DESC LIMIT 20;
-- 期望 key=idx_status_created、Extra 无 filesort(对照 08 章 7.2)执行中监控(另开终端):
# 粗进度:DDL 线程状态
mysql -h db1 -e "SHOW PROCESSLIST" | grep -i altering
# 细进度:当前 stage 与完成量(对重建/索引构建类有效)
mysql -h db1 -e "SELECT event_name, work_completed, work_estimated
FROM performance_schema.events_stages_current
WHERE event_name LIKE '%alter%';"
# 从库延迟(每 5s)
while sleep 5; do mysql -h h21 -e "SHOW REPLICA STATUS\G" | grep Seconds_Behind_Source; done延迟预算不可接受,或表更大(亿行)时,改走 pt-osc 做同一件事:
pt-online-schema-change --host=db1 --alter "ADD INDEX idx_status_created (status, created_at)" \
D=shop,t=orders --chunk-time=0.5 --max-load Threads_running=50 --critical-load Threads_running=200 \
--check-slave-lag=h21,h22 --lock-wait-timeout=60 --no-drop-old-table --execute8.2 案例 B:orders.user_id 从 INT 改为 BIGINT UNSIGNED
改列类型必须重建表,5000 万行按第 4 节预算:空间要两份表 + 时间按小时计——原生 INPLACE 做等于把重建风险和从库延迟一次性押上,标准答案是 gh-ost:
# 1) 预检:空间、binlog 格式与从库 log_slave_updates、账号权限(REPLICATION SLAVE, CLIENT 等)
# 2) 先在从库演练:test-on-replica 全程只拷贝、到 cut-over 前停止且不切换,手动清理 _gho/_ghc 即可
gh-ost --host=replica1 --database=shop --table=orders \
--alter="MODIFY user_id BIGINT UNSIGNED NOT NULL" \
--chunk=1000 --max-lag-millis=1500 --critical-load="Threads_running=100" \
--test-on-replica --initially-drop-ghost-table --execute 2>&1 | tee ghost-drill.log
# 记录演练耗时与 lag 曲线,回填变更窗口;不带 --execute 时 gh-ost 只打印计划、不会真正跑
# 3) 正式执行(连主库、以从库为 lag 监控对象;原表改名 _orders_del 默认保留)
gh-ost --host=db1 --database=shop --table=orders \
--alter="MODIFY user_id BIGINT UNSIGNED NOT NULL" \
--chunk=1000 --max-lag-millis=1500 --critical-load="Threads_running=100" \
--throttle-query="SELECT CASE WHEN (SELECT COUNT(*) FROM orders WHERE created_at > NOW() - INTERVAL 1 MINUTE) > 5000 THEN 1 ELSE 0 END" \
--throttle-additional-flag-file=/tmp/gh-ost.throttle \
--cut-over=atomic --execute
# 运行中紧急控速(无需重启任务):
# touch /tmp/gh-ost.throttle # 拷贝立即暂停,业务写入照常
# rm -f /tmp/gh-ost.throttle # 恢复分步验证(切换完成后):
SHOW CREATE TABLE orders; -- user_id 已为 bigint unsigned
SELECT COUNT(*) FROM _orders_del; -- 与预期行数一致
-- EXPLAIN 原高频 SQL,确认列类型变更后计划未漂移(08 章)
-- 观察期(通常 1~7 天)结束后再 DROP _orders_del;期间它就是你的回滚按钮回滚:cut-over 前出问题直接终止即可(原表未动);cut-over 后短时间内 RENAME TABLE orders TO _orders_gho_done, _orders_del TO orders; 即切回(注意改名同样要抢 MDL,放低峰、带短 lock_wait_timeout);更晚发现问题走 binlog 闪回路线(11-备份恢复与性能调优.md)。
等价 pt-osc 写法备查:
pt-online-schema-change --host=db1 --alter "MODIFY user_id BIGINT UNSIGNED NOT NULL" \
D=shop,t=orders --chunk-time=0.5 --max-load Threads_running=50 \
--check-slave-lag=h21 --ask-pass --execute9. 实战建议
- 变更单的算法字段写死:
ALGORITHM=INSTANT / INPLACE, LOCK=NONE显式声明,让"不支持"以秒级报错暴露,而不是以"锁了全库四十分钟"的事故形态暴露。 - 给所有 DDL 会话设
lock_wait_timeout,把第 1 节的雪崩概率降到最低——这条比任何事后监控都便宜。 - 能 INSTANT 的全走原生(加列、默认值),千万行以上重建类一律工具化;gh-ost 与 pt-osc 选一个在团队内标准化,另一套命令参数只作备查。
- 把"重建归零"写进年度计划:频繁 INSTANT 增删列的表会撞行版本上限;同时删列(8.0.29)不回收空间,长期靠重建瘦身。
- 变更前备份先行、变更后 diff 留档:SHOW CREATE TABLE、触发器、索引清单三份快照,出问题比对是分钟级还是小时级的事。
- 观察期纪律:
_old表在业务确认稳定之前不删;删_old本身又是一次 DDL(drop 大表历史上有瞬时 IO/MDL 抖动,同样放低峰做)。 - 分库分表之后,DDL 是 N 倍工程:每一片都要执行、工具要跑 N 遍、回滚要原子协调——变更平台化(一次工单驱动全片灰度)是
12-分库分表.md场景的前置能力。 - 最后把本章压成一句:小表看语法,大表看空间与延迟,所有表看 MDL。
待实测疑点(8.4 环境逐项验证后回填)
- 单表 INSTANT 行版本的确切上限数字,以及超限时报错文案与"重建归零"的推荐操作(OPTIMIZE 是否等效)。
max_length_for_sort_data与 filesort 单/双遍在 8.4 的实际判定(08章 7.1 已按谨慎口径表述)。- ALGORITHM=COPY 路径上触发器/权限随 rename 的具体行为差异(不同小版本曾有过修正,变更前以 SHOW TRIGGERS 留档 diff 为准)。
- gh-ost 在 8.4 + binlog 事务压缩(transaction compression)下的兼容性(官方 issue 区有讨论,使用前以工具 CHANGELOG 为准)。
innodb_online_alter_log_max_size打满时的推荐处置(调大 vs 降并发窗口),生产口径待压测。