什么是 Buffer Pool
Buffer Pool(缓冲池),是 InnoDB 存储引擎在 Mysql 启动时向操作系统申请的一片连续的内存,是为了缓存磁盘中的页。 相关配置项:
innodb_buffer_pool_size = 134217728
Buffer Pool 的组成
- 缓冲页 Buffer Pool 的内存被划分为若干个页,页面大小与 InnoDB 表空间使用的页面大小一致,默认是 16KB。我们称其为缓冲页。
- 控制块 为了更好地管理缓冲页,InnoDB 为每个缓冲页都创建了一些控制信息,含有缓冲页的表空间编号、页号、地址、链表节点信息等。我们称其为控制块。
- 碎片 控制块和缓冲页之间的空间。可能没有。
innodb_buffer_pool_size 并不包含控制块占用的内存空间大小,所以实际申请的内存空间会比 innodb_buffer_pool_size 大一些。
free 链表的管理
InnoDB 向操作系统申请内存空间后,把它划分成若干对控制块和缓冲页,并没有真实的磁盘页倍缓存到 Buffer 中,之后随着程序的原型,才会有 磁盘上的页被缓存到 Buffer Pool 中。
我们把所有空闲的缓冲页对应的控制块作为一个节点放到一个链表中,称其为 free 链表/空闲链表。
刚刚完成初始化时,所有的缓冲页都在 free 链表中。
基节点并不包含在为 Pool Pool 申请的内存空间中。
没当需要从磁盘中加载一个页时,就从 free 链表中取一个空闲的缓冲页,并且把它从 free 链表中移除。
缓冲页的哈希处理
怎样判断一个页是否在 Buffer Pool 中呢?InnoDB 使用了哈希表来管理缓冲页。我们使用表空间号和页号作为 key,把缓冲页的控制块作为 value,存储到哈希表中。
flush 链表的管理
flush 链表是专门用来管理脏页的链表。当一个页被修改后,就会被加入到 flush 链表中。
脏页:页中的数据和磁盘上的数据不一致。 一个缓冲页不可能同时在 free 链表和 flush 链表中。
LRU 链表的管理
Buffer Pool 对应的内存大小是有限的。当 free 链表中没有空闲的缓冲页时,需要从 Buffer Pool 中淘汰一些缓冲页,为新的页腾出空间。
LRU 链表是为了按照最近最少使用原则来淘汰缓冲页而创建的。当一个缓冲页被使用时,就会被加入到 LRU 链表的表头。当需要淘汰缓冲页时,就从 LRU 链表的表尾开始淘汰。
划分区域的 LRU 链表
有两种情况可能会降低 Buffer Pool 的命中率:
-
加载到 Buffer Pool 中的页不一定被用到。 预读:InnoDB 认为在执行当前请求时,可能会在后面读取某些页面,就 会把这些页面预先加载到 Buffer Pool 中。预读可以分为线性预读和随机预读。线性预读是指在访问某个区的页面超过系统变量的值,就会异步地预读下一个区的页面。随机预读是指某个区的 13 个连续的页面都被加载到了 Buffer Pool 中,都会异步读取本区中所有其他页面。
-
如果有非常多的使用频率偏低的页被同时加载到 Buffer Pool 中,可能会把那些使用频率高的页从 Buffer Pool 中淘汰掉。
如果某个语句需要访问的页面特别多,比如全表扫描,而 Buffer Pool 又不能全部容纳它们,就需要将其它页面淘汰掉,这样就会导致缓存命中率降低。
为了解决这个问题,InnoDB 把 LRU 链表划分为 young 区域和 old 区域。young 区域用来存储使用频率非常高的缓冲页,也成为热数据。old 区域用来存储使用频率低的缓冲页,也称为冷数据。
我们是按照比例将 LRU 链表划分为 young 区域和 old 区域的,比例由参数 innodb_old_blocks_pct 控制,默认值是 37。也就是说,young 区域占 LRU 链表的 37%,old 区域占 63%。
当磁盘上的某个页面在初次加载到 Buffer Pool 中的某个缓冲页时,该缓冲页对应的控制块会放到 old 区域的头部。当该缓冲页被访问时,就会被移动到 young 区域的头部。 但是这样解决不了全表扫描时的问题。全表扫描时,虽然首次加载到 Buffer Pool 中的页放到了 old 区域的头部,但是后续马上就会访问到,每次进行访问时又会把该页放到 young 区域的头部,这样仍然会把那些使用频率高的页面 排挤 下去。 所以,InnoDB 还引入了一个参数 innodb_old_blocks_time,用来控制 old 区域中的缓冲页在被访问后,要在多长时间内保留在 old 区域中。默认值是 1000,单位是毫秒。也就是说,old 区域中的缓冲页在首次被访问后,如果 1000 毫秒内再次被访问,不会被移动到 young 区域的头部。 如果把 innodb_old_blocks_time 设置为 0,就表示每次访问 old 区域中的缓冲页,都会把它移动到 young 区域的头部。
进一步优化的 LRU 链表
为了避免频繁地对 LRU 链表执行节点移动操作,只有在被访问的缓冲页位于 young 区域 1/4 的后面时,才会把它移动到 LRU 链表头部。
刷新脏页到磁盘
后台有专门的线程负责每隔一段时间就把脏页刷新到磁盘上。刷新方式有两种:
- 从 LRU 链表的冷数据中刷新一部分到磁盘上。 后台线程会定时从 LRU 链表尾部开始扫描一些页面。如果发现脏页,则把它们刷新到磁盘上。这种刷新方式称为:BUF_FLUSH_LRU。
- 从 flush 链表中刷新一部分到磁盘上。 后台线程会定时从 flush 链表中刷新一部分页面到磁盘,刷新的速率取决于当时系统是否繁忙。这种刷新方式称为:BUF_FLUSH_LIST。
多个 Buffer Pool 实例
在多线程环境下,访问 Buffer Pool 中的各种链表都需要加锁处理。在 Buffer Pool 大切并发访问量高的情况下,单一的 Buffer Pool 可能会 影响请求的处理速度。所以,InnoDB 引入了多个 Buffer Pool 实例,每个 Buffer Pool 实例都有自己的 LRU 链表、free 链表和 flush 链表。
我们可以再服务器启动的时候通过设置 innodb_buffer_pool_instances 参数来指定 Buffer Pool 实例的个数。 每个 Buffer Pool 实例占用的空间可以这样计算:innodb_buffer_pool_size / innodb_buffer_pool_instances。
当 innodb_buffer_pool_size 的值小于 1GB 时,设置多个实例是无效的,因为 InnoDB 会自动把 innodb_buffer_pool_instances 修改为 1。
查看 Buffer Pool 的状态信息
可以通过 show engine innodb status 来查看 Buffer Pool 的状态信息:
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 0
Dictionary memory allocated 789776
Buffer pool size 8191
Free buffers 7007
Database pages 1178
Old database pages 454
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 1035, created 143, written 191
2.29 reads/s, 0.00 creates/s, 0.00 writes/s
Buffer pool hit rate 833 / 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: 1178, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
其中部分字段的含义如下:
- Buffer pool size:Buffer Pool 的大小,单位是页。
- Free buffers: Buffer Pool 中空闲的缓冲页数量。
- Database pages: 代表 LRU 链表中页的数量。
- Old database pages: 代表 LRU 链表中 old 区域的页的数量。
- Modified db pages: 代表脏页数量。
- Pending reads: 等待从磁盘加载到 Buffer Pool 链表中节点的数量。
- Buffer pool hit rate:表示在过去某段时间内,平均访问 1000 次页面时,有多少次是在 Buffer Pool 中命中的。
此文原载于本人个人网站:链接。
参考书籍:
- 《MySQL 是怎样运行的-从根上理解 MySQL》
- 《MySQL 技术内幕:InnoDB 存储引擎(第 2 版)》
本文由mdnice多平台发布