Buffer Pool详解

发布于2024-06-04
字数: 3176
MySQL
Buffer Pool

Buffer Pool详解

Buffer PoolInnodb存储引擎体系结构中非常重要的一部分,常见的CRUD都需要使用到Buffer Pool,从下面的Innodb体系结构图也可以看出Buffer Pool的重要性。今天就来深入的了解一下Buffer Pool。

Buffer Pool的作用

大家都知道MySQL的数据存储在磁盘中,但是磁盘的读写性能是比较差的,为了提高性能,MySQL设计了Buffer Pool来充当数据的缓冲池,当从磁盘中读取出数据时会将其缓存在内存中,修改数据时也在缓冲池中修改,之后在后台异步刷入磁盘。

Buffer Pool有多大

MySQL8中默认是128M,通过show variables like 'innodb_buffer_pool_size';查看,在配置文件中通过修改innodb_buffer_pool_size来控制缓冲池的大小。MySQL官方文档的描述中,在专用服务器上,通常会将高达80%的物理内存分配给缓冲池。

Buffer Pool的组成

Buffer Pool的组成有自适应哈希索引数据页undo页Change Buffer锁信息等。

下图是Buffer Pool的物理结构,自上而下分为instance、chunk、page三层。

  • Buffer Pool Instance

    Buffer pool instance对应的结构体是buf_pool_t。整个Buffer Pool由多个instance组成。instances是为并发读取和写入设计的,各个instances之间没有锁竞争关系。instance包括锁、信号量、chunks、逻辑链表等。

  • Buffer pool chunk

    Buffer pool chunk对应的结构体是buf_chunk_t。每个buffer pool instance被均匀划分成多个chunk,数量由 innodb_buffer_pool_size / innodb_buffer_pool_chunk_size确定,MySQL官方文档中建议数量不要超过1000个。chunk分为数据页和数据页对应的控制体,控制体中有指针指向数据页。

Buffer Pool缓存了哪些数据

MySQL在磁盘中是按页为单位进行数据存储的,默认的页大小是16kb,当我们查询一条数据时,会把这条数据所属的这一页数据都加载进来;在MySQL启动时,Innodb会申请一片连续内存空间,并且按页划分,每页大小是16KB。为了更方便的管理这些缓存页,MySQL为每个缓存页都创建了一个控制块来管理。

控制块对应的结构体是buf_block_t,包括了页号 缓存页地址 链表等信息。控制块和缓存页的对应关系如下图所示。

Buffer Pool的管理

随着MySQL的运行,Buffer Pool中的内存空间会有已经使用的页,也有从未使用过的页,为了更加方便的管理这些页,MySQL针对不同类型的页做了不同的设计。

  • 空闲页:未使用的缓存页
  • 干净页:已使用但是数据没有发生过修改的缓存页
  • 脏页:已使用且数据发生改变的缓存页

空闲页的管理

为了快速找到空闲页,Innodb设计了链表的结构来管理空闲页,将缓存页的控制块作为链表的节点,这个链表就称之为Free链表(空闲链表)。

Free链表除了控制块节点,还有一个头节点,记录了空闲页数量、头节点和尾节点的地址,另外,头节点是单独分配的一块内存空间,并不属于Buffer Pool的连续内存空间。当从磁盘加载一页数据后只需要从链表中取出一个空闲页填好控制块信息,之后从Free链表移除该节点即可。

脏页的管理

MySQL为了提高写性能,在每次数据发送改变后,不会在每次修改数据后立刻刷入磁盘,而是将这些缓存页标记为脏页。为了快速查找脏页,Innodb设计了Flush链表,结构和Free链表类似,只不过Flush链表的子节点是脏页。

缓存页的管理

在了解缓存页的管理之前,我们需要先思考两个问题:

  • 从Free链表移除的缓存页去哪里了
  • Buffer Pool的大小是有限制的,如果用完了,这个时候有新的数据页需要加入进来应该如何处理

这两个问题都需要使用LRU链表来处理,我们先来看看LRU链表是什么东西。

LRU链表是什么

Innodb中,数据的读写都是在Buffer Pool中实现的,但是因为Buffer Pool大小是有限的,所以对于热点数据肯定是希望能够一直驻留在缓存池中,对于访问较少的数据我们希望能够移除。

为此InnoDB采用了LRU算法(最近最少使用),将频繁访问的数据放在头部,访问较少的数据放在尾部,空间不够时从尾部开始淘汰。LRU的结构和前面两个链表结构基本一样。

LRU的优化

下图表示BufferPool中的页面使用情况。每一个小方块都可以看作一页。空闲表示空闲页面,即页面对应的数据为空,可以随时填写新数据。Clean表示一个干净的页面,即该页面包含数据,而数据尚未更新。Dirty表示脏页,即该页包含已更新的数据。不同类型的页面共存于Buffer Pool中,并通过每个链表链接在一起进行协同管理。

Innodb并没有采取简单的LRU算法,因为简单的LRU算法无法解决两个问题:

  • 预读失效
  • Buffer Pool污染

什么是预读,预读失效是什么

MySQL遵循局部性原理,即认为靠近被访问到的数据的数据在未来有很大概率也会被访问,所以在加载访问数据时,也会将把相邻的数据页加载进来。但是预读加载的数据页有可能永远不会被访问,这个时候预读就失效了,会导致预读页一直驻留在Buffer Pool中,降低了缓冲池的利用效率。

什么是Buffer Pool污染

当某一个 SQL 语句扫描了大量的数据时,因为Buffer Pool可能无法容纳所有数据,这时候会将 Buffer Pool 里的所有页都替换出去,导致大量热数据被淘汰了,等这些热数据又被再次访问的时候,由于缓存未命中,就会产生大量的磁盘 IO,MySQL 性能就会急剧下降,这个过程被称为 Buffer Pool 污染

这里需要注意的点是扫描了大量数据,并不是查询的结果集数据量很大。比如select * from user where name like '%cxk%',这条SQL会导致全表扫描,即使最后查询出来的结果只有一两条,还是会导致Buffer Pool污染。

既然知道了问题的所在,那我们接下来看看InnoDB是如何解决这两个问题。

预读失效的解决

前面提到预读失效可能会导致预读页一直留在头部,那么我们只需要让预读页只有在真正被访问的时候才移动到头部,为此InnoDBLRU链表分成youngold区域。young区域在链表头部,old在尾部,预读的页放在链表的old区域,真正被访问的才会被放在young区域。两个区域的比例通过innodb_old_blocks_pct参数来控制,默认old区域占比是37,以下面的例子为例:

Buffer Pool污染的解决

将链表分成两部分解决了预读失效的问题,对于Buffer Pool污染的问题其实也很简单,只需要提高进入young区域的门槛即可。具体的规则是这样的:

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

这个阈值由innodb_old_blocks_time确定的,通过show variables like 'innodb_old_blocks_time';可查看,默认是1000ms,也就是说需要驻留超过1s并且被真正访问才会进入young区域,

脏页的刷盘时机

前面提到修改数据时只是在Buffer Pool中修改,此时磁盘中的数据还是旧的,MySQL会在以下这些情况将脏页刷入磁盘。

  • 当 redo log 日志满了的情况下,会主动触发脏页刷新到磁盘
  • Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘
  • MySQL 认为空闲时,后台线程会定期将适量的脏页刷入到磁盘
  • MySQL 正常关闭之前,会把所有的脏页刷入到磁盘

至于还未刷盘MySQL就宕机导致的数据安全问题,MySQL使用了WAL策略来解决,MySQL会先写日志,再写数据,这样宕机后可以通过redo log做数据恢复。

Change Buffer

在文章开头的Innodb体系结构图中可以看到还有一块区域是change buffer,下面我们就来学习一下change buffer。

什么是Change Buffer

这里引申一下MySQL官方文档中对于Change Buffer的定义。Change Buffer是一种特殊的数据结构,用于缓存对二级索引页的更改。

Change Buffer的工作流程

当需要更新一个数据页时,如果数据页在Buffer Pool中则直接更新,如果数据页不在内存中,在不影响数据的一致性的前提下,InnoDB会将数据更改记录在Change Buffer中,这样就不需要从磁盘加载数据页了。之后如果对这个数据页访问时,将数据页读入内存,然后执行change buffer中与这个页有关的操作。

Change Buffer虽然是在内存中,但是change buffer是可以刷入磁盘的,MySQL将这个操作称之为merge,以下几种情况会触发merge:

  • 这个数据页被访问加载到Buffer Pool中
  • 后台线程定期merge
  • MySQL正常关闭时
  • Buffer Pool缓冲空间不足
  • Redo Log 写满时

为什么Change Buffer不能针对唯一索引

Change Buffer只适用于非唯一索引数据的变更修改情况,而不能为唯一索引或者主键数据的原因在于唯一索引需要做唯一性校验。因此在对唯一索引进行更新时必须将对应的数据页加载到Buffer Pool缓存中进行校验,所以自然用不到Change Buffer。也因此引出了一个结论:一般情况下update时非唯一索引性能相对来说会更好一点

Change Buffer的优点

更新数据时当辅助索引页不在缓冲池中时,对辅助索引更改进行缓冲可以避免性能开销比较大的随机访问I/O操作。数据的更改可以延后批量刷盘,因为后面可能会有其他读取操作会将受影响的页面读取到缓冲池中。

总结

InnoDB设计了Buffer Pool作为缓冲池,以此来提高读写性能,通过LRU区域分成old和young区域以及停留时间晋升到young头部这两个优化点解决了预读失效和Buffer Pool污染的问题;此外还设计了Change Buffer来提高非唯一索引页更改的性能。


参考资料:

MySQL :: MySQL 8.0 Reference Manual :: A.16 MySQL 8.0 FAQ: InnoDB Change Buffer

MySQL :: MySQL 8.0 Reference Manual :: 17.5.2 Change Buffer

MySQL :: MySQL 8.0 Reference Manual :: 17.5.1 Buffer Pool

MySQL十六:36张图理解Buffer Pool_bufferpool 数据结构

MySQL写缓冲Change Buffer原理解读