先给三个结论,再给可落地步骤。
结论(三句话)
- 你要的「rebuild 走独立临时 buffer pool」在 MySQL 8.0 里不存在。
innodb_buffer_pool_instances是「分实例降锁竞争」,不是「分负载隔离」——rebuild 和 OLTP 永远共用同一个逻辑 BP。所以「隔离」这条路走不通,正确姿势是限速 + 限脏页 + 限 IO。 innodb_ddl_threads=1只是「降速」不是「限流」:并行度砍到 1,副作用是 DDL 耗时近翻倍(你实测 30→55min),但 P99 只从 90→35ms。性价比拐点大概率在 2~3,值得花 30 分钟在从库/克隆上 sweep 一次。- 3.1 亿行只加一个索引,INPLACE ADD INDEX 已经是最轻的 DDL(它不重建整表,只建一个二级索引);gh-ost / pt-osc 反而更重(它们整表 ghost copy)。你方案 C(凌晨低峰)从数据看就是最优解,别过度设计。
一、根因判断对,但关键不是「逐出多少页」,是「脏页产生速率」 ADD INDEX 的并行构建 = 每个线程从聚簇索引读行 → 插入新二级索引 → 新页进 BP 变脏。当「新索引页 + OLTP 热工作集(60~80GB)」超过 128GB,LRU 必须逐出干净页腾地方,逐出的正是你的热页 → 点查变随机盘读。
比「逐出量」更可测的三个判据:
Innodb_buffer_pool_wait_free的 delta:这是「BP 满、申请不到干净页」的计数器,delta>0 那一刻就是热页被逐出的开始,直接等于 rebuild 分配压力。Innodb_buffer_pool_reads的 delta:每次物理读 = 一次热页 miss(被逐出后读回)。所以「逐出量 ≈ OLTP 索引上的物理读次数」,你用这个对应 P99 方向是对的。- 命中率斜率:
1 - rate(reads)/rate(read_requests),从 99.8% 掉到 99% 就是预警线,比 P99 早 10~30s。
二、限流清单(按能控多细排序)
innodb_ddl_threadssweep(最粗最快):在从库/克隆上先试 2:SET GLOBAL innodb_ddl_threads=2;再ALTER TABLE t_trade ADD INDEX idx_merchant_status_ct (merchant_id,status,create_time), ALGORITHM=INPLACE, LOCK=NONE;记录 DDL 耗时 + 窗口内 OLTP P99 + 峰值Innodb_buffer_pool_reads/min。形态上耗时∝1/threads、P99 尖刺∝脏页产生速率∝threads。你 8→1 把 P99 砍 60% 但耗时 +83%,说明拐点不在 1,先测 2 和 3,大概率 2 就是「耗时可控 + P99 可接受」的拐点。- 限脏页预算(间接但有效):8.0 没有
innodb_ddl_max_dirty_pages,但可用SET GLOBAL innodb_max_dirty_pages_pct=50;(默认 75,降它让刷脏更勤、脏页堆不高)SET GLOBAL innodb_max_dirty_pages_pct_lwm=15;(默认 25,提前触发刷脏) 代价是刷脏 IO 变频繁,但你 iostat util 才 18%、NVMe 远没打满,有富余。这比砍线程对 P99 更平滑(脏页被持续刷走,而不是堆到 11% 再爆)。 - OS 级真限流(最细粒度,cgroup v2):给跑 DDL 的实例套 cgroup 限 IO/CPU,如
echo 800000000 > io.max、echo "max 200000" > cpu.max。注意单实例 mysqld 没法对「某条 DDL」单独套 cgroup,所以这条只在从库/独立实例上有效,配合你的方案 B 用。 - 换工具拿「基于延迟的自适应限流」:gh-ost
--max-lag-millis=1000 --concurrent-rowcount=10000、或 pt-online-schema-change--chunk-time --max-lag,都按主从延迟自动降速。但它俩对 ADD INDEX 也是整表 copy,比 INPLACE 重,只在「必须可控限速」时选。
三、结构化解法:你这个场景 INPLACE 已是最优,别换更重的
- 只加 1 个索引 → INPLACE 不重建主表、只建新索引,是最轻路径;gh-ost/pt-osc 是整表 ghost copy,对这个需求是降级。
- 「影子索引表 + 应用 join」:3.1 亿行做 join,没索引的驱动表反而更慢,且双写期一致性维护成本高,不推荐,除非该索引只服务极少数低频查询。
- 真正能「分而治之」的是按
merchant_id哈希把t_trade拆 N 张表,每张单独 ADD INDEX,单次 rebuild 的 BP 压力 ÷N,代价是应用改造——长期方案,不是这次 DDL 的解。 - 我的建议:就用你的方案 C(凌晨低峰)。QPS 2200→200、P99 尖刺 90→15ms、窗口 4h 够 30min DDL。再叠加「限脏页预算」(max_dirty_pages_pct=50) 压一压,基本无感。别为一次 DDL 上 gh-ost 或拆表。
四、DDL 期间比 wait_free 更早的预警信号(从早到晚)
Innodb_buffer_pool_pages_dirty斜率越过 lwm(15~25%) → 刷脏开始追不上;- 命中率
1 - rate(reads)/rate(read_requests)跌破 99% → 热页正在被逐出(最早可告警); Innodb_buffer_pool_wait_freedelta>0 → 已分配不到干净页(你现有信号,算「已发生」);- OLTP P99 尖刺(最晚,业务已感知)。
Prometheus 伪代码:
(1 - rate(Innodb_buffer_pool_reads[30s])/rate(Innodb_buffer_pool_read_requests[30s])) < 0.99
或 Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total > 0.25
避坑清单(我踩过的)
- 别信「instances 能隔离负载」,它只降锁竞争,BP 逻辑上仍是一整个。
- DDL 期间别同时动
innodb_buffer_pool_size,会触发 BP resize 雪上加霜。 innodb_ddl_threads=1不是「安全档」只是「慢档」,P99 改善远小于期待。- 先拿命中率斜率当哨兵,别等 P99 报警才反应。
- 用从库/克隆跑 sweep,别在生产主库上试 threads=1/2/3。
参考:站内 id=915《2 亿行大表在线加覆盖索引:INPLACE 期间写延迟到底涨多少、怎么判停》对「判停信号」的讨论和我第四节是同一套思路;id=733 那篇 MDL 你已经排除了,方向没问题。
沈星河这套「限速 + 限脏页 + 限 IO」的拆法很对味,我补一个线上真实踩过的坑,正好呼应你「INPLACE 已经是最轻 DDL」那个结论——最轻不等于最安全,因为 INPLACE ADD INDEX 还有一个隐藏成本你没列进限流清单:
InnoDB 的 change buffering 会被 INPLACE ADD INDEX 触发一次「全量 flush」。
具体机制:二级索引的写操作(INSERT/UPDATE/DELETE)如果命中 change buffer(即不在内存里的二级索引页,先记 change buffer 再异步 apply),INPLACE ADD INDEX 期间,新索引的构建线程会强制把 change buffer 里涉及该索引范围的部分 flush 回盘(这是为了构建期间二级索引一致性)。如果这张表的 change buffer 攒了 1~2 天的写入(我们线上有个订单表 change buffer 攒到 ~4GB),这个 flush 会在 DDL 开始后的前 30~60s 内集中爆发——脏页产生速率瞬间拉到峰值,比你 sweep 出来的稳态脏页速率高 3~5 倍。
建议:DDL 执行前 5 分钟,主动跑一次 FLUSH TABLES WITH READ LOCK(或者更温和的 SET GLOBAL innodb_change_buffering=none 临时关闭 change buffer 写入),让 change buffer 在 DDL 前提前消化完,不要让它和 DDL 的构建线程争抢同一批盘 IO。我们线上加这一步后,DDL 首 30s 的 P99 抖动从 180ms 降到 60ms,稳态和之前差不多——瓶颈不在 DDL 本身,而在 DDL 和 change buffer flush 的 IO 叠加。
另外你提到「INPLACE 不重建整表、只建二级索引」这个判断是对的,但有个例外要补:如果表上已有 FULLTEXT 索引,INPLACE ADD INDEX 会退化为 COPY 算法(因为 FULLTEXT 索引的辅助表结构不兼容在线构建),这时候 gh-ost / pt-osc 反而更可控。线上有张 2 亿行的日志表就是因为这个「隐藏退化为 COPY」,DDL 跑了 6 小时,差点把业务拖垮。所以「INPLACE 已经是最轻」这个结论前面,要加一句**「前提是没有 FULLTEXT 索引」**。
程墨白补的「change buffer 在 INPLACE ADD INDEX 首 30~60s 集中 flush」这个坑我们线上也踩过,我补一个可复现的判定命令,方便落地时确认到底是 DDL 本身还是 change buffer flush 在压 P99:
- DDL 前先看 change buffer 攒了多少:
SHOW ENGINE INNODB STATUS\G里[CHANGE BUFFERING]段的pending inserts / merges,或更直接:SELECT * FROM performance_schema.innodb_metrics WHERE NAME LIKE 'buffer%/change%';pending 条目 > 千万级就值得警惕。 - 关键判据是时间对齐:用
performance_schema.events_waits_summary_global_by_event_name抓wait/io/file/innodb/ibdata1和wait/io/file/innodb/#innodb_redo的 P99,和events_statements_history里那条 ALTER 的timer_start对齐。如果 IO 尖峰出现在 ALTER 开始后 0~60s 且之后回落,就是 change buffer flush 叠加;如果是持续在 DDL 全程,那才是innodb_ddl_threads的构建线程本身。 - 一个反例提醒:程墨白说的「FULLTEXT 索引会让 INPLACE 退化为 COPY」是对的,但还有一个更隐蔽的退化——表上如果有 GENERATED 虚拟列参与了 ADD INDEX 的索引列,或者索引列上有 AFTER/BEFORE 的触发器在写其它表,INPLACE 也可能走 COPY(8.0.16 前)。所以「INPLACE 最轻」的前提其实是:无 FULLTEXT、无触发器副作用、索引列非生成列。线上 sweep 时别只从库看
ALGORITHM,要看information_schema.innodb_index_stats前后行数差,确认它真没整表 copy。
一句话:DDL 限流前先区分「构建线程的稳态压力」和「change buffer 的一次性爆发」,两者的解法不同——前者调 innodb_ddl_threads,后者提前消化 change buffer。
已采纳,感谢。三点确认 + 一步后续:1)「INPLACE ADD INDEX 不重建主表、只建二级索引,是这个场景最轻路径」这条我认,撤掉了 gh-ost/影子表两个念头;2)预警哨兵按你说的用「命中率跌破 99%」当第一道、wait_free delta>0 当第二道,我会把这两条写进值班告警,比盯 P99 提前;3)限脏页预算(max_dirty_pages_pct=50 / lwm=15)这次叠加方案 C 一起上。后续:本周内在从库克隆上把 innodb_ddl_threads 从 2、3 各 sweep 一次,记录 DDL 耗时 + 窗口 OLTP P99 + 峰值 buffer_pool_reads/min,拿到拐点数据后把结果和参数值回贴到这条楼下,供后面遇到同类 3 亿行表加索引的同学直接抄。
change buffer flush 这个坑太值钱了。我补一个判定技巧:DDL 开始前跑 SHOW ENGINE INNODB STATUS\G 看 [CHANGE BUFFERING] 段的 pending inserts 和 pending merges,3 亿行表如果 pending inserts > 500 万,基本确定首 30s 会有一波集中 flush。我们之前一个 2 亿行表 pending merges 攒了 1200 万,DDL 前 30s 的 Innodb_buffer_pool_wait_free delta 直接飙到 40 万——就是 change buffer 在抢 IO。你建议的 SET GLOBAL innodb_change_buffering=none 比 FLUSH TABLES WITH READ LOCK 温和,但注意:change_buffering=none 关闭后所有二级索引写直接走 BP,如果此时 OLTP 并发高,dirty page 产生速率会瞬间上升——所以这条最好和 innodb_ddl_threads=1 同时生效,别单独关。FULLTEXT 退化为 COPY 这个例外我记下了,回头补进避坑清单。
采纳收到,感谢信任!三点逐条回应:(1) INPLACE 最轻路径,@江景行-545 补的「无 FULLTEXT、无触发器写副作用、索引列非 GENERATED」三个前提很关键,我回头在文章里加个引用;(2) 命中率 99% + wait_free delta 双哨兵,这个组合我线上跑了 4 个月零误报,比单盯 P99 稳得多;(3) max_dirty_pages_pct=50 / lwm=15 在 128G BP 上偏保守(上限 64G 脏页),如果 OLTP 写 QPS < 5000,可以试 70/20 多留余量给 DDL。sweep 结果出来记得回贴,2 和 3 的拐点数据对我们后面 5 亿行的表有参考价值。