Buffer Pool

在对数据库执行增删改查操作的时候,因为对磁盘的随机读写操作速度非常慢。所以通过Buffer Pool缓存磁盘的真实数据。

缓冲池是主内存中的一个区域,在 InnoDB 访问时缓存表和索引数据。缓冲
池允许直接从内存访问频繁使用的数据,从而加快处理速度。

官网地址:https://dev.mysql.com/doc/refman/8.0/en/innodb-buffer-pool.html

数据页

MYSQL中抽象出来的数据单位,MYSQL把很多行数据放在一个数据页里。实际上我们更新一行数据的时候,是通过数据库找到这行数据所在的数据页,然后加载到Buffer Pool中。
默认情况下,一个数据页是16KB

缓存页

因为在Buffer Pool中存放的也是一个一个的数据页,也叫作缓存页。在默认情况下,是和磁盘上的数据页一一对应的,所以也是16KB。

但是缓存页也会有额外的描述信息

缓存页的描述信息

用于描述缓存页的一些基本信息,比如数据页所属表空间、数据页的编号、在Buffer Pool中的地址等。
每个缓存页都有对应的一个描述信息,在Buffer Pool中,每个缓存页的描述信息在最前面,然后各个缓存页放在后面
描述数据大概相当于缓存页是5%。

free链表

概念引入

当读取数据页放入Buffer Pool的时候,怎么知道哪些缓存页是空闲的?

free链表是一个双向链表,每个节点是一个空闲的缓存页的描述块地址

刚开始数据库启动的时候,所有的缓存页都是空闲的,因为此时可能是一个空的数据库,一条数据都没有,所以此时所有缓存页的描述数据块,都会被放入这个free链表中。

空间占用

  • free链表只是一个逻辑上的概念,因为每个缓存页的描述数据块中维护了两个指针,free_prev和free_next,分别指向free链表的上一个节点和下一个节点,这样就串成了一个free链表。
  • free链表还有一个基础节点(但是不在链表中,链表头结点的prev=null,尾结点的next=null),40个字节,存放了free链表的头结点的地址、尾结点的地址以及free链表里当前还有多少个节点。

数据页读取到Buffer Pool的过程

  1. 从free链表里获取一个描述数据块,获取到对应的空闲缓存页

    ( 一开始数据库启动时, Buffer Pool 中的所有描述数据块都会存入free链表 )

  2. 把数据页读取到对应的缓存页,写入相关的描述信息到描述数据块中
  3. 从free链表中移除

flush链表

update users set name='lisi' where id=2

执行以上操作的时候, mysql 肯定会去更新Buffer Pool的缓存页中的数据,此时一旦更新了缓存页中的数据,那么缓存页里的数据和磁盘上的数据页里的数据,是不是就不一致了?

这个时候,我们就说缓存页是脏数据,脏页,如下图。

1704873822817.png

类似于free链表,通过缓存页描述数据块中的两个指针来将脏数据页串起来。组成一个双向链表,也有一个基础节点存放头结点尾结点的地址等。

MYSQL预读机制

官网地址:https://dev.mysql.com/doc/refman/8.0/en/innodb-performance-read_ahead.html

当从磁盘上加载一个数据页的时候,可能会连带把这个数据页相邻的其他数据页也加载到缓存中去。分为以下两种预读方式,暂时不做说明

  • 线性预读

    顺序访问了一个区里的多个数据页(默认56页),就会把下一个相邻区中的所有数据页加载到缓存中
  • 随机预读

    如果Buffer Pool中缓存了一个区的13个随机数据页,而且这些数据页是比较频繁被访问的,就会把这个区的其他数据页都加载到缓存中

LRU链表

简化版

  • 当free链表已经没有空闲页的时候,所有的缓存页都塞了数据库,此时就要淘汰掉一些缓存页。
  • 此时可以将一个脏数据页刷到磁盘,然后清空这个缓存页,就有了一个空闲的缓存页。但是选择哪一个脏数据页去清空,此时就要用到LRU链表。
  • 当把一个数据页加载到缓存页的时候,把对应的描述数据块放到LRU链表头部。后续查询了或者修改了某个缓存页,也会把这个缓存页挪动到LRU链表头部。
  • 但是MYSQL的预读机制可能会加载没人访问的数据页,如下图。

基于冷热分离的LRU链表

  • 将链表按照5:3的比例分割,63%的热数据,37%的冷数据。
  • 数据页第一次加载到缓存的时候,放入冷数据头部。在1s后(参数配置)访问这个缓存页,就会被加入热数据头部。
  • 在热数据区域的前1/4部分缓存页被访问后不会移动到链表头部,避免浪费性能
  • 有一个后台线程会定时把冷数据区域的尾部缓存页刷回磁盘,清空加入回free链表。
  • 热数据区域也会在MYSQL闲暇的时候刷回磁盘
  • 无空闲缓存页时从冷数据区域尾部找到一个缓存页刷回磁盘并清空成为空闲页。

Doublewrite 双写缓存区

Mysql page页会异步刷新到磁盘,但是page页的大小是16k,而常用的 Linux文件系统页(OS Page)的是4K,一个页是需要同步到磁盘的,中间如果出现操作系统,磁盘或者进程意外退出怎么办?所以Mysql提供了一个doubleWrite 机制。

何为双写,就是page页刷新到磁盘的时候,把这个page数据写到不同的地方去,当出现问题是,有备份来达到持久性跟数据的一致性.

所以,Doublewrite 机制会占用一部分内存和磁盘的空间,同时也会导致一定的性能损失,但这是为了保证数据的安全性和可靠性而进行的权衡。

为什么redolog无法代替double write buffer?

redolog的设计之初,是“账本的作用”,是一种操作日志,用于MySQL异常崩溃恢复使用,是InnoDB引擎特有的日志,本质上是物理日志,记录的是 “ 在某个数据页上做了什么修改” ,但如果数据页本身已经发生了损坏,redolog来恢复已经损坏的数据块是无效的,数据块的本身已经损坏,再次重做依然是一个坏块

所以此时需要一个数据块的副本来还原该损坏的数据块,再利用重做日志进行其他数据块的重做操作,这就是double write buffer的原因作用。

因此,double write buffer与redolog对于容灾场景,缺一不可。

Change Buffer

change-buffer FAQ
官网:https://dev.mysql.com/doc/refman/8.0/en/faqs-innodb-changebuffer.htm

更改缓冲区是一种特殊的数据结构, 当这些 page不在缓冲池中时,它会缓存对二级索引页面的更改。可能由操作 (DML)产生的缓冲更改 稍后会在其他读取操作将页面加载到缓冲池中时合并。

为啥是二级索引,因为二级索引的插入跟修改一般是无序的,所以IO开销更大,更需要提升性能。

由于changebuffer更改后,在查询数据的时候需要去进行合并。那么如果查询的场景比较大,那么合并的消耗比IO的消耗可能更大。所以一般不适合读比较多的场景。

0写缓冲区,仅适用于非唯一普通索引页,为什么?
如果在索引设置唯一性,在进行修改时,InnoDB 必须要做唯一性校验,因此必须查询磁盘,做一次IO操作。会直接将记录查询到BufferPool 缓存页中,然后在缓冲池修改不会在ChangeBuffer操作
1哪些类型的操作会修改二级索引并导致更改缓冲?
INSERT , UPDATE , 和 DELETE 操作可以修改二级索引。如果受影响的索引页不在缓冲池中,则更改可以缓冲在更改缓冲区中。
2InnoDB 更改缓冲区有什么好处?
当二级索引页不在缓冲池中时缓冲二级索引更改可避免立即从磁盘读取受影响的索引页所需的昂贵的随机访问 I/O操作。当其他读取操作将页面读入缓冲池时,可以稍后分批应用缓冲的更改。
3更改缓冲区是否支持其他类型的索引?
不可以,change buffer 只支持二级索引。不支持聚集索引、全文索引 和 空间索引。全文索引有自己的缓存机制。
4何时发生更改缓冲区合并?
当 page 被读入缓冲池时,在 page 可用之前,缓冲的更改会在读取完成后合并。更改缓冲区合并作为后台任务执行。该 innodb_io_capacity 参数设置后台任务执行的 I/O 活动的上限,InnoDB 例如合并更改缓冲区中的数据。在崩溃恢复期间执行更改缓冲区合并。当索引页被读入缓冲池时,更改缓冲区(在系统表空间中)将更改应用于二级索引的叶页。更改缓冲区是完全持久的,可以在系统崩溃时幸存下来。重新启动后,更改缓冲区合并操作将恢复为正常操作的一部分。可以使用 强制更改缓冲区的完全合并作为缓慢服务器关闭的一部分 --innodb-fast-shutdown=0
5何时刷新更改缓冲区?
更新的page由刷新占用缓冲池的其他page的相同刷新机制刷新。
6什么时候应该使用更改缓冲区?
更改缓冲区是一项功能,旨在在索引变大且不再适合缓冲InnoDB 池时减少对二级索引的随机 I/O。通常,当整个数据集不适合缓冲池时,当存在修改二级索引页面的大量DML 活动时,或者当有大量二级索引被 DML 活动定期更改时,应使用更改缓冲区。
7什么时候不应该使用更改缓冲区?
如果整个数据集都适合缓冲池 InnoDB ,如果您的二级索引相对较少,或者如果您使用的是固态存储,那么您可能会考虑禁用更改缓冲区,其中随机读取的速度与顺序读取的速度差不多。在进行配置更改之前,建议您使用具有代表性的工作负载运行测试,以确定禁用更改缓冲区是否有任何好处。

Buffer Pool

BufferPool 监视器信息

我们也可以通过监视器查看返回的bufferpool的信息,我们可以通过SHOW ENGINE INNODB STATUS 来查看bufferpool的信息。

mysql> SHOW ENGINE INNODB STATUS\G;

里面会包含bufferpool内存相关信息

BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 0
Dictionary memory allocated 697300
Buffer pool size 8192 -- 分配给缓冲池的页面总大小。
Free buffers 1024 -- 空闲的page树数量
Database pages 6908 -- 数据页大小 lru链表的大小
Old database pages 2530 -- LRU链表的 old 部分大小
Modified db pages 0 -- 修改的数据库页
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 1333711, not young 11020045
0.00 youngs/s, 0.00 non-youngs/s
Pages read 172526, created 22673, written 126290
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s,
Random read ahead 0.00/s
LRU len: 6908, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]

InnoDB缓存池指标:https://dev.mysql.com/doc/refman/8.0/en/innodb-buffer-pool.html

并发访问Buffer Pool的性能问题

  • 多线程同时访问Buffer Pool,就会同时操作同一个free链表、flush链表和lru链表。那必然要进行加锁
  • 因为是基于内存的操作,所以很快。其次这些链表的操作,也是基于指针的操作,也不存在性能低下的可能。

多个Buffer Pool优化并发能力

给Buffer Pool分配比较大的内存,则可以设置多个Buffer Pool.如果给分配的内存小于1G,最多就只有1个Buffer Pool。

innodb_buffer_pool_instances=8

基于chunk机制动态调整Buffer Pool的大小

  • Buffer Pool是由多个chunk组成的,默认一个chunk的大小是128M。

    分配Buffer Pool的内存8G,4个Buffer Pool实例,那么每个Buffer Pool是2G,拥有16个chunk。
  • 需要动态扩容的话只需要申请一系列128MB大小的chunk就行,然后分配给buffer pool就行。

内存的分配

  • Buffer Pool总共占用机器内存的50%-60%
  • buffer Pool总大小 = (chunk size buffer pool instance) chunk count

总结

  • 根据机器的内存设置合理的buffer pool的大小,然后设置buffer pool的数量,使得chunk数量*chunk size 接近单个buffer pool的内存。充分利用内存减少内存碎片
  • 每个buffer pool里的多个chunk共用一套链表数据结构。
  • 后台线程定时根据lru链表和flush链表,去把一批缓存页刷入磁盘并释放,同时更新free链表
  • 如果缓存页满了,无法加载自己的缓存页,就把lru链表冷数据区域的缓存页刷盘
最后修改:2024 年 01 月 11 日
如果觉得我的文章对你有用,请点个赞吧~