2382 字
约 7 分钟
16
S11-A22 · 大表的结构变更:在线 DDL 的判据与代价
2024-12-31
2026-11-10

一张三千万行的订单表要加一列。开发在测试环境(十万行)上跑了几秒钟,直接在生产执行,然后看着进度条走了四十分钟——期间该表的写入全部堆积,接口逐级超时。

DDL 在测试环境上的耗时几乎没有任何参考价值:它的代价随表大小与并发写入增长,而"几秒"与"四十分钟"之间不是数量级差异,是机制差异。判断一个 DDL 能不能在线做,需要看清它属于哪一类。


1. 三类 DDL 的实现机制

类型 机制 对写入的影响
元数据变更 只改数据字典 毫秒级
重建表 拷表 + 增量同步 全程双写,接近上限
原地变更 就地修改页 短时阻塞

第二类是"看起来在线、实际最重"的:在线 DDL 的 IO 与 CPU 开销会持续整场,不是一次短阻塞。它比"锁表几秒"更危险,因为影响时间长且容易被忽略。

三类之外还有一个"隐性第四类":加外键或加约束需要校验全表。它不重建表,但要扫一遍数据,代价与表大小线性相关,而且校验期间对写入有额外影响。

第一类是真正廉价的:加默认值(MySQL 8 的 instant)、改注释、改列名(部分版本)都属于这类。

2. 常见变更的档位

变更 档位
加列(有默认值) 元数据(instant)
加列(需重排) 重建表
加索引 重建索引(并行读,双写)
改列类型 重建表
加外键 需校验全表
改字符集 重建表

第四项最容易被低估:varchar(50) 改 varchar(200) 在部分版本是重建,int 改 bigint 一定是重建。类型变更没有"廉价"的版本。

-- MySQL 8:用 ALGORITHM 显式声明,让不支持时立刻失败而不是悄悄降级
ALTER TABLE orders ADD COLUMN channel VARCHAR(16) DEFAULT 'web',
  ALGORITHM=INSTANT;
-- 若该操作不支持 INSTANT,会直接报错 → 在预发就能发现,而不是生产上跑四十分钟

ALTER TABLE orders MODIFY amount BIGINT, ALGORITHM=INPLACE, LOCK=NONE;
-- 显式要求不锁写;若不满足会报错,不会退化成锁表

3. 上线前的四项准备

准备 判据
预估时长 按行数与实测吞吐外推
评估锁与双写影响 写入延迟是否可接受
准备回滚 逆向 DDL 是否同样昂贵
选择窗口 低峰 + 有人盯

第二项(锁与双写影响)要落到具体指标:在变更期间写入延迟上升到多少是可接受的。这个数应当事先写出来,否则"还能忍"会一直忍到用户报障。

第三项常被忽略:加列容易,删列也容易,但"改类型"的回滚同样是一次重建。所以回滚方案的成本必须与正向方案一起评估。

第一项的正确做法是"在等规模的表上实测":用生产数据的一份副本(或从库)跑一次,计时。测试环境的小表给不出这个数。

// DDL 时长外推:以实测吞吐为基础
实测:1000 万行耗时 6 分钟 → 约 10 万行/秒
目标:3000 万行 → 约 30 分钟(线性外推)
修正:写入并发会降低吞吐,取 1.5 倍安全系数 → 45 分钟
结论:需要低峰窗口 + 45 分钟以上的观察期

4. 降低影响的手段

手段 代价
在从库先执行,再切换 需要切换流程
影子表 + 双写 + 切换 工程量大,最安全
分批(按分区)执行 需要分区设计
gh-ost / pt-osc 类工具 额外空间与写入

第四类是业界常用方案:它们通过"影子表 + binlog 同步 + 原子切换"把重建过程变成可控的,代价是需要额外磁盘与一次短暂的表名切换。

这类方案还有一个"必须接受"的代价:切换瞬间的短暂写锁。它通常只有几百毫秒,但在高并发下会形成一段尖峰,所以切换时机要选在流量低谷,而不是"复制完成就切"。

第二类与第四类是同一思路的不同实现:把"一次大 DDL"拆成"后台复制 + 瞬时切换",用户可见的只有切换那一刻。

// gh-ost 的典型参数:限流与切换策略
gh-ost --host=... --database=shop --table=orders \
  --alter="ADD COLUMN channel VARCHAR(16) DEFAULT 'web'" \
  --max-load='Threads_running=30' \      # 超过即暂停,保护生产
  --critical-load='Threads_running=60' \ # 超过即中止
  --chunk-size=1000 --exact-rowcount \
  --switch-to-rbr --allow-master-master=false

时长之外还有一个"不可预见"的量:重放的增量写入。在线 DDL 要同步变更期间的写入,若这段时间写入量很高,追赶阶段的耗时可能超过拷贝阶段——所以窗口选择要同时看"当前负载"与"这段时间的历史写入量"。

// DDL 时间的三段构成
1. 拷贝存量:行数 / 实测吞吐
2. 追增量:变更期间的写入量 / 回放吞吐
3. 切换:短暂写锁(毫秒到秒)
// 多数低估发生在第 2 段,因为评估时只看了第 1 段

5. 变更期间的观测

观测项 为什么
写入延迟 双写放大的直接体现
复制延迟 从库跟不上会拖垮读
连接数与队列 阻塞会传导到连接池
磁盘空间 重建需要额外空间

观测还要覆盖"变更结束之后":回滚窗口内的指标应当继续观察一段时间,因为索引重建、统计信息收集可能滞后发生。变更后立刻宣告完成,往往漏掉这一段的劣化。

第四项是失败主因之一:重建需要一份完整副本的空间,空间不足会让 DDL 在中途失败,而失败后还要回滚已写入的部分。

-- 变更前确认三件事:空间、复制延迟、当前写入压力
SELECT table_name, ROUND(data_length/1e9,1) data_gb, ROUND(index_length/1e9,1) idx_gb
FROM information_schema.tables WHERE table_schema='shop' AND table_name='orders';
SHOW REPLICA STATUS\G   -- 看 Seconds_Behind_Source
SHOW GLOBAL STATUS LIKE 'Threads_running';
-- 空间不足或复制已在追赶 → 先处理,不要开始 DDL

6. 排错:五种"DDL 出问题"

现象 成因 处理方向
执行几十分钟不结束 重建表 + 大表 中止,改工具方案
期间接口超时 双写与锁 降低并发或改窗口
从库延迟暴涨 重建产生大量 binlog 限流或延迟执行
中途失败留下中间态 空间不足/被杀 检查磁盘,准备清理
变更后查询变慢 统计信息未更新 ANALYZE

还有一类"变更后变慢"与统计信息无关:新索引改变了优化器的选择集合,它可能开始选一个"看起来更便宜"但实际更差的计划。这类问题要回到执行计划去看,而不是继续加索引。

第五项很常见且容易误判:DDL 之后优化器对新列/新索引没有统计信息,选择的计划可能很差。执行一次统计信息收集即可,不必怀疑索引本身。

7. 边界:什么情况可以直接执行

场景 为什么 做法
小表(百万行以内) 秒级 直接执行
元数据类变更 毫秒级 直接执行
低峰且无写入 无并发影响 直接执行
只读归档表 无写入 直接执行

最后一条与流程有关:DDL 应当与代码发布分离。同一个窗口里既改结构又改代码,出问题时无法区分是结构变更的连锁反应还是代码缺陷——而这两类问题的排查路径完全不同。

判断口径一句话:这次变更会不会长时间持续占用写入能力。会,就要么换工具(影子表方案),要么换窗口;不会,直接在低峰执行即可。


8. 自测

  1. 三类 DDL 机制里,哪一类"看起来在线、实际最重"?为什么它比短时锁表更危险?
  2. 为什么"改列类型"没有廉价版本?举两个一定重建的例子。
  3. ALGORITHM=INSTANT 与 LOCK=NONE 的作用是什么?它们如何避免"悄悄降级"?
  4. 为什么 DDL 时长必须在等规模的表上实测?外推时为什么要加安全系数?
  5. 影子表方案(gh-ost/pt-osc)把大 DDL 拆成了什么?它需要什么额外资源?
  6. DDL 之后查询变慢,最可能的原因是什么?判断"能否直接执行"的那个问题是什么?
S11-A22 · 大表的结构变更:在线 DDL 的判据与代价
https://www.clxhxhhr.top/posts/6457/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。