InnoDB的Buffer Pool
May 7, 2023
什么是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版)》