Buffer Pool 命中率偏低,通常不是单一问题造成的,常见原因包括缓存污染、未完成预热、参数配置不匹配,以及宽索引等设计反模式。要判断命中率是否真的偏低,不能只看一个瞬时指标,还需要结合重启 30 分钟后的长期命中率、youngs/s,以及 pages_data / total_pages 的比例进行交叉分析,再通过高频访问表与索引排查、unused_indexes 筛选等方式进一步定位根因。

Buffer Pool 命中率低,十有八九并不是 innodb_buffer_pool_size 配得不够大,而是缓存被污染、预热缺失、关键参数没有配套,或者索引设计存在反模式。尤其是在 MySQL 5.7 环境中,如果刚重启就看一眼 SHOW ENGINE INNODB STATUSG 里的 Buffer pool hit rate 并直接下结论,基本很难得出真实判断。
怎么确认是不是真的低?别只看单个数值
MySQL 5.7 中的 Buffer pool hit rate(例如显示为 856 / 1000)本质上是滚动 1 秒窗口内的瞬时值,波动非常明显。特别是服务刚重启后的前 10 分钟,这个指标参考意义有限,因为此时 Innodb_buffer_pool_reads 往往快速增长,而 Innodb_buffer_pool_read_requests 还没有进入稳定区间,最终算出的命中率可能只有 70% 左右,并不能代表真实状态。
- 真正应该重点关注的是:
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';,再用公式(Innodb_buffer_pool_read_requests - Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests * 100计算长期 Buffer Pool 命中率,建议至少运行 30 分钟后再评估 - 同时查看
SHOW ENGINE INNODB STATUSG中BUFFER POOL AND MEMORY段的youngs/s:如果低于 10,通常表示大量数据页刚进入缓存就被淘汰,较大概率存在缓存污染问题 - 再执行
SELECT pages_data, pages_total FROM information_schema.INNODB_BUFFER_POOL_STATS;,如果pages_data / pages_total < 0.75,说明缓存空间并没有真正被有效利用,不一定是“容量太小”,更可能是“没有被用起来”
为什么innodb_buffer_pool_size调大了反而更糟?
在 MySQL 5.7 中,innodb_buffer_pool_size 需要满足 innodb_buffer_pool_instances × 128MB 的整数倍要求。比如设置为 48G,但 innodb_buffer_pool_instances = 5,那么 5 × 128MB = 640MB,48G ÷ 640MB = 76.8,不是整数。此时 MySQL 在启动时会自动向下截断,实际生效值可能只有 47.5G 左右;如果 chunk 对齐失败,错误日志里通常会记录 warning,但不会直接报错,容易被忽略。
- 检查是否真正生效:执行
SHOW VARIABLES LIKE 'innodb_buffer_pool%';,确认当前innodb_buffer_pool_size与innodb_buffer_pool_instances是否和配置文件中的设定一致 - 常见推荐起步值:
innodb_buffer_pool_instances = 8或12,例如 size 为 48G 时,48G ÷ 128MB = 384,384 ÷ 8 = 48,能够整除,更符合 MySQL 5.7 的要求 - instances 设得过高(例如 32)会让单个 instance 过小,LRU 局部性下降,冷热数据混杂问题更明显;设得过低(例如 1)则容易在高并发场景下出现锁争用,导致页加载延迟上升
宽索引和全表扫描才是隐形杀手
像 INDEX (a,b,c,d,e) 这样的宽索引,如果查询实际只使用前两列,InnoDB 仍然需要把整页索引数据加载到内存;而发生回表时,还要再读取一次聚簇索引页。这样一来,两个页都会占用 Buffer Pool,但真正被访问的内容却只是一部分。大量低效、低频的数据页持续挤占缓存空间,就会直接拉低 youngs/s 和整体命中率。
- 定位缓存污染来源:
SELECT * FROM performance_schema.table_io_waits_summary_by_table WHERE COUNT_READ > 100000 ORDER BY COUNT_READ DESC LIMIT 5;先找出高频读取的表,再结合information_schema.STATISTICS检查是否存在 ≥5 列的二级索引 - 验证索引是否冗余:通过
EXPLAIN FORMAT=JSON SELECT ...查看输出中的key_parts和used_columns是否明显不匹配,以判断索引设计是否过宽或利用率过低 - 删除前先筛查闲置索引:
SELECT * FROM sys.schema_unused_indexes WHERE selectivity = 0 AND last_update < DATE_SUB(NOW(), INTERVAL 90 DAY);,这类长期未使用且无选择性的索引,通常可以优先评估是否直接DROP INDEX
预热没做,等于每天都在重复冷启动
Buffer Pool 在启动初期是空的,所有查询都需要触发磁盘读取。若没有做好预热,命中率只能依赖业务流量慢慢把热点数据“暖”进缓存,这个过程可能需要数小时,甚至一两天才能恢复到正常水平。期间常见的连锁反应包括慢查询增多、响应超时以及连接堆积。
- MySQL 5.7 支持
innodb_buffer_pool_dump_at_shutdown = ON和innodb_buffer_pool_load_at_startup = ON,可在重启后自动恢复上次 dump 的热点页,缩短 Buffer Pool 预热时间 - dump 文件默认保存为
ib_buffer_pool,具体路径由innodb_buffer_pool_filename控制,需确认 MySQL 进程具备写入权限 - 手动执行预热时要谨慎:像
SELECT COUNT(*) FROM t1 JOIN t2 USING (id) WHERE ...这类语句很容易把冷数据一并刷入缓存;更建议优先使用SELECT id FROM t WHERE id BETWEEN ? AND ?这类基于主键范围的轻量查询来做热点预热
最难的从来不是简单调参数,而是识别哪些数据页应该留在 Buffer Pool,哪些页应该尽快淘汰。这需要借助 EXPLAIN FORMAT=JSON 观察真实执行路径,结合 performance_schema 分析实际 IO 分布,再使用重启后半小时的 youngs/s 与 pages_data / pages_total 做交叉验证。只是一味增大 innodb_buffer_pool_size,却不处理索引设计和预热策略,本质上就像不断往一个漏水的桶里加水。
