Buffer Pool

Buffer Pool

由于CPU和硬盘的速度存在很大差异,类似如CPU Cache、Linux Page Buffer, 都是为了缓和速度差异,有了Buffer Pool 的概念。

有了缓冲池后:

  • 当读取数据时,如果数据存在于 Buffer Pool 中,就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。
  • 当修改数据时,首先是修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页,最后由后台线程将脏页写入到磁盘。

Buffer Pool 有多大?

Buffer Pool 是在 MySQL 启动的时候,向操作系统申请的一片连续的内存空间,默认配置下 Buffer Pool 只有 128MB

可以通过调整 innodb_buffer_pool_size 参数来设置 Buffer Pool 的大小,一般建议设置成可用物理内存的 60%~80%。

Buffer Pool 缓存什么?

InnoDB存储数据的单位是页, 默认16KB, 因此Buffer Pool也按页来划分.

img

当我们查询一条记录时,InnoDB 是会把整个页的数据加载到 Buffer Pool 中,因为,通过索引只能定位到磁盘中的页,而不能定位到页中的一条记录。将页加载到 Buffer Pool 后,再通过页里的页目录去定位到某条具体的记录。

管理 Buffer Pool

Buffer Pool是启动时向操作系统申请一块连续的虚拟地址空间,按照页面进行管理,首先将其划分为空闲页,加入Free List进行管理。

管理空闲页面:Free List

为了能够快速找到空闲的缓存页,可以使用链表结构,将空闲缓存页的「控制块」作为链表的节点,这个链表称为 Free 链表(空闲链表)。

Free 链表上除了有控制块,还有一个头节点,该头节点包含链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。

Free 链表节点是一个一个的控制块,而每个控制块包含着对应缓存页的地址,所以相当于 Free 链表节点都对应一个空闲的缓存页。

有了 Free 链表后,每当需要从磁盘中加载一个页到 Buffer Pool 中时,就从 Free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上,然后把该缓存页对应的控制块从 Free 链表中移除。

管理脏页:Flush List

设计 Buffer Pool 除了能提高读性能,还能提高写性能,也就是更新数据的时候,会操作LRU链表中的页面,不需要每次都要写入磁盘,而是将 LRU中对应的缓存页标记为脏页,然后再由后台线程将脏页写入到磁盘。

那为了能快速知道哪些缓存页是脏的,于是就设计出 Flush 链表,它跟 Free 链表类似的,链表的节点也是控制块,区别在于 Flush 链表的元素都是脏页。

有了 Flush 链表后,后台线程就可以遍历 Flush 链表,将脏页写入到磁盘。

实际上,Flush链表跟redo日志关联很大,redo文章中再提。

管理普通页:LRU List

如何提高缓冲命中率, 最容易想到的是 LRU(Least recently used)算法.

但会出现一些问题

  • 预读失败
  • Buffer Pool污染

针对以上两个问题,需要对普通的LRU进行一定的改进。

预读失败

MySQL在加载数据页的时候, 由于程序的空间局部性的存在, 会将被访问数据的周围数据也加载进来, 以此减少磁盘I/O, 但是有可能这些数据没有被访问到, 这就是预读失败.

MySQL有两种预读策略,都是异步读取

  • 线性预读:如果读取该区的页面数量超过阈值,则预读下一个区的所有页面。
  • 随机预读:如果在一个区读取一定量的热点页面,则读取该页面所在的整个区的所有页面。

预读成功了能加快速度,但失败就会冲刷热点页面。

所以MySQL改进LRU算法, 将LRU划分成两个区域, old区域和young区域

young在前, old在后优先被淘汰.

预读页加入到old区域的头部, 不影响young中高频使用的数据页,访问到了才加到young区域.

这样,预读失败也不会影响主要的热点页面。

Buffer Pool污染

短时间内访问了大量数据页, 例如全表扫描,导致Buffer Pool被这些仅访问一次的数据冲刷.

MySQL是这样处理的, 进入到yound区域的条件增加了一个停留在old区域的时间判断.

具体是这样做的,在对某个处在 old 区域的缓存页进行第一次访问时,就在它对应的控制块中记录下来这个访问时间:

  • 如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该缓存页就不会被从 old 区域移动到 young 区域的头部
  • 如果后续的访问时间与第一次访问的时间不在某个时间间隔内,那么该缓存页移动到 young 区域的头部

这个间隔时间是由 innodb_old_blocks_time 控制的,默认是 1000 ms。

也就说,只有同时满足「被访问」与「在 old 区域停留时间超过 1 秒」两个条件,才会被插入到 young 区域头部,这样就解决了 Buffer Pool 污染的问题 。

全表扫描两次访问同一页面一般不会超过1s,因此也不会影响young区域。

其他优化

  1. 针对 young 区域,为了防止 young 区域节点频繁移动到头部。young 区域前面 1/4 被访问不会移动到链表头部,只有后面的 3/4被访问了才会。

其他链表

除了上面提到的Free、Flush、LRU链表外,还有一些其他页面的链表。如

  • unzip LRU:管理解压页面
  • zip clean : 管理压缩页
  • zip free:每个元素都是一个链表

等等其他用于更好的管理Buffer Pool的数据结构。

脏页什么时候刷入磁盘?

InnoDB的更新操作采用的是Write Ahead Log即先写日志, 再写入磁盘, 通过redo log让MySQL拥有了奔溃恢复能力

下面几种情况会触发脏页刷新

  • 后台线程扫描LRU链表定时刷盘:BUF_FLUSH_LRU
  • 后台线程刷新Flush一部分页面:BUF_FLUSH_LIST
  • 用户线程需要读取一个磁盘页时,空间不够,由用户线程同步刷盘:BUF_FLUSH_SINGLE_PAGE
  • redo log日志满了的情况
  • Buffer Pool空间不足, 会淘汰一部分数据页, 如果是脏页就同步到磁盘
  • MySQL认为空闲时
  • 正常关闭时.

在我们开启了慢 SQL 监控后,如果你发现「偶尔」会出现一些用时稍长的 SQL,这可能是因为脏页在刷新到磁盘时可能会给数据库带来性能开销,导致数据库操作抖动。

如果间断出现这种现象,就需要调大 Buffer Pool 空间或 redo log 日志的大小。

优化Buffer Pool

上述我们提到Buffer Pool是连续大内存块,增大需要完整复制,很耗时。为了动态调整该大小。后面Innodb的Buffer Pool由chunk组成,每个chunk都是连续的内存空间,可以动态增删chunk。

此外,后续的版本更新还支持多个Buffer Pool Instance,更具扩展性。


Buffer Pool
https://messenger1th.github.io/2024/07/24/MySQL/Buffer Pool/
作者
Epoch
发布于
2024年7月24日
许可协议