
MySQL缓冲池英文叫 InnoDB Buffer Pool是我接手任何一个数据库实例时第一个要看的组件。很多人把 MySQL 装好、数据导进去、业务跑起来然后某天收到慢查询告警一脸茫然地问我明明加了索引为什么还是慢这时候我十有八九会先打开几个状态变量看看缓冲池的命中率。原因很简单SQL 写得再好索引建得再多最终访问的数据页还是要落到磁盘上而缓冲池就是 InnoDB 在内存里给数据页和索引页准备的“高速缓存区”。这篇文章就把缓冲池讲透它是什么、怎么工作、参数怎么配、怎么监控、常见的坑有哪些。适合刚接触 MySQL 性能调优的运维和开发同学也适合准备面试时被问到 InnoDB 内存结构的人。先声明一下文中涉及的参数和 SQL 主要在 MySQL 5.7 和 8.0 上验证过8.4 LTS 的基本逻辑一致差别我会顺带提一句。1. 先搞明白缓冲池到底在“池”什么1.1 没有缓冲池InnoDB 会怎样InnoDB 存储引擎以“页”为单位管理数据默认一页 16KB。也就是说你查一条记录InnoDB 不是只把这一条记录从磁盘拿出来而是把所在的整个页都加载进来。如果没有缓冲池每一次读取都得发起一次真正的磁盘 IO不管这页刚被读过多少次下次还是要走磁盘。内存和磁盘的延迟差距有多大内存随机访问大概是几十到一百纳秒级别机械磁盘随机 IO 是毫秒级哪怕是现在很普及的 NVMe SSD单次 IO 延迟也在几十微秒级别。这中间差了不止一个数量级。你用索引定位一行数据B 树从上到下可能要读三到五个页如果每次都从磁盘读单条查询的延迟直接就是几毫秒起步高并发场景下这个延迟还会被放大。缓冲池的作用就是把这些经常访问的页长期留在内存里让逻辑读尽量不碰磁盘。生活里类比一下缓冲池就像你办公桌旁边的常用文件柜而不是公司仓库。你要天天用的文件放在手边随手就能拿到仓库里堆着的档案只有需要的时候才跑一趟去取。InnoDB 的缓冲池干的就是这件事并且它比人工更聪明的地方在于它会自动判断哪些页值得留下。没有缓冲池的 InnoDB 不是不能跑是完全跑不动。所以 MySQL 从设计之初就把这块内存作为核心结构。启动时 InnoDB 会向操作系统申请一块连续内存区域作为缓冲池按页切分维护页的副本从磁盘读入后会放在这里后续同样的页再被访问就直接命中内存。1.2 缓冲池里缓存的内容很多人以为缓冲池只缓存数据页其实它管的事情比这多。我在面试候选人的时候经常问一句“InnoDB 缓冲池里到底有什么”能答全的人不多。按我的理解至少包含下面几类内容作用备注数据页聚簇索引页和二级索引页也就是普通表数据的承载单元这是缓冲池的主体索引页B 树的非叶子节点和叶子节点都在页里索引页本质上也是数据页Undo 页事务回滚、MVCC 读取旧版本时使用大事务会产生大量 undo 页自适应哈希索引AHIInnoDB 根据热点查询自动为索引页构建的哈希索引不需要人工干预Change Buffer 相关页缓存对二级索引的 DML 变更占用缓冲池的一部分空间这里最容易混淆的是排序缓冲、连接缓冲这些会话级内存它们不算 InnoDB 缓冲池。sort_buffer_size、join_buffer_size 这些参数是每个会话独立分配的内存归 MySQL Server 层管理跟 InnoDB 缓冲池不是一个东西。调优的时候别混在一起算否则你会觉得内存怎么算都不对。2. 缓冲池的运行机制LRU、预读与 Change Buffer2.1 改进版 LRU防止全表扫描“冲垮”热点数据缓冲池的空间是有限的不可能无限缓存。总要有淘汰机制把不常用的页挤出去给新页腾位置。InnoDB 用的不是教科书里那种标准 LRU而是改进版的 LRU 链表。标准 LRU 有一个很典型的坑一次全表扫描就能把整个缓冲池里的热点数据全部冲掉。你想一下一张大表从头到尾扫一遍每个页刚被读进来就变成了“最近使用”后面业务要查的热点页反而被挤到队尾很快被淘汰。等到热点查询再来的时候又要从磁盘重新读性能肉眼可见地崩塌。InnoDB 的设计是把 LRU 链表分成两段前 5/8 是 young 区域也就是真正的热数据区后 3/8 是 old 区域相当于一个“观察区”。新读入的页不是直接放到链表头部而是先放到 old 区域的头部。如果这个页在 old 区域里存活了一段时间又被访问到才有资格晋升到 young 区域头部。如果它就是被扫描一次之后再也不用了那它会沿着 old 区域慢慢滑到尾部然后被淘汰根本影响不到 young 区域里的热点数据。这里有个关键参数innodb_old_blocks_time默认是 1000 毫秒。意思是说页在 old 区域停留超过这个时间后再次被访问才会被提升到 young 区域。为什么要设这个时间因为一次全表扫描中同一个页可能会在极短时间内被多次访问。如果没有时间门槛扫描照旧会把页提升到 young 区危害依然在。设置了时间之后快速连续访问就不算数只有隔了一段时间仍然被访问的页才被认为是真正有价值的。我实际遇到过一个案例某分析任务每天凌晨两点跑一次全表统计把整张 50GB 的表扫一遍。白天业务高峰期核心订单表查询命中率从 99% 跌到 80% 左右接口响应时间翻了三倍。后来我就是把innodb_old_blocks_time从默认的 1000ms 调到了 3000ms同时把分析任务挪到了凌晨四点的低峰期情况才慢慢恢复。记住LRU 的改进不仅仅是为了“缓存”更是为了隔离扫描流量和热点流量。innodb_old_blocks_pct控制 old 区域占比默认 37也就是大约 3/8。这个值一般不需要动除非你的业务模式非常特殊比如大量短时间扫描可以考虑调到 50 左右试试。2.2 预读提前把可能要用的页载入内存预读是 InnoDB 对顺序访问场景的优化手段。磁盘顺序读和随机读的性能差距非常大如果 InnoDB 判断你正在按顺序读某个区里的页它就会趁你还没读到后面的页时提前把整个区读进缓冲池。下次你访问到那里命中的是内存而不是磁盘。预读分成两种。一种叫线性预读由innodb_read_ahead_threshold控制默认 56。InnoDB 会观察一个区extent默认 1MB包含 64 个页里被顺序访问的页数如果超过 56 个就认为你在顺序扫这个区于是把剩余的页一次性读入。另一种叫随机预读曾经通过innodb_random_read_ahead控制但效果不稳定5.7 里默认就是关闭的8.0 里直接把参数移除了。这个知道就行不用管。预读不是免费的。预读进来的页如果没被用到就是浪费了一次磁盘 IO还占了缓冲池空间。怎么判断预读合不合理看两个状态变量SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_read_ahead%;关注Innodb_buffer_pool_read_ahead_evicted它表示预读进缓冲池但还没被访问就被淘汰的页数。这个值如果一直很高说明预读太激进读进来的页大多用不上。反过来如果预读数量很少又可能是你实际的查询模式偏向随机访问预读本来就不该起太大作用。2.3 Change Buffer二级索引的“延迟合并”Change Buffer 是一个容易被忽略但很重要的设计。它以前叫 Insert Buffer5.5 版本之后扩展成了 Change Buffer专门缓存对二级索引页的变更。为什么需要它聚簇索引按主键顺序排列插入操作通常是顺序写的IO 成本低没必要缓存。但二级索引就不一样了二级索引的键值在逻辑上是随机的你插入一行数据对应的二级索引页可能分散在磁盘各个位置。如果每次都立刻更新二级索引页大量随机 IO 会把系统拖垮。Change Buffer 的机制是先把这些变更记录到内存里等目标二级索引页将来被读到缓冲池时再把变更合并进去。这个策略很聪明相当于把随机 IO 变成了顺序 IO还延迟了写入压力。innodb_change_buffer_max_size控制 Change Buffer 最多占用缓冲池的比例默认 25%。注意它占用的空间是缓冲池的一部分不是额外的内存。如果你的负载很少更新二级索引或者二级索引很少这个值可以调小如果业务是大量随机的 insert、update、delete并且表上有多个二级索引那么 25% 是个比较合理的起点。合并发生的时机有三个目标页被读入缓冲池、后台线程定期合并、数据库关闭时强制合并。还有一种情况要注意如果缓冲池里长时间没读到那个二级索引页Change Buffer 里的记录会一直攒着占用空间这时候你就得当心磁盘上那些“不活跃”的索引页积压的变更过多。不过一般业务很难到这个程度了解机制就行。3. 生产环境如何配置缓冲池参数与容量规划3.1 innodb_buffer_pool_size 怎么定MySQL 安装完默认的innodb_buffer_pool_size是 128MB这个值对生产环境来说小的可怜。你会发现安装教程走完后MySQL 能跑但稍微有点数据量就开始慢。不夸张地说很多“装完 MySQL 之后总觉得不对劲”的情况改完缓冲池就有明显改善。我的经验公式是先看物理内存总量再看这台机器是不是专机专用。如果是专门的 MySQL 实例没有跑别的服务缓冲池可以占到物理内存的 60% 到 70%。比如 16G 内存的机器设 10G 是一个比较稳的起点。如果机器上还跑着 Redis、Nginx、Java 应用那就要砍到 40% 到 50% 甚至更低具体看其他服务的内存需求。另一个参考维度是热数据量。缓冲池至少要能放下你的活跃热数据。如果表总共 200G热数据每天被高频访问的大概 30G那么缓冲池设 32G 以上才有意义。命中率上不去的时候先别急着怀疑参数想想是不是热数据本来就已经大于缓冲池容量那再调也只是治标。这里有个实操技巧可以用 performance_schema 或者直接观察SHOW ENGINE INNODB STATUS里的Database pages数量乘以页大小估算当前实际缓存的数据量。配置方式很简单在 my.cnf 的[mysqld]段下加一行然后重启[mysqld] innodb_buffer_pool_size 10G或者写成字节数innodb_buffer_pool_size 10737418240。MySQL 也接受带单位的写法比如 10G5.7 和 8.0 都支持写起来直观很多。3.2 实例数、Chunk Size 与在线调整innodb_buffer_pool_instances这个参数决定了缓冲池被拆成几个独立的实例。每个实例有自己的 LRU 链表、Free 链表、Flush 链表独立加锁这样多线程并发访问时可以降低锁竞争。官方默认规则是缓冲池大小小于 1GB 时实例数为 1大于等于 1GB 时默认 8。上限是 64。实例数不是越大越好。拆得太细每个实例管理的内存变小额外的维护和锁检查开销反而变大。我一般建议缓冲池 4GB 以下设 4 个实例8GB 以上设 8 个就足够没必要盲目追高。如果你观察到SHOW ENGINE INNODB STATUS里buffer pool部分的等待时间很长或者运行SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_%发现某些实例的负载分布不均再考虑调整实例数。还有一个容易踩坑的参数是innodb_buffer_pool_chunk_size8.0 默认 128MB。在线调整缓冲池大小时新的 size 必须是 chunk_size 的倍数。比如 chunk 是 128MB你想从 10G 改成 11G这是可以的因为 11G 是 128MB 的整数倍但如果你改成 10.5G就会失败或者产生意想不到的结果。这个参数一般不建议动保持默认调大小时注意一下倍数关系就行。MySQL 5.7 和 8.0 都支持在线调整缓冲池大小不用重启SET GLOBAL innodb_buffer_pool_size 12884901888; -- 12G调整过程是异步的MySQL 会逐步把页从旧缓冲池搬到新缓冲池。你可以通过状态变量观察进度SHOW STATUS LIKE Innodb_buffer_pool_resize_status;千万别在业务高峰期频繁调整虽然官方说在线调整比较安全但我实测下来大跨度调整短时间内还是会有性能抖动尽量选低峰期做。3.3 与缓冲池容易搞混的内存参数既然聊到内存规划把容易和缓冲池混淆的几个参数一次说清楚。innodb_log_buffer_size是 redo log 的缓冲区默认 16MB8.0 下也是 16M。事务提交时重做日志会先写到这里再刷到磁盘上的 redo 文件。它和缓冲池是配合关系缓冲池里的页变成脏页后需要 redo log 保证崩溃恢复。日志缓冲区太小事务提交频繁时会造成额外的磁盘写入但通常 16M 到 64M 就够用不需要像缓冲池一样动辄几个 G。sort_buffer_size、join_buffer_size、tmp_table_size这些是会话级内存每个连接各自分配。注意不是配了 2M 就只会用 2M高并发下 1000 个连接每人 2M就是 2G 内存。很多人内存爆炸就是栽在这里跟缓冲池没有直接关系但排查内存问题时容易被误判。参数归属默认值主要用途innodb_buffer_pool_sizeInnoDB 全局128M缓存数据页和索引页innodb_log_buffer_sizeInnoDB 全局16M缓存 redo logsort_buffer_size会话级256K排序操作join_buffer_size会话级256K连接操作tmp_table_size会话级16M内存临时表我见过一个典型案例某同事接手一台 32G 内存的机器看到一堆连接慢就一口气把sort_buffer_size调到 64M结果没跑到一个小时服务器内存直接爆掉OOM killer 一顿乱杀。会话级内存的增长是乘数效应一定要小心。4. 怎么监控和判断缓冲池够不够用4.1 三个关键状态变量监控缓冲池我最先看三个状态变量SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_read_requests; SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_reads; SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_wait_free;Innodb_buffer_pool_read_requests是逻辑读请求次数也就是 InnoDB 一共向缓冲池发起了多少次读请求。Innodb_buffer_pool_reads是从磁盘真正读取的次数也就是缓存没命中、必须去磁盘取页的次数。命中率这样算命中率 1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests)这里是两个时间点的累计值要算一段时间内的命中率就得取两次快照做差值。一般业务达到 95% 以上算及格核心 OLTP 最好稳定在 99% 以上。我见过一个查询类的报表库命中率只有 60%页面查询延迟经常超过 500ms后来把缓冲池从 2G 加到 16G命中率拉到了 98%延迟直接降了一个数量级。Innodb_buffer_pool_wait_free表示线程等待空闲缓冲页的次数。这个值如果持续增长说明缓冲池里的空闲页不够InnoDB 要等后台线程把脏页刷盘之后才能腾出位置。这是内存紧张或者刷脏能力不足的信号光加缓冲池不一定能解决还要看刷脏线程配置和磁盘 IO 能力。4.2 SHOW ENGINE INNODB STATUS 怎么看分析 InnoDB 内存状态最传统也最实用的命令还是这个SHOW ENGINE INNODB STATUS\G输出里找到BUFFER POOL AND MEMORY这一段重点看几行。Buffer pool size是缓冲池总页数Free buffers是空闲页数Database pages是已缓存的数据页数Modified db pages是脏页数。一段经典的输出大概是Buffer pool size 8192 Free buffers 5966 Database pages 2226 Old database pages 0 Modified db pages 0 Pending reads 0 Buffer pool hit rate 1000 / 1000Free buffers长期很低甚至清零说明缓冲池已经被用满淘汰操作频繁发生。Modified db pages很高说明积压了好多没刷盘的脏页这时候如果数据库崩溃恢复时间会变长同时刷脏线程的压力也大。这里的Buffer pool hit rate直接给了一个千分比数值平时瞄一眼这个就能快速判断有没有大问题。还有两行容易被忽略Young making rate和Not young making rate。它们反映的是 LRU 晋升发生的频率。如果Young making rate很高说明大量冷数据正在被提升到 young 区通常意味着正在跑大批量扫描或者缓冲池容量确实不够用了。4.3 information_schema 查询从 MySQL 5.7 开始可以用 information_schema 里的表做更细粒度的监控SELECT POOL_ID, POOL_SIZE, FREE_BUFFERS, DATABASE_PAGES, OLD_DATABASE_PAGES, MODIFIED_DATABASE_PAGES, PAGES_MADE_YOUNG, PAGES_NOT_MADE_YOUNG FROM information_schema.INNODB_BUFFER_POOL_STATS;如果你设置了多个 buffer pool 实例这张表会按 POOL_ID 返回多行。PAGES_MADE_YOUNG和PAGES_NOT_MADE_YOUNG可以帮你判断 LRU 里冷热数据的活动情况相比看累计状态变量更直观。我习惯把这张表的数据定期采到监控系统里画成曲线观察缓冲池容量和业务增长之间的关系。需要提醒一下DATABASE_PAGES乘上页大小就是当前实际缓存的数据量。比如DATABASE_PAGES是 50000页大小 16KB那大约缓存了 800MB 数据。拿这个数和innodb_buffer_pool_size对比能判断你设置的空间是否被充分利用。5. 实战排障缓冲池相关的坑与速查表5.1 冷启动与预热数据库重启之后缓冲池是空的所有访问都得从磁盘读这是很多人反馈“重启之后业务慢了好久”的根本原因。MySQL 5.6 开始提供了innodb_buffer_pool_dump_at_shutdown和innodb_buffer_pool_load_at_startup两个参数专门解决冷启动问题。[mysqld] innodb_buffer_pool_dump_at_shutdown ON innodb_buffer_pool_load_at_startup ON innodb_buffer_pool_dump_pct 50dump_at_shutdown会在关闭时把当前缓冲池中的页号列表写入磁盘文件默认在数据目录下的 ib_buffer_pool。load_at_startup会在下次启动时读取这个文件按页号把数据重新加载回缓冲池。注意文件里存的只是页的位置信息不是页内容本身所以加载过程还是会有实际磁盘读取但比业务流量乱打发热点快得多。innodb_buffer_pool_dump_pct默认 25表示关闭时只 dump 最近最热的 25% 的页。如果你的业务热数据覆盖比较大可以适当调高到 50 甚至 80。加载是异步进行的启动后不会立刻全部加载完。想看进度SHOW STATUS LIKE Innodb_buffer_pool_load_status;冷启动还有一个更土但有效的办法挑一个业务低峰故意对几个核心大表做几次覆盖索引扫描人为把热点页拉进缓冲池。这个方法虽然粗暴但实测很有效适合没有提前开启 dump/load 参数的老实例。5.2 命中率正常但业务还是慢这是最容易被误导的场景。命中率 99%慢查询却一波接一波问题往往不在缓冲池容量而在 SQL 本身。命中率高只说明大部分页都在内存里但不代表每条 SQL 干的事都合理。一个典型情况是一条 SQL 扫描了几十万行即使这些页全部在缓冲池里CPU 把页解开、逐行过滤、再聚合一样要花不少时间。这时候该做的是看慢查询日志、看执行计划而不是盲目调大缓冲池。另一种常见情况是排序和临时表排序走的是sort_buffer_size如果大小不够就会落到磁盘临时表IO 压力剧增很多人误以为是缓冲池不够疯狂加大缓冲池结果磁盘压力一点没降。排查路径我建议这样先看SHOW PROCESSLIST里有没有长时间 running 的查询再看慢日志里这些查询的执行计划最后用EXPLAIN分析是不是走了全表扫描或者 filesort。只有确认是磁盘读导致的慢才回头调缓冲池。顺序反了你会白白浪费很多时间。5.3 启动失败、容器部署与 OOM缓冲池设得太大在高并发业务到来之前系统可能已经挂了。MySQL 启动时就要把innodb_buffer_pool_size对应的内存真正申请下来如果机器上其他进程占了一堆内存MySQL 启动就会直接报错Cant allocate memory表现成服务无法启动。很多人配置了过大的缓冲池重启之后发现 MySQL 起不来又不知道是什么原因其实只要把缓冲池调小就能正常启动。容器场景更容易踩这个坑。用 Docker 部署 MySQL 时容器内存限制是多少你的缓冲池就得在这个限制内预留余量。举个例子docker run -m 4g限制了容器最多用 4G 内存但 my.cnf 里写了innodb_buffer_pool_size 6G那么容器会在启动阶段直接 OOM日志里看到的是进程被杀根本轮不到 MySQL 报错。正确做法是让缓冲池大小加上系统其他开销不超过容器限制的 70% 到 80%。部署 MySQL 到容器前先docker inspect确认内存上限再反过来配缓冲池顺序别搞反。还有一个容易混淆的概念经常在 Windows 服务器上被问到任务管理器里“非分页缓冲池”占用很高跟 MySQL 有什么关系答案是基本没关系。Windows 的“分页缓冲池”和“非分页缓冲池”属于操作系统内核内存非分页缓冲池占用高通常和底层驱动、内核组件有关不是 InnoDB 的锅。MySQL 的 innodb_buffer_pool 是进程在用户态申请的内存占用的是提交内存private bytes不是系统非分页池。如果 Windows 上非分页池泄露优先查驱动和硬件问题通过调小 MySQL 缓冲池解决不了它只会白白牺牲性能判断方向。5.4 常见问题速查表现象可能原因处理思路缓冲池命中率低于 95%热数据总大小超过缓冲池容量或大量全表扫描加大缓冲池同时优化扫描类 SQLInnodb_buffer_pool_wait_free 持续增长缓冲池空闲页不足刷脏跟不上检查磁盘 IO 能力考虑增加 buff pool 或调刷脏参数数据库重启后业务变慢缓冲池冷启动开启 dump/load或低峰期手动预热修改缓冲池大小后启动报错内存不足或与 chunk_size 倍数不匹配调小容量确认 size 是 chunk 的整数倍容器内 MySQL 频繁重启容器内存限制小于缓冲池配置按容器内存重新计算参数Innodb_buffer_pool_read_ahead_evicted 很高预读太激进读入的页大量未使用调高 innodb_read_ahead_threshold命中率正常但查询依然慢SQL 本身扫描数据量大或排序落盘走 EXPLAIN 分析优化 SQL 和索引最后分享一个我自己的小习惯每次接手一个 MySQL 实例我会先做一次基线记录把innodb_buffer_pool_size、命中率、Innodb_buffer_pool_read_requests和Innodb_buffer_pool_reads这几项记下来隔一段时间再对比一次。数据库调优不是一次性的动作参数的调整要配合业务变化持续观察。尤其是缓冲池这种“加大就变快但内存是硬约束”的东西更要清楚地知道自己每一份内存花在了哪里。你花一晚上把缓冲池调得漂漂亮亮第二天业务一增长数字又变了这时候你手里有历史基线排查起来会从容很多。