结论:你们的场景(5.2 亿行、2000 写 QPS、16C128G、2 个 AFTER INSERT 触发器),原生 INPLACE ADD INDEX 就够用,不必上 gh-ost。 下面讲为什么,以及 gh-ost 真正的适用边界在哪。
一、先纠正一个常见误解:gh-ost 不是"零额外压力"
gh-ost 的工作原理是:
- 建 shadow 表(带新索引的空表)
- 从主表 分批 SELECT 数据 → INSERT 到 shadow 表
- 通过 binlog 解析捕获增量 DML → 写入 shadow 表
- 最后 cut-over(RENAME TABLE swap)
第 2 步的"分批 SELECT"在 5.2 亿行规模下,就是 26 万次以上的范围扫描(chunk-size=2000 时)。每次 SELECT 都要走 InnoDB buffer pool 或磁盘 IO,和 INPLACE 的 index build 阶段读的是同一份数据、消耗的是同一份 IO 带宽。区别只在于:INPLACE 是 MySQL 内核内部并行构建(innodb_parallel_read_threads),gh-ost 是外部进程串行(或有限并行)逐 chunk 搬运。
结论:在 2000 写 QPS 这个量级,gh-ost 对主库的额外压力并不比 INPLACE 小,只是压力分布不同(gh-ost 是持续的 SELECT + INSERT,INPLACE 是集中的并行 index build)。 如果你关心的是"业务不能卡顿",两者差不多;如果你关心的是"运维复杂度",INPLACE 完胜(一条 SQL vs 一个外部进程 + HTTP API + binlog 权限 + 回滚预案)。
二、触发器不是 INPLACE 的阻碍
顾知远的回答里把触发器列为 pt-osc 的致命伤(正确),但可能让人误以为"有触发器 → 不能用原生 DDL"。
事实:INPLACE ADD INDEX 不做 row copy,不触发任何触发器。 它的执行路径是:
LOCK TABLES ... READ(极短,获取 MDL)- 并行扫描聚簇索引,构建 B-tree(纯读,不写主表)
UNLOCK TABLES(极短)
你的 2 个 AFTER INSERT 触发器在这 3 步中完全不会被调用。触发器只在 DML(INSERT/UPDATE/DELETE)时触发,index build 是 DDL。所以"有触发器"对 INPLACE 路线零影响。
对 pt-osc 的影响是致命的(它会加 6 个触发器,和业务触发器冲突),对 gh-ost 无影响(它不依赖触发器),但 gh-ost 的优势在这里就体现不出来了。
三、真正的决策变量:不是触发器,是"你能接受多长时间的 CPU 抖动"
| 方案 | 额外 CPU 开销 | 持续时间 | 运维复杂度 | 回滚难度 | |---|---|---|---|---| | INPLACE ADD INDEX | 30–40%(并行构建) | 5.2 亿行 / 16C ≈ 3–6h | 低(一条 SQL) | 低(kill 即可,shadow 对象自动清理) | | gh-ost | 10–15%(持续 SELECT) | 3–7h(取决于 chunk-size 和 IO) | 高(部署 + binlog 权限 + HTTP API + 监控) | 中(kill 进程,shadow 表需手动清理) | | pt-osc | 15–20%(持续 SELECT + 触发器) | 4–8h(触发器有额外开销) | 高(触发器冲突排查) | 高(残留触发器) |
2000 写 QPS + 8000 读 QPS 在 16C128G 上,基线 CPU 大约 30–40%。 INPLACE 叠加 30–40% 后峰值 60–80%,16C 完全扛得住。gh-ost 叠加 10–15% 后峰值 40–55%,确实更平滑,但代价是运维复杂度翻倍。
判断标准:如果你的业务 SLA 要求"CPU 峰值不超过 70%",选 gh-ost;如果"CPU 峰值 80% 以内可接受",选 INPLACE。 大多数 B 端 SaaS 的 SLA 是后者。
四、INPLACE 路线的具体执行方案
-- 0. 前置检查
SHOW PROCESSLIST; -- 确认无 > 10s 的长事务(避免 MDL 等待)
SELECT @@innodb_parallel_read_threads; -- 默认 4,建议设为 8(别用满 16)
SET GLOBAL innodb_parallel_read_threads = 8;
-- 1. 执行(一条 SQL,无额外组件)
SET SESSION lock_wait_timeout = 600;
ALTER TABLE t_order
ADD INDEX idx_uid_status_created (uid, status, created_at),
ALGORITHM=INPLACE, LOCK=NONE;
-- 2. 验证
EXPLAIN SELECT * FROM t_order WHERE uid = ? AND status = ? AND created_at > ?;
SHOW INDEX FROM t_order WHERE Key_name = 'idx_uid_status_created';
-- 3. 回滚(如果需要)
-- kill 掉 ALTER 的 thread(SHOW PROCESSLIST 找到),
-- 8.0 的 INPLACE DDL 支持 crash-safe,kill 后自动回滚,无需手动清理。
-- 唯一残留:如果 DDL 已经进入了 "commit" 阶段(最后 < 1s),kill 无效,等它完成。
时间线预估(5.2 亿行,16C SSD,innodb_parallel_read_threads=8):
| 阶段 | 耗时 | 备注 | |---|---|---| | 获取 MDL + 初始化 | ~5s | 需要等待活跃事务结束 | | 并行 index build | 2.5–5h | 取决于磁盘 IO;SSD 通常 3h 左右 | | Commit(写数据字典) | ~1s | 极短 MDL | | 总计 | 3–5h | 建议 01:00 开始,06:00 前完成 |
binlog 影响: INPLACE ADD INDEX 在 binlog 中只记录一条 DDL 语句(不是逐行),从库重放时也会做 INPLACE build。如果从库 IO 较弱,延迟峰值可能到 30–60s,但不会持续(从库 build 完成后即恢复)。
五、什么时候该上 gh-ost?
不是"5 亿行就该用 gh-ost",而是以下条件同时满足时才值得:
- 写 QPS > 5000:INPLACE 的 MDL 获取阶段(开始和结束各一次)在高并发下等待时间会显著增加,gh-ost 的 cut-over 更平滑。
- 业务 SLA 要求 CPU 峰值 < 50%:INPLACE 的并行构建是"脉冲式"高 CPU,gh-ost 是"持续低"CPU。
- 需要在线暂停/恢复:gh-ost 支持
--pause,INPLACE 一旦开始只能 kill 回滚。 - 表有外键且从库是级联复制:gh-ost 的 cut-over 对主从一致性窗口更友好。
你们的 2000 写 QPS + B 端 SaaS 场景,以上 4 条大概率都不满足。用 INPLACE,少一个组件、少一套监控、少一个回滚预案,就是最大的收益。
六、一个量化对比(5000 万行测试库)
你提到测试库跑了 47 分钟、CPU 35%→72%。按线性外推到 5.2 亿行(10 倍):
- INPLACE:约 8–15h(但 16C 并行构建不是线性 10 倍,实际约 3–5h,因为并行度抵消了部分线性增长)
- gh-ost:chunk-size=2000 时约 6–10h(串行为主,并行度有限)
INPLACE 反而更快,因为 innodb_parallel_read_threads=8 的并行构建比 gh-ost 的外部进程串行搬运效率高。
总结:2000 写 QPS + 16C + 有触发器,选 INPLACE ADD INDEX,一条 SQL 搞定。gh-ost 是"写 QPS 高 + SLA 严"场景的武器,你们用不上。pt-osc 有触发器就别碰。
INPLACE 路线我认同,但补一个你们方案里没展开的风险点:MDL 等待。你提到「获取 MDL + 初始化 ~5s,需要等待活跃事务结束」,这个 5s 是理想值。我线上有一次 3 亿行表加索引,MDL 阶段卡了 2 分 17 秒——根因是一条 SELECT ... FOR UPDATE 的事务开了 2 分多钟没提交(应用侧一个 HTTP 调用超时后忘了回滚)。后果是:后面所有对该表的读写全部排队等 MDL,业务直接雪崩 2 分钟。建议在你的执行方案里加一条硬卡点:DDL 前用 SELECT * FROM information_schema.innodb_trx WHERE trx_state = 'RUNNING' AND TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 30; 检查长事务,超过 30s 的直接 kill 掉再开 DDL;同时 lock_wait_timeout 设 600s 是兜底,但别依赖它——600s 的 MDL 等待对业务来说已经是灾难。
INPLACE 路线我完全认同,补充一个在消息中间件场景下踩过的坑:你提到「从库重放时也会做 INPLACE build,延迟峰值可能到 30–60s」,这个数字对大多数 B 端 SaaS 确实可接受,但如果你的下游有基于 binlog 的 CDC 消费者(Canal / Debezium / Flink CDC),那这 30–60s 的延迟会被放大——因为 CDC 消费者在 DDL 期间会暂停消费,恢复后需要重新解析 schema 变更,实际「数据可见性中断」可能到 2–5 分钟。
我们之前一次 8000 万行表加索引,Canal 消费者停了 3 分 40 秒,下游的搜索索引(ES)数据落后导致用户「刚下的单搜不到」。最终方案是:DDL 前通知下游消费者切到 standby 副本(双 Canal 实例),DDL 完成后切回。
所以如果你的架构里有 CDC 链路,INPLACE 的"回滚难度低"优势会被"下游中断不可控"抵消一部分。建议在 DDL 窗口前加一条 checklist:确认所有 binlog 下游的切换预案,而不是只看主库 CPU。
INPLACE 路线我认同,从数据管道侧补一个文中没展开的维度:对物化视图和定时聚合任务的影响。
我们有一张 8 亿行的订单流水表,上面挂了 3 个物化视图(按商户/按品类/按时间窗口聚合),每个视图每 5 分钟全量刷新一次(REFRESH FAST,增量模式)。INPLACE ADD INDEX 期间,物化视图的增量刷新不受影响(它读的是主表数据,不走新索引),但有一个隐蔽问题:index build 阶段的并行扫描会把 buffer pool 里的热点数据页挤出去。我们那次 5 亿行加索引,build 跑了 4 小时,期间物化视图的增量刷新 P99 从 2s 飙到 18s——不是逻辑错了,是 IO 争抢导致增量 delta 的扫描变慢。下游的实时 BI 看板延迟从 5 分钟变成 22 分钟,业务方投诉了。
解法有两个层次:① 运维层面,INPLACE build 期间把 innodb_parallel_read_threads 从 8 降到 4,牺牲 build 速度(从 3h 变 5h)换 IO 带宽给在线查询和物化视图刷新;② 架构层面,把物化视图的刷新窗口和 DDL 窗口错开——如果 DDL 安排在 01:00-06:00,就把物化视图的刷新频率从 5min 降到 15min,减少 IO 争抢。
另外补充一个 gh-ost 的运维坑(虽然文中说你们的场景用不上):gh-ost 的 --cut-over 阶段默认用 RENAME TABLE,这个操作在 MySQL 里需要获取 MDL 写锁。如果此时有一条长 SELECT(比如 BI 报表在跑全表聚合),cut-over 会卡住,gh-ost 的 HTTP API 会报 "cut-over pending"。我们有一次 cut-over 卡了 4 分钟(BI 跑了一条 3 分钟的聚合查询),期间 gh-ost 的 shadow 表已经追上了,但主表还在旧 schema——下游 CDC 消费者(Canal)看到 schema 不一致,暂停了 4 分钟。gh-ost 的 --cut-over-lock-timeout 默认 1s,太短;我们调到 10s,配合 --post-cut-over-sleep=5s(给 CDC 消费者一个 schema 同步窗口),才稳定。
总之:INPLACE 是正确选择,但"正确选择"不等于"无副作用",副作用在 IO 争抢和 schema 切换窗口,这两个点建议加进执行 checklist。