MySQL 8.0 升到 8.4 后变慢实测:写入慢 2.3–3.4 倍,主因是 change buffer 默认关了,不是自适应哈希
在测试库上用 MySQL 8.0.46 和 8.4.10 逐项复现 InnoDB 默认值变化(自造 300 万行 orders,128MB 缓冲池):同样写 20 万行,8.0 默认 4.91–5.20 秒,8.4 默认 12.09–16.55 秒,读盘页数从约 5 千涨到约 20.7 万;只把 innodb_change_buffering 改回 all 就降到 6.30–6.46 秒,缓冲池给到 1GB 则 3.14–3.32 秒。自适应哈希(AHI)关闭只在 8 并发 join 上慢约 28%,点查和单线程无差别。附 13 项默认值对比表、只读排查 SQL、复现脚本与 CSV。
MySQL 8.0.46 → 8.4.10 实测(测试库、自造数据,2026-11-04):二级索引装不进缓冲池的写入负载,8.4 默认配置比 8.0 默认慢 2.3–3.4 倍,主因是 innodb_change_buffering 从 all 变成 none——在 8.0 上单独关掉它也慢到 10.62–16.50 秒;8.4 改回 all 后 6.30–6.46 秒。innodb_adaptive_hash_index 从 ON 变 OFF 只影响并发 join 这类反复走 B 树的读(8 并发 6.27–6.39 秒 vs 开启后 4.83–4.97 秒)。我的判断:升级后变慢先看 Innodb_buffer_pool_reads 有没有暴涨,按负载决定改回 change buffering 还是加内存;不要把 8.0 的整份 my.cnf 抄回去。
适用对象
- 结论先说:本次测试里 8.4.10 默认配置写 20 万行要 12.09–16.55 秒,8.0.46 默认只要 4.91–5.20 秒;差距几乎全部来自 innodb_change_buffering=none(8.0 上单独关掉它也要 10.62–16.50 秒)。
- 慢在读盘:每插一行都要把随机位置的二级索引页读进缓冲池。8.4 默认这一轮 Innodb_buffer_pool_reads 约 20.6–20.9 万、读 3.2GB;改回 all 后约 5 千、179MB。
- 修法按负载选:SET GLOBAL innodb_change_buffering=all(动态生效)回到 6.30–6.46 秒;或者让索引装进内存,1GB 缓冲池下 8.4 默认只要 3.14–3.32 秒,比 128MB 下的 8.0 默认还快。
- AHI 关闭不是写入变慢的原因:点查、单线程 join 开关无差别;8 并发 join 关闭时慢约 28%(6.27–6.39 秒 vs 4.83–4.97 秒)。只对这类负载值得打开。
- 本机对比 13 项默认值变化,还包括 innodb_flush_method fsync→O_DIRECT、innodb_io_capacity 200→10000、innodb_log_buffer_size 16M→64M;把 flush_method 改回 fsync 不能解决写入变慢(15.32–15.43 秒)。
8.0.46 和 8.4.10 实际差了哪些默认值(本机 SHOW VARIABLES 对比)
两个实例的 my.cnf 只写了端口、路径、关闭 binlog 和缓冲池大小,其余全用编译默认值。我导出两边全部 innodb_% 和 temptable_% 变量做 diff,得到下面 13 项(不含版本号)。官方《What Is New in MySQL 8.4 since MySQL 8.0》的 Table 1.1 列出的也是这一批,另外几项(buffer_pool_instances、page_cleaners、parallel_read_threads 等)在本机配置下两边取值相同或只是计算方式变化。
判断一项变化会不会让你变慢,先看它影响的是哪条路径:change_buffering 影响写二级索引时要不要先读页;adaptive_hash_index 影响读路径上 B 树查找;flush_method、io_capacity、doublewrite 影响刷盘。下面两节就按这个顺序测。
本节判断
- 如果你的 8.0 配置文件里本来就显式写了这些变量,升级后它们不会变;只有「依赖默认值」的那几项会悄悄改变。下载区 SQL 的第 2 段用 performance_schema.variables_info 查每个变量的来源,VARIABLE_SOURCE=COMPILED 就是在用默认值。
| 变量 | 8.0.46 | 8.4.10 | 主要影响 |
|---|---|---|---|
| innodb_change_buffering | all | none | 写二级索引:页不在内存时要先读盘 |
| innodb_adaptive_hash_index | ON | OFF | 读:热点 B 树查找走不走哈希 |
| innodb_flush_method | fsync | O_DIRECT | 数据文件绕过操作系统页缓存 |
| innodb_use_fdatasync | OFF | ON | 刷盘系统调用 |
| innodb_io_capacity / _max | 200 / 2000 | 10000 / 20000 | 后台刷脏和合并的 IO 上限 |
| innodb_log_buffer_size | 16M | 64M | redo 日志缓冲 |
| innodb_doublewrite_pages | 4 | 128 | 双写批量 |
| innodb_numa_interleave | OFF | ON | 多路服务器内存分配 |
| innodb_purge_threads | 4 | 1 | 本机 8 核;8.4 按 CPU 数计算 |
| innodb_buffer_pool_in_core_file | ON | OFF | core 文件是否带缓冲池 |
| temptable_use_mmap / temptable_max_mmap | ON / 1G | OFF / 0 | 内部临时表溢出方式 |
写入慢 2.3–3.4 倍:change buffer 从 all 变成 none
测试负载:orders 表有主键和两个二级索引,idx_order_no(order_no=MD5(id)) 和 idx_shop_status(shop_id, status),键值随机分布。每一轮都先把 orders 重置成同一份 300 万行、慢关机合并干净,再用 128MB 缓冲池冷启动,然后用存储过程分 200 批、每批 1000 行插入 20 万新行。128MB 远小于约 300MB 的二级索引,模拟「索引装不进内存的写多表」。
change buffer 的作用是:要改的二级索引页不在缓冲池时,先把改动记下来,等这个页以后被读进来时再合并,从而省掉一次随机读。8.4 默认 none 之后,每插一行都要先把对应的 idx_order_no 页从磁盘读进来。这一轮读盘页数从约 5 千涨到约 20.7 万,读了 3.2GB,耗时也跟着翻了 2–3 倍。
本节判断
- 「8.0 只关 change buffer」这一行是关键对照:版本没换,只动这一个变量,就慢到和 8.4 默认同一个量级。说明主因是它,不是版本本身。
- 改回 all 后 8.4 仍比 8.0 慢约 25%(6.3 秒 vs 约 5.0 秒)。我没有再逐项拆开;一个可能的原因是 O_DIRECT 绕过了操作系统页缓存(本机 15GB 内存,8.0 用 fsync 时能吃到系统缓存),这一点只是推测,未单独验证。
- 我也试过把 8.4 的 innodb_flush_method 改回 fsync、其他保持默认(change_buffering 仍为 none)再测,两次分别为 15.32 和 15.43 秒(启动参数 --innodb-flush-method=fsync,运行记录里写明 flush=fsync),仍落在 8.4 默认的 12.09–16.55 秒区间内,没有改善。
- change buffer 不是白给的:改回 all 的那几轮结束时,SHOW ENGINE INNODB STATUS 里 Ibuf 还有约 1,070–1,140 页没合并,这部分工作被推迟到后台、后续读或慢关机。官方文档也写明:受影响的行和二级索引很多时,合并可能持续数小时,期间磁盘 IO 增加、读盘查询明显变慢,合并还可能延续到事务提交之后甚至重启之后。官方没有单独解释 8.4 为什么改成 none,这是改回 all 时要自己承担的代价。
| 版本 / 配置 | 耗时(秒) | Innodb_buffer_pool_reads | 读盘量 |
|---|---|---|---|
| 8.0.46 默认(all,AHI 开,fsync) | 4.91 / 4.95 / 5.20 | 4,031–5,534 | 79–101MB |
| 8.0.46 只改 change_buffering=none | 10.62 / 12.15 / 16.50 | 186,650–187,828 | 约 2.9GB |
| 8.4.10 默认(none,AHI 关,O_DIRECT) | 12.09 / 15.90 / 16.55 | 206,452–208,699 | 约 3.2GB |
| 8.4.10 只改 change_buffering=all | 6.30 / 6.30 / 6.46 | 4,976–5,336 | 约 179MB |
| 8.4.10 默认 + 缓冲池 1GB | 3.14 / 3.32 | 5,548–7,272 | 87–114MB |
自适应哈希关了:只有并发 join 慢约 28%
网上很多文章把 8.4 变慢归到 AHI 关闭。我在 8.4.10、1GB 缓冲池(数据全在内存)下用 mysqlslap 动态开关 innodb_adaptive_hash_index 测了三类读:随机点查、3 万行热点点查、10 万行热点 id 去 join orders(每个客户端跑 10 次)。每组 3 次,先预热再计时。
结论很清楚:点查和单线程 join 开关无差别,差值在噪声范围内;只有 8 并发 join 时,关闭 AHI 要 6.27–6.39 秒,开启只要 4.83–4.97 秒,慢约 28%。从 innodb_metrics 看,开启后 8 并发这一组约 1,000 万次查找走了哈希(adaptive_hash_searches),关闭时这些全部回到 B 树查找(adaptive_hash_searches_btree 约 1,560 万)。
本节判断
- 官方文档的说法是双向的:AHI 能让合适的负载更像内存数据库,但在「多个并发 join」这类重负载下也可能成为争用点。本次 8 并发、8 核的规模里它是净收益;更高并发下会不会反转,我没有测,需要你在自己的负载上开关对比。
- innodb_adaptive_hash_index 可以 SET GLOBAL 动态开关,关闭会立即清空哈希表,打开后要随访问重新建立,所以对比时要先预热。
| 负载 | 并发 | AHI 关(秒) | AHI 开(秒) | 判断 |
|---|---|---|---|---|
| 20 万次热点点查(3 万行热点) | 1 | 3.10–3.16 | 3.23–3.30 | 无差别 |
| 20 万次热点点查(3 万行热点) | 8 | 3.96–5.40 | 3.93–4.41 | 噪声范围 |
| 10 万行 join × 10 次 | 1 | 3.53–3.57 | 3.48–3.72 | 无差别 |
| 10 万行 join × 10 次 | 8 | 6.27–6.39 | 4.83–4.97 | 关闭慢约 28% |
为什么我说「先查 change buffer,别先开 AHI」
这次我考虑过五个切入角度:把 13 项默认值做成升级清单;AHI 该不该打开;加内存比改参数更有效;别把 8.0 的 my.cnf 原样抄回去;以及「升级后写入变慢,主因是 change buffer」。最后我选了最后一个作为主判断,因为它最能直接指导排查顺序,而且有一个干净的对照(8.0 上单独关掉 change buffer 就复现了变慢)。
理由有三。第一,影响量级不同:写入路径慢 2.3–3.4 倍,AHI 只在特定读负载上差约 28%。第二,症状可以直接量出来:change buffer 关闭导致的变慢一定伴随 Innodb_buffer_pool_reads 暴涨,看一个计数器就能确认或排除,不用猜。第三,修法不止一种:如果内存允许,让二级索引装进缓冲池(本次 1GB 时 3.14–3.32 秒)比改回 change buffering 更稳,也避开了后台合并带来的额外 IO。
我不建议「把 8.0 的整份默认值抄回 8.4」。flush_method 改回 fsync 本次没有任何收益,io_capacity 该设多少取决于磁盘的真实 IOPS——这几项要各自按证据决定,而不是一次性回退。
本节判断
- 写多、二级索引大于缓冲池、读盘页数在升级后明显上涨 → 先 SET GLOBAL innodb_change_buffering=all 观察,或评估扩大缓冲池;确认有效后写进 my.cnf。
- 读多、并发 join 多、CPU 高而读盘不高 → 在低峰对比 AHI 开 / 关,看 adaptive_hash_searches 和延迟。
- 读写都没变化但延迟抖动 → 查 io_capacity 是否超过磁盘真实能力(本次未测,云盘 IOPS 有上限时要特别注意)。
在自己库上怎么判断:4 步只读检查
下面这些都是只读查询,下载区的 SQL 文件里有完整版本。最好在升级前的 8.0 上先跑一遍留底,升级后同一时段再跑一遍对比;没有留底的话,就用业务低峰和高峰各取一次。
本节判断
- 第 1 步:查 13 个变量当前值和来源(performance_schema.variables_info),确认哪些是跟着默认值变的。
- 第 2 步:间隔 60 秒取两次 Innodb_buffer_pool_reads,算每秒读盘页数;和升级前比涨了几倍。
- 第 3 步:按 information_schema.TABLES 的 INDEX_LENGTH 排出二级索引最大的 20 张表,和缓冲池大小对比;再用 performance_schema.table_io_waits_summary_by_table 找写入等待最多的表。两张榜单重合的表最可能受 change buffering=none 影响。
- 第 4 步:查 INNODB_METRICS 的 adaptive_hash_searches 和 adaptive_hash_searches_btree,结合慢查询里是否有大量并发 join,再决定 AHI 要不要做开关对比。
- 任何改动先在从库或测试实例验证,改完记录前后的读盘页数和 P99 延迟;体检时我也是按这个顺序交付结论的。
觉得有用?转给正在做这件事的同事或朋友
参考依据
以下来源用于确认市场趋势、政策背景和术语边界;具体落地方案仍以客户的数据范围、权限和交付目标为准。
常见问题
MySQL 8.4 升级后写入变慢,最可能是哪个参数?
本次实测(MySQL 8.0.46 → 8.4.10,测试库、自造 300 万行、128MB 缓冲池)最主要的是 innodb_change_buffering 从 all 改为 none:8.4 默认写 20 万行 12.09–16.55 秒,改回 all 后 6.30–6.46 秒;8.0 上单独改成 none 也会慢到 10.62–16.50 秒。确认方法是看 Innodb_buffer_pool_reads 是否在升级后明显上涨。
要不要把 innodb_adaptive_hash_index 重新打开?
取决于负载。本次点查和单线程 join 开关无差别,8 并发 join 关闭时慢约 28%。官方文档也提醒它在多并发 join 这类重负载下可能成为争用点。建议在低峰动态开关对比,看 adaptive_hash_searches 和延迟再决定。
把 innodb_change_buffering 改回 all 有什么风险?
改动会推迟到后台合并:本次改回 all 后结束时仍有约 1,100 页未合并。官方文档提到合并可能持续数小时,期间读盘查询会明显变慢,合并还可能延续到重启之后。内存允许的话,优先让二级索引装进缓冲池。
能不能直接把 8.0 的默认值全部写回 8.4 的配置文件?
不建议一次性回退。本次把 innodb_flush_method 改回 fsync 没有改善写入(15.32–15.43 秒);io_capacity 要按磁盘真实 IOPS 设。按证据逐项改,每改一项记录前后指标。
这些数字能代表我的生产库吗?
不能直接代表。这是测试库、自造数据、8 vCPU 虚拟机和本地 SSD 上的结果,用来说明机制和量级。生产以你库上的读盘页数、写入延迟对比为准,复现脚本只许在测试实例运行。
下一步
推荐动作
你提供升级前后两次只读 SQL 的结果(变量来源、读盘页数、二级索引大小与写入最多的表,可脱敏),我们判断变慢来自哪一项默认值变化、该改参数还是加内存,并给出验证和回退步骤;生产变更由你方授权执行。