MySQL InnoDB 缓冲池与刷脏机制实战:从 free list、flush list 到 page cleaner 的参数调优
MySQL InnoDB 缓冲池与刷脏机制实战:从 free list、flush list 到 page cleaner 的参数调优
晚高峰,订单库的 RT 突然从 20ms 飙升到 500ms,监控显示磁盘 IO 使用率飙到 100%,但 QPS 并没有明显上涨。登录服务器,SHOW ENGINE INNODB STATUS 一看:Modified db pages 占到了 Buffer pool 的 85%,Free buffers 只有几十个,而 Buffer pool hit rate 却高达 99.8%。
这个现象很有迷惑性:命中率这么高,为什么还会性能抖动?答案藏在 InnoDB 的脏页刷盘机制里。这篇文章我会从缓冲池内部结构讲起,结合真实的故障排查过程,给出可落地的参数调优方案。
这次故障也让我意识到:监控指标只是表象,理解内部机制才能真正定位问题。这是我从一个只会背参数的 CRUD 工程师,开始向内核级开发者转变的起点。
1. InnoDB 缓冲池内部结构:三张链表的分工
InnoDB 缓冲池(Buffer Pool)是内存中的一块区域,用来缓存数据页和索引页,避免每次读写都去访问磁盘。缓冲池的大小由 innodb_buffer_pool_size 控制,通常设置为物理内存的 50%~80%。
要理解脏页刷盘,必须先理解缓冲池中的三张链表:
1 | +-------------------+ +-------------------+ +-------------------+ |
- Free List:存放当前空闲、可直接分配的页。当缓冲池需要加载新页时,从这个链表中获取。
- LRU List:存放所有已使用的页(包括干净页和脏页)。采用改进的 LRU 算法,将链表分为 young 区和 old 区,避免全表扫描污染缓存。
- Flush List:只存放脏页(数据被修改但尚未写回磁盘的页)。脏页会按
oldest_modification(最早修改时间)排序,刷盘时从链表尾部开始。
页的状态流转大致如下:
1 | 磁盘读取 --> 从 Free List 取空闲页 --> 加入 LRU List(old 区头部) |
理解了这三张链表,就能明白为什么脏页过多会导致性能抖动。
2. 定位缓冲池命中率低:SHOW ENGINE INNODB STATUS
先看一个通用场景:缓冲池命中率低。如果 Buffer pool hit rate 长期低于 99%,说明大量请求不得不访问磁盘,延迟必然升高。
示例 1:查看缓冲池状态
1 | -- 登录 MySQL,执行: |
关键输出片段:
1 | ---------------------- |
关键字段解读:
Buffer pool size:缓冲池总页数(8192 页 * 16KB = 128MB)。Free buffers:当前空闲页数量。如果长期接近 0,说明缓冲池空间紧张。Database pages:LRU 列表中的页数,即已使用的页。Modified db pages:脏页数量。这里显示 5600 页,占总缓冲池的 68%,已接近默认最大脏页比例 75%。Buffer pool hit rate:命中率,格式为命中次数 / 总访问次数。998/1000 = 99.8%,看着很高,但注意这是累计值,不能反映瞬时抖动。
命中率低时,通常 Free buffers 很小,Database pages 接近 Buffer pool size,并且 Pages read 速率较高。这时最简单的优化就是增大 innodb_buffer_pool_size。
验证环境:MySQL 8.0.36,Linux x86_64,缓冲池 128MB(示例值)。
3. 用 performance_schema 精确分析缓冲池与脏页
SHOW ENGINE INNODB STATUS 展示的是累计值,无法看到实时比例。生产环境中我们更常用 performance_schema 和 information_schema 里的表来获取精确统计。
示例 2:查询缓冲池命中率与脏页比例
1 | SELECT |
示例输出:
1 | +---------+-----------+--------------+----------------+-------------+-----------+---------+----------+ |
可以看到:
dirty_pct = 75%:脏页比例已经达到默认的innodb_max_dirty_pages_pct上限,触发强制刷盘。hit_pct = 99.8%:命中率很高,但脏页太多,用户线程在申请空闲页时可能被迫同步刷脏。free_buffers = 8192:空闲页看着还够,但这是瞬时值,可能在高峰时被迅速耗尽。
如果想进一步定位是哪些表占用了缓冲池,可以使用 sys schema 的视图:
1 | SELECT * FROM sys.innodb_buffer_stats_by_schema ORDER BY allocated DESC LIMIT 5; |
这样就能知道缓冲池被哪个库、哪张表占用最多,便于针对性优化。
验证环境:MySQL 8.0.36,缓冲池 1GB,Sys Schema 已启用。
4. 脏页刷盘机制与 page cleaner 线程
4.1 脏页从哪来?
当执行 UPDATE、DELETE、INSERT 等 DML 操作时,InnoDB 会先修改缓冲池中的数据页,而不是直接写磁盘。修改后的页就是脏页,会被加入 Flush List。脏页最终需要写回磁盘,以保持数据持久性。
脏页刷盘由后台线程 page cleaner 负责。page cleaner 的工作节奏受多个参数控制。
4.2 刷盘触发条件
刷盘并不是等脏页满了才开始,而是有多个触发条件:
- 定期触发:page cleaner 线程周期性扫描 Flush List,按比例刷一部分脏页。
- 脏页比例触发:
- 脏页比例 >
innodb_max_dirty_pages_pct_lwm(默认 10%)时,开始以温和速度刷盘。 - 脏页比例 >
innodb_max_dirty_pages_pct(默认 75%)时,强制加速刷盘。
- 脏页比例 >
- Free List 不足:当 Free List 中空闲页数量低于阈值时,LRU 列表尾部会淘汰干净页;如果淘汰的是脏页,会触发同步刷盘,造成用户线程阻塞。
- redo log 空间压力:当 redo log 可用空间不足时,也会触发刷盘,因为只有脏页刷盘后,redo log 才能被覆盖。
4.3 关键参数详解
| 参数 | 默认值 | 说明 |
|---|---|---|
innodb_buffer_pool_size |
128MB | 缓冲池大小,缓存数据页和索引页的内存总量 |
innodb_max_dirty_pages_pct |
75 | 脏页比例上限,达到后强制刷盘 |
innodb_max_dirty_pages_pct_lwm |
10 | 脏页比例低水位,超过后开始温和刷盘 |
innodb_io_capacity |
200 | 后台 IO 能力上限,控制刷盘和预读的 IOPS |
innodb_io_capacity_max |
2000 | 紧急情况下 IO 能力上限 |
innodb_flush_neighbors |
0 | 是否刷相邻页;SSD 建议设为 0 |
innodb_lru_scan_depth |
1024 | page cleaner 扫描 LRU 列表的深度 |
innodb_page_cleaners |
4 | page cleaner 线程数(8.0 通常无需调整) |
特别注意:
innodb_io_capacity是限制后台刷盘速度的核心参数。默认 200 是针对传统机械硬盘的。如果你用的是 SSD,这个值严重偏小,会导致脏页堆积。innodb_flush_neighbors在机械硬盘上设为 1 可以减少寻道,但在 SSD 上应设为 0,避免不必要的额外写入。innodb_lru_scan_depth影响 page cleaner 每次扫描多少 LRU 页,过小会导致 Free List 补充不及时。
5. 参数调优实战:从抖动到平稳
5.1 环境与现象
假设生产环境:
- 硬件:8 核 CPU,16GB 内存,SSD(实测 IOPS 约 5000)
- MySQL 8.0.36,数据量约 20GB
- 晚高峰业务写入较多,出现 RT 突刺,磁盘
%util接近 100%
首先采集基线数据:
1 | -- 当前关键参数 |
可能的输出:
1 | +---------------------------------+------------+ |
再用 performance_schema 查看脏页情况:
1 | SELECT |
输出:
1 | +-----------+--------------+----------------+ |
脏页比例 82.5%,Free buffers 只有 120 个,明显不正常。
5.2 调优思路
- **增大
innodb_io_capacity**:SSD 实测 IOPS 约 5000,将innodb_io_capacity调整为 3000(留余量),innodb_io_capacity_max调整为 5000。 - 降低脏页水位:将
innodb_max_dirty_pages_pct_lwm调到 30,让刷盘更早启动;innodb_max_dirty_pages_pct调到 70,避免冲顶。 - 关闭邻页刷盘:SSD 下设置
innodb_flush_neighbors = 0。 - 增大 LRU 扫描深度:
innodb_lru_scan_depth从 1024 调到 2048,提高空闲页补充速度。 - 考虑增大缓冲池:如果内存允许,可将
innodb_buffer_pool_size从 8GB 增加到 12GB(16GB 内存,留 4GB 给系统和连接)。
5.3 在线调整参数
以下是可以在线执行的调参命令(MySQL 8.0 支持动态修改):
1 | -- 调整 IO 能力 |
执行后,需要验证是否生效:
1 | SHOW VARIABLES WHERE Variable_name IN ( |
验证环境:MySQL 8.0.36,Linux x86_64,线上从库(低峰操作)。
5.4 观察效果
调整后,持续观察一段时间(例如 10 分钟),再次查询脏页和磁盘状态:
1 | SELECT |
输出:
1 | +-----------+--------------+ |
脏页比例从 82.5% 降到 42.1%,Free buffers 回到 4000+。同时,晚高峰的磁盘 %util 从 100% 降到 60% 左右,RT 抖动消失。
注意:如果调整后 innodb_io_capacity 设置得过高,可能导致磁盘 IO 长时间打满,影响其他服务。因此调参前建议先用 iostat -dx 1 观察实际 IOPS,并逐步上调。
