MySQL InnoDB Buffer Pool 内存结构、命中率与刷脏调优
更新时间:2026-09-18。Buffer Pool 是 InnoDB 性能核心:磁盘上的数据页/索引页在内存中的缓存,读写几乎都先过它。MySQL 索引原理 解释了页和 B+ 树,本篇解释这些页在内存里怎么组织、怎么淘汰、命中率怎么看才不被误导。日志侧见 redo/undo log 与崩溃恢复。
本文要回答的问题
- Buffer Pool 缓存了什么?改进 LRU 的 young/old 分区解决什么问题?
- 命中率公式是什么?为什么「命中率 99%」也可能很慢?
innodb_buffer_pool_size设多大?为什么专用库建议物理内存 50~75%?- 脏页怎么刷?page cleaner、
innodb_io_capacity如何配合? - Change Buffer、AHI、预读分别扮演什么角色?
一、Buffer Pool 的内容
以 16KB 页为单位缓存,主要包含:
| 组成 | 作用 |
|---|---|
| 数据页(index pages) | 聚簇/二级索引的 B+ 树页,行在聚簇叶子页里 |
| 自适应哈希(AHI) | 对频繁等值访问的页自动建哈希,让查找近似 O(1) |
| Change Buffer | 二级索引页不在内存时暂存其增删改,稍后合并 |
| undo 页/锁信息 | 支撑 MVCC 与回滚 |
| 脏页(dirty) | 被改但尚未刷盘的页 |
二、改进 LRU:防止全表扫描冲掉热点
朴素 LRU 的致命问题:一次全表扫描/备份读入几百 GB 冷页,会把热点全挤出去(缓冲池污染)。InnoDB 用中点插入策略:
+------------------ LRU 链表 ------------------+
| young(默认 5/8) | old(默认 3/8) |
| 真正热点,靠近 new 端 | 新读入页先放这 |
+---------------------------+----------------+
↑ midpoint(innodb_old_blocks_pct=37)
新页先插入 old 头部;在 old 待够 innodb_old_blocks_time(默认1000ms)
且再次被访问,才晋升到 young。全表扫描的页顺序读入 old、彼此覆盖、短时间不晋升,保护了 young 区热点;反复访问的页才晋升。
SHOW VARIABLES LIKE 'innodb_old_blocks%';
-- innodb_old_blocks_pct=37;innodb_old_blocks_time=1000坑一:大备份/全表扫描仍可能扰动缓冲。 大批量统计前可调大 old 区比例或确保尽快扫完;业务库上无 WHERE 的大查询要评估对 young 区的扰动。
三、命中率:别被一个百分比骗了
SHOW ENGINE INNODB STATUS\G
-- BUFFER POOL AND MEMORY: Buffer pool hit rate 990 / 1000
SHOW GLOBAL STATUS WHERE Variable_name IN
('Innodb_buffer_pool_read_requests','Innodb_buffer_pool_reads',
'Innodb_buffer_pool_pages_dirty','Innodb_buffer_pool_read_ahead',
'Innodb_buffer_pool_pages_data','Innodb_buffer_pool_pages_total');
-- hit_rate ≈ 1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests坑二:高命中率不等于快。 ① 命中率是「页命中」,命中了但要回表很多次照样慢(见 MySQL 查询优化);② 冷数据占池、工作集很小,数字好看但无效 IO 仍在;③ 刚重启预热期命中率天然低。更实用的判据是池能否装下热点工作集:看稳态下 Innodb_buffer_pool_reads(真实磁盘读次数)增速与磁盘 IO,而不是只盯百分比。
四、大小、实例与在线调整
| 参数 | 建议 | 说明 |
|---|---|---|
innodb_buffer_pool_size | 专用 DB 取物理内存 50~75% | 留内存给 OS page cache、连接、排序、临时表 |
innodb_buffer_pool_instances | 每实例 ≥1GB,大池 4~16 个 | 多实例降低锁竞争 |
innodb_buffer_pool_chunk_size | 默认 128M | 在线 resize 最小粒度 |
不设到 90% 的原因:还有每连接排序/join 缓冲、临时表、redo/undo、OS 文件缓存;设太满高峰可能 swap,而数据库一旦 swap 延迟灾难性抖动。应关 swap 或调低 swappiness 并留余量。
-- 5.7.5+ 在线调整(低峰做)
SET GLOBAL innodb_buffer_pool_size = 12*1024*1024*1024;
SHOW STATUS LIKE 'Innodb_buffer_pool_resize_status';冷启动预热:开 innodb_buffer_pool_dump_at_shutdown=on / load_at_startup=on,关停/启动时转储恢复热点页地址,缩短预热窗口。
五、刷脏:page cleaner 与 IO 容量
修改先落 Buffer Pool 脏页和 redo log,脏页由后台逐步刷盘,崩溃靠 redo 恢复(详见 redo/undo log 与崩溃恢复)。

| 参数 | 含义/建议 |
|---|---|
innodb_io_capacity | 磁盘常规 IOPS(机械盘几百,SSD/NVMe 数千~2 万),决定后台刷脏速率 |
innodb_io_capacity_max | 紧急刷脏上限(常取 capacity 的 2 倍) |
innodb_max_dirty_pages_pct | 脏页比例阈值(默认约 75/90),逼近加紧刷 |
innodb_flush_neighbors | 是否连带刷相邻脏页;SSD 建议关(0),机械盘可开 |
innodb_page_cleaners | 刷脏线程数,多实例时建议等于 instances |
坑三:IO 容量设错导致周期性 IO 尖刺。 设太低,后台刷脏跟不上,脏页堆到阈值或 redo 空间逼近触发紧急 checkpoint,磁盘 util 100%、RT 周期性抖动;设太高超出真实磁盘能力,后台 IO 挤占前台。要按 fio/压测实测 IOPS 设置(SSD 远高于古老默认 200)。
六、Change Buffer 与 AHI
Change Buffer(写缓冲):对非唯一二级索引页,若目标页不在内存,不立即读页,而把变更缓存到 Change Buffer,等该页被读入时再 merge,省一次随机 IO。写多读少、二级索引多收益大;写后立刻读同一页则收益小甚至反噬。
自适应哈希(AHI):监测到某些 B+ 树页被等值查询频繁访问就自动建哈希。多数 OLTP 有益,但高并发下其锁竞争(btr0sea.c)可能成瓶颈,必要时关 innodb_adaptive_hash_index 做 A/B 对比,不要想当然。
七、排查清单
- 池是否装得下热点工作集(稳态
Innodb_buffer_pool_reads增速、pages_data/pages_total)。 - 命中率看趋势不看瞬时,结合慢查询与回表次数判断「命中但仍慢」。
- 脏页比例与刷脏是否平滑(pages_dirty、磁盘 util 周期尖刺)。
innodb_io_capacity(_max)是否匹配真实磁盘。- 确认没有 swap、预留内存充足。
SHOW ENGINE INNODB STATUS的 BUFFER POOL 段看命中率、youngs/s 与 non-youngs/s(全表扫描污染会让 non-young 飙升)。
八、相关与延伸
页与 B+ 树基础见 MySQL 索引原理;脏页为何能延迟刷、崩溃如何恢复见 redo/undo log 与崩溃恢复;回表与覆盖索引决定逻辑读次数,见 MySQL 查询优化 与 MySQL EXPLAIN 深挖;PostgreSQL 对应物 Shared Buffers 对照见 PostgreSQL 架构与 MVCC。
总结
InnoDB Buffer Pool:以 16KB 页缓存数据/索引/undo/Change Buffer/AHI,用 young/old 两段式改进 LRU 防全表扫描污染热点;innodb_buffer_pool_size 专用库取物理内存 50~75%(留余量防 swap),大池分多实例、可按 chunk 在线调整;命中率要看稳态磁盘读趋势而非瞬时百分比,命中高也可能因回表多而慢;刷脏由 page cleaner 按 redo LSN 与脏页比例平滑进行,innodb_io_capacity(_max) 必须匹配真实磁盘,设低了周期 IO 尖刺、设高了挤占前台。