Buffer Pool
什么是Buffer Pool
如果数据库直接对磁盘进行随机读写,速度和效率都会很低。因此,引入内存读写缓冲池,也叫Buffer Pool
,用来提高MySQL的处理速度和效率。
执行CRUD
时,实际上主要是针对Buffer Pool
中的数据进行的,然后再由后台随机I/O
线程将Buffer Pool
中的数据写入磁盘。

Buffer Pool的结构
Buffer Pool
默认大小128M,由innodb_buffer_pool_size
参数进行调节。
MySQL抽象出一个数据页的概念,磁盘中包含很多数据页,每个数据页又放了很多行数据,更新一行数据,数据库会找到这行数据所在的数据页,然后加载到Buffer Pool
。
也就是说,Buffer Pool
存放的是一个个的数据页。

数据页又叫缓存页,默认情况下,数据页和缓存页是一一对应的,大小16KB。
每个缓存页会有一个描述信息,是缓存页的元数据
。元数据的大小 ≈ 缓存页大小 × 5%
。
因此,Buffer Pool实际大小 = 128M + 若干元数据
。

问题
Buffer Pool
会残存内存碎片吗?如果有,该如何尽可能减少内存碎片?
free链表
free
链表是一个双向链表,每一个节点就是一个空闲缓存页的描述数据块的地址。链表本身就是由描述数据块组成,并没有单独创建一个链表,每个描述数据块都有两个指针:free_pre
和free_next
。
数据库启动时,所有的缓存页都是空闲的,因此所有缓存页的描述数据块,都是free
链表的节点。
free
链表的有一个特殊的节点,叫做基础节点,它不属于Buffer Pool
,基础节点会引用free
链表的头节点和尾节点,以及链表中有多少个节点(即多少个空闲的缓存页)。

将数据页读取成缓存页的过程如下。
首先从
free
链表里得到一个描述数据,就可以得到对应的缓存页。再把磁盘上的数据页读取到对应缓存页中。
最后把描述数据块从
free
链表中删除。
数据库还会有一个哈希表数据结构。
key
:表空间 + 数据页编码。value
:缓存页地址。
如果key
对应的值为null
,则说明该数据页还未被缓存,否则就直接读取。
问题
表 + 行
与表空间 + 数据页
这两个概念的区别是什么?它们之间是否存在联系?
flush链表
当缓存页和数据页的内容不一致时,就产生了脏数据,也叫脏页。
flush
链表同free
链表的结构一模一样,只不过flush
链表的节点是由被修改过的描述数据块组成的。
凡是被修改过的缓存页,都会把其描述数据块加入到flush
链表,因此flush链表中的缓存页都是脏页,后续都是要写回到磁盘中去的。

基于LRU的缓存页淘汰:
- 把脏数据(脏页)写回到磁盘中,然后这个缓存页就清空了;
- 再把磁盘上需要的新的数据页加载到这个空闲出来的缓存页中。
LRU链表
对于高频查询和修改的某个缓存页,可以说它的“命中率”很高。而LRU(Least Recently Used)链表记录哪些缓存页命中率最低。
只要加载数据,就把缓存页的描述数据块放到LRU的头部去。
如果缓存页对应的描述数据块在LRU尾部,那么只要查询或者修改了这个缓存页的数据,则其对应的描述数据块就会被挪动到LRU
的头部去。
因此需要被被淘汰的缓存页,都在LRU
的尾部,只需要把尾部缓存页数据写入磁盘,再把需要的新数据加载到缓存页中即可,然后再把尾部缓存页移动到LRU
头部,就完成了一次缓存页淘汰。

当MySQL从磁盘上加载数据时,其预读机制会把该数据页相邻的其它数据页也一并加载进来。这些被“捎带”加载进来的数据页,即使没有被访问,也可能不会被移到LRU
的尾部去,会造成巨大的空间浪费。
触发MySQL预读机制的条件为如下。
innodb_read_ahead_threshold
,默认65,如果顺序访问同一个区里数据页的数量超过这个阈值,就把下一个相邻区的所有数据页都加载到缓存(区间机制)。innodb_random_read_ahead
,默认关闭,如果Buffer Pool
里缓存了同一个区的13个连续数据页,且命中率都较高,就把该区其它数据页都加载到缓存(区内机制)。
当执行SELECT * FROM TABLE_NAME
,会把表中所有的数据页,全部从磁盘加载到Buffer Pool
缓存页,造成大量的空间浪费。同时,这种全表扫描也会造成LRU问题。
问题
为什么MySQL有预读机制?
为什么要加载相邻数据页?有什么意义?
在什么场景下这样做很有用?
冷热分离
由于预读机制,导致一些无用的缓存页也混在LRU链表里。因此MySQL采取了冷热分离的设计思想。
一部分是热数据
LRU
。一部分是冷数据
LRU
。冷热
比例由innodb_old_blocks_pct
控制,默认冷数据占37%
。

冷热分离的运行机制如下。
第一次加载数据的缓存页,会被放在冷数据区域的链表头部。
经过
innodb_old_blocks_time
(默认值1000ms)所指定的时间之后,再访问缓存页时,才会将这个缓存页移动到热数据区域的头部。

预读机制及全表扫描加载进来的一大堆缓存页,都放在冷数据区域的头部,不会对热数据区域造成影响。加载到冷数据区域的缓存页,1s之后如果被访问了,就移动到热数据区域的头部,如果缓存页不够,需要淘汰一些缓存,就直接找到冷数据区域的尾部,写入磁盘,并加载新数据。
MySQL对LRU
热数据区域的优化机制是:只有当缓存页位于热数据区域后部3/4时,才会被移动到链表头部。设计缓存机制时,可以借鉴冷热数据分离的思想,如Redis。
缓存页
Buffer Pool
中有若干缓存页,每个缓存页都有对应的描述文件,在MySQL加载数据前,由free链表统一管理这些缓存页。当从磁盘加载数据到内存时,
free
链表会移除被使用的缓存页,同时LRU
链表的冷数据区域会放入这个缓存页。如果这个缓存页被修改,那么
flush
链表会记录这个脏页,如果修改时间在缓存页加载完毕1s之后,那么这个缓存页还会被LRU
链表从冷数据区域移动到热数据区域的头部。如果这个缓存页本身就位于热数据区域,并且如果是位于链表位置的3/4部位,那么也会被移动到热数据区域的头部。
如果缓存页空间不足,那么会直接将
LRU
冷数据区域尾部的缓存页数据写入磁盘,然后腾出空间来存放新数据。
除了空间不足,其他将缓存页写入磁盘的时机如下。
后台线程定时将LRU冷数据区域尾部的一些缓存页数据刷入磁盘,并把它们加回
free
链表,也一并从flush
链表和LRU
链表移除。后台线程会不定时把
flush
链表中的一些缓存页刷入磁盘,并把它们加回free
链表,也一并从flush
链表和LRU
链表移除。

问题
- 该优化哪些MySQL内核参数,才能尽可能避免缓存页经常出现空间不足的情况?
推荐做法
Buffer Pool的问题
多线程并发访问Buffer Pool
时,必然要进行加锁,非常影响性能。

如果给Buffer Pool
分配的内存空间 < 1GB,那么最多只有一个Buffer Pool
。因此,可以通过适当调整MySQL内核参数来提升Buffer Pool
的处理效率。
innodb_buffer_pool_size = 8589934592
innodb_buffer_pool_instance = 4
给Buffer Pool
分配了8GB内存空间,设置了4个实例,每个Buffer Pool
= 2GB。
因此在实际生产环境中,Buffer Pool
的大小、数量、机器配置,都会对系统性能造成直接的影响,这需要通过不断压测来达到一个动态的平衡。

chunk机制
如果Buffer Pool
初始化的大小被固定死,是无法动态调整的。MySQL为此设计了chunk
机制:由很多chunk
来拼成Buffer Pool
。
通过
innodb_buffer_pool_chunk_size
控制每个chunk
大小,默认值128MB。每个
chunk
里都包含缓存页、描述数据块,但共享free
链表、flush
链表、LRU
链表等数据结构。

Buffer Pool大小设置
Buffer Pool
大小一般为机器物理内存的50%~60%。Buffer Pool
总大小 = (chunk大小 × Buffer Pool数量) × 整数倍,可以据此来确定每个chunk的大小及Buffer Pool数量。内存32G,chunk=128M,Buffer Pool=20G,Buffer Pool数量=10 20倍 √
内存32G,chunk=128M,Buffer Pool=20G,Buffer Pool数量=32 5倍 √
内存32G,chunk=640M,Buffer Pool=20G,Buffer Pool数量=8 4倍 √
内存32G,chunk=128M,Buffer Pool=20G,Buffer Pool数量=256 小数倍 ×
查看innodb引擎状态
数据库启动后,可以通过show engine innodb status
查看innodb
引擎中关于Buffer Pool
的状态数据。
> show engine innodb status;
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 8585216
Dictionary memory allocated 314658
Buffer pool size 512
Free buffers 254
Database pages 256
Old database pages 0
Modified db pages 0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 427, created 58, written 91
1.29 reads/s, 0.00 creates/s, 0.94 writes/s
Buffer pool hit rate 978 / 1000, young-making rate 0 / 1000 not 0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 256, unzip_LRU len: 0
I/O sum[96]:cur[3], unzip sum[0]:cur[0]
参数 | 含义 |
---|---|
Total large memory allocated | Buffer Pool总大小 |
Buffer pool size | Buffer Pool一功能容纳多少个缓存页 |
Free buffers | free链表中共有多少可用空闲缓存页 |
Database pages和Old database pages | LRU链表中共有多少缓存页,及冷数据区缓存页的数量 |
Modified db pages | flush链表中缓存页数量 |
Pending reads和Pending writes | 等待从磁盘加载进缓存页的数量,以及即将从LRU链表和flush链表中刷入磁盘的数量 |
Pages made young和not young | LRU冷数据区域转移到热数据区域的缓存页的数量,以及在LRU冷数据区1s内被访问且没有进热数据区域的缓存页的数量 |
Pending reads和Pending writes | 每秒从冷数据区域进入热数据区域的缓存页的数量,以及每秒在冷数据区域里被访问了但不能进入热数据区域的缓存页的数量 |
Pages read、created、written、 | |
reads/s、creates/s和writes/s | 已经读取、创建和写入的缓存页数量,以及每秒读取、创建和写入的缓存页数量 |
Buffer pool hit rate xxx / 1000 | 每1000次访问,有多少次是直接命中buffer pool里的缓存页的 |
young-making rate xxx / 1000和not | 每1000次访问,有多少次让缓存页从冷数据区域移到了热数据区域,以及没移动的缓存页数量 |
LRU len | LRU链表里缓存页的数量 |
I/O sum | 最近50s读取磁盘数据页的总数 |
问题
- 针对手头的MySQL执行状态查询命令,分析数据库
buffer pool
的使用情况。
感谢支持
更多内容,请移步《超级个体》。