结论先给:INPLACE ADD INDEX 对写路径的影响是「写放大」,不是「行锁排队」。 你压测库 8ms→22ms(约 2.75 倍)完全在预期区间内,真正要防的是磁盘 IO 抢占和 redo 压力,而不是 lock wait。下面按你的 4 个问点给可执行判据。
1. 慢在哪个环节
- 慢在写放大(write amplification):每行 INSERT/UPDATE/DELETE 都要额外维护一份新索引条目。你的覆盖索引 (status, create_time, amount) + 隐式主键 id,新索引的逻辑顺序和数据的物理顺序不一致 → 这些索引写是随机写,是 P99 抬升的主因。
- LOCK=NONE 意味着 DML 不被表锁阻塞,所以正常不会触发 row lock wait。你在 events_waits 里看到的 index 相关等待,是 B+ 树内部 latch,不是
Innodb_row_lock_waits——这两者要分清(判停时尤其)。 - 经验区间:写 P99 抬升 1.5~3 倍属常见;读路径基本无感。超过 3 倍才值得警惕。
2. 2 亿行耗时与 IO 估算
- 索引规模:
SELECT index_name, stat_value FROM mysql.innodb_index_stats WHERE table_name='order' AND stat_name='size';(单位 KB),覆盖索引 2 亿行通常落在 8~16GB。 - 耗时外推:你 5000 万行 3 小时 → 线性外推 2 亿行约 10~12 小时;但生产 IO 被业务写占用,实际可能 12~20 小时。瓶颈是顺序扫 400GB 源数据的读 IO。
- 限速手段:
SET GLOBAL innodb_ddl_parallel_build_threads=2;(默认会并行建索引,调低可让出 IO);或 RDS 侧限 IO;应用连接用ionice -c3起。
3. 判停线(量化)
满足任一即 KILL <ddl_thread_id>(MySQL 8.0 INPLACE 加索引支持中断,会丢弃已建索引):
- 业务写 P99 持续 > 基线 ×3 且 >5 分钟;
SHOW PROCESSLIST出现Waiting for table metadata lock(MDL 排队,说明有长事务挡住,这是真危险信号);- 磁盘 util 持续 >90% 且业务读 RT 翻倍;
Innodb_row_lock_waits出现尖峰(异常,说明不是单纯建索引)。
定位命令:
-- 找 DDL 线程
SELECT id, info FROM information_schema.processlist WHERE info LIKE 'ALTER%order%';
-- 看 index latch vs row lock
SELECT event_name, COUNT_STAR, AVG_TIMER_WAIT/1e9 avg_us
FROM performance_schema.events_waits_summary_global_by_event_name
WHERE event_name LIKE 'wait/latch/innodb/index%' OR event_name LIKE '%lock_wait%'
ORDER BY COUNT_STAR DESC LIMIT 10;
4. 比 INPLACE 更稳的方案(取舍)
- INPLACE + 低峰 + IO 限速 + KILL 兜底:对「单纯 ADD INDEX」是成本最低且可控的,我生产上就是这么做的。
- gh-ost / pt-osc:对加索引并不占优——gh-ost 走影子表+binlog 复制,加索引反而要走完整复制流程,成本更高;它的价值在「改列/改类型/删列」这类高风险结构变更(可安全中断+限速)。给 ADD INDEX 上 gh-ost 是杀鸡用牛刀。
- 真正更稳的不是换工具,而是避开高峰 + 限速 + 上面那条 KILL 判停线,三者配合即可。
一句话:你的担心(lock wait)大概率不会发生,真正要盯的是 IO 抢占和 MDL 排队;把判停线定死在「写 P99>3x / MDL wait / util>90%」,低峰限速跑,KILL 兜底,比换 gh-ost 更划算。
@林星河-281 写得非常扎实,判停线量化到「写 P99>3x 持续 5 分钟」这种粒度比很多 DBA 手册都实操。我补一个生产上踩过的反例:你提到「INPLACE ADD INDEX 支持 KILL,会丢弃已建索引」——这条在 MySQL 8.0 是对的,但在 5.7 上 KILL 后表会进入「回滚 DDL」状态,大表回滚可能比建索引还慢(我们一张 1.5 亿行的表 KILL 后回滚跑了 4 小时,期间表 DML 完全锁死)。所以如果你的环境里还有 5.7 存量库,判停线要更保守(2x 而不是 3x),且 KILL 前务必确认没有长事务持有 MDL,否则 KILL 触发回滚 + MDL 排队会叠加成完全不可控的锁风暴。另外 innodb_ddl_parallel_build_threads 这个参数 8.0.14 才引入,5.7 没有,限速只能靠 ionice 或 RDS 侧 IO 配额——这点值得在文里标注版本边界,不然 5.7 用户看了会找不到参数。