前置知识:数据页、数据区

Buffer Pool是什么?


Buffer Pool 是 MySQL 数据库里面的一个非常关键的组件,存在于内存中,它缓存了真实的磁盘数据。对 MySQL 进行操作的时候,先将磁盘上的数据复制进去内存中的 Buffer Pool ,然后对 Buffer Pool 里面的数据进行操作。然后通过一系列的机制,将 Buffer Pool 里面的数据写入磁盘。

Buffer Pool的结构


Buffer Pool里面存在大量的缓存页,用来装磁盘上的数据页。由于是用来装磁盘上的数据页的,所以,它的大小和数据页的大小对应,一般数据页的大小为16K,所以,缓存页的大小也是16K。 每个缓存页都有一个描述块的数据结构来描述这个缓存页,大概记录着对应的缓存页里面装载的数据页的表空间,数据页编号,地址等等。

所有的描述块放在一起, 然后缓存页放在后面,大概如下图所示:

😄详解MySQL的Buffer Pool - 图1
数据库启动后,MySQl就会根据设置的Buffer Pool 大小(默认128M)向操作系统申请内存,然后将内存划分为一个个的数据描述块和缓存页,这时候,描述块和缓存页都是空的没有存储数据的。

free 链表

Buffer Pool 里面有一个专门用于记录空闲缓存页的双向链表,称为 free链表。free链表里,每个结点就是一个空闲的数据缓存页的描述块的地址,只要缓存页是空的,没有存入数据的,都会被放入到free链表中。

将数据页从磁盘上读取到缓存页的时候,先从free链表里面找到一个空闲的缓存页,然后将数据页复制一份到对应的缓存页,然后将这个对应的缓存页的数据描述块从free链表里面移除。

数据页缓存哈希表

这个哈希表,会用 表空间号 + 数据页号 作为 key , 缓存页作为 value,存储着已经缓存在 Buffer Pool 里面的数据页。将数据页读取到缓存页之前,会在这个哈希表里面查找,如果存在记录,则直接操作内存中的缓存页,如果不存在,则去 free链表 找到空闲的缓存页,然后将数据页复制到缓存页。

flush 链表

Buffer Pool 里面有一个专门用于记录脏页的双向链表,称为 flush链表。flush链表里,每个结点就是一个已经被修改过的数据缓存页的描述块的地址,只要缓存页是被修改过的,都会被放入到 flush链表中。

脏页是指,已经在缓存页中进行了修改,但是还没有刷回磁盘的数据页。

后台会有一个线程,不定时的从 flush 链表读取脏页刷回磁盘。

LRU 链表 (Least Recently Used)

为了增大缓存的命中率,减少频繁的从磁盘读取数据页到缓存,引入了LRU链表。LRU链表也是双向链表,每个结点存储着每个已经使用的缓存页的数据描述块。

当一个数据页被装载入缓存页的时候,对应的数据描述块就会放入LRU链表的头部。在LRU里面的缓存页,如果被访问和使用了,也会移动到LRU链表的头部。如果出现free链表已经使用完的情况,就会先将LRU链表尾部的数据页刷入磁盘,腾出空闲的缓存页提供使用。这个只是大致的流程,实际的流程是已经经过优化的流程,会复杂很多,会在下文进行详细的分析。

MySQL预读机制

MySQL设计了一些预读机制,增加缓存的命中率。

  • 如果访问了一个数据区里的多个数据页,访问的数据页的数量超过了阈值,就会出发预读机制,将向邻的下个数据区的所有数据页都加载到缓存页中去。这个阈值是使用这个参数来进行配置的: innodb_read_ahead_threshold 。 默认是56 ,一个数据区,默认是有64个数据页。

  • 如果Buffer Pool里面缓存了一个数据区里面的13个连续的数据页,而且这些数据页都是比较频繁被访问到的,这时候就会触发预读机制,将这个数据区里面剩下的所有数据页加载到缓存中去。 这个机制是通过这个参数来进行配置的: innodb_random_read_ahead。默认是关的。


LRU 链表优化

如果将所有缓存的链表都放在一个LRU链表里,就会由于MySQL的预读机制,将一些不常用的数据页加载到了LRU的头部,导致被频繁使用的缓存页放到了LRU的尾部,出现缓存页不够用的情况,可能会导致频繁使用的缓存页被淘汰。

基于这个问题,LRU进行了优化:将LRU链表分为两部分,分别是 热数据区 和 冷数据区。它们的占比由参数 innodb_old_blocks_pct 控制,默认为37,表示冷数据占LRU链表的37%。

数据页从磁盘进入缓存页的时候,全部先加载进去冷数据区的头部,如果这些数据页在1秒之后,还产生访问的话,就会被移动到热数据区的头部。这个1秒由参数 innodb_old_blocks_time 控制,默认为1000,表示1000毫秒。

在热数据区内的数据,也不是所有的数据被访问后,都需要移动到热数据区的头部。只有在热数据区后3/4的部分的数据页被访问后才会移动到热数据区的头部,热数据区的钱1/4的数据页被访问后,是不会产生移动的。这样就有效减少了数据页移动的次数。


chunk 机制

实际上,MySQL 还设置了一个 chunk 机制 ,Buffer Pool 是由很多个 chunk 组成的。每个 chunk 包含数据描述块和缓存页,每个 chunk 的默认大小是128M,由参数 innodb_buffer_pool_chunk_size 控制多个 chunk 共用一套 free,flush,LRU链表。chunk机制的加入,就可以令 Buffer Pool 不需要申请连续的大内存空间,只需要一个个的 chunk 大小的空间就可以了。还可以在 MySQL 运行的过程中,动态的调整 Buffer Pool 的大小:申请新的 chunk ,然后加入到 Buffer Pool 即可。
😄详解MySQL的Buffer Pool - 图2

Buffer Pool运行过程


MySQL不断的执行增删改查的过程中,3个链表会不断的动态的变化。不断加载数据页到缓存页的过程中,free链表不断的缩小,LRU链表不断的增大和进行结点的移动,缓存页发生改变的时候,flush链表不断的增加。与此同时,后台会有一些线程,不断的将flush链表中的缓存页刷回磁盘,不断的将LRU链表中的冷数据淘汰,在淘汰和回刷磁盘的同时free链表不断的增大,flush链表和LRU链表不断的缩小。在运行的过程中,这些操作都是同时动态执行的。

Buffer Pool调优


MySQL 进行 Buffer Pool 操作的时候,就是加载数据页到缓存页,操作free链表,flush链表,LRU链表,从缓存页回写数据入磁盘的时候,是会用多个线程去进行操作的。但是,这些线程会进行排队,先获取锁,然后进行一系列操作,然后释放锁给其他线程操作。如果只有1个 Buffer Pool 的话,线程队列性能就会比较低,可以通过分配多个 Buffer Pool 来提升并发能力。

MySQL默认的规则是,给 Buffer Pool 分配的内存空间小于 1G 的时候,最多分配1个 Buffer Pool 。如果分配的内存空间大于 1G ,则可以使用一下参数来配置 Buffer Pool 的内存空间大小和 Buffer Pool 的个数。

  1. innodb_buffer_pool_size = 8589934592
  2. innodb_buffer_pool_instances = 4

innodb_buffer_pool_size : 指定了 Buffer Pool 的大小 , 这里指定了8G 。
innodb_buffer_pool_instances :指定了 Buffer Pool 的个数, 这里指定了4个。
上面配置了 Buffer Pool 的空间有8G,4个 Buffer Pool 实例,每个实例 2G 。这样,多个线程并发访问的时候,就可以分开到4个 Buffer Pool 进行操作,提升了多线程并发能力。

一般来说,分配Buffer Pool 的大小,占用到总内存的50%~60%。 留下一些存储空间提供给系统的其他软件使用。

Buffer Pool 总大小 = (chunk大小 * Buffer Pool数量) 的倍数

例如,总内存有32G ,那么Buffer Pool 的总大小 大概 = 20G ,chunk 默认是128M 。 这时候可以设置 Buffer Pool 的数量为16个:128M * 16 = 2048M ,总大小是20G ,就是2048M的10倍。设置16个是没问题的。

也可以分配多一些 Buffer Pool l例如32个:128M * 32 = 4096M ,总大小是20G ,就是4096M的5倍。这样设置也是没问题的。

具体设置多少个,需要针对MySQL服务器进行压测来测试出来就可以了。

查看Buffer Pool状态


连接上数据库后,执行 SHOW ENGINE INNODB STATUS; 命令就可以查看 innodb 引擎的详细情况,包含了Buffer Pool 的详细状态。

BUFFER POOL AND MEMORY
----------------------
Total memory allocated 137363456; in additional pool allocated 0   #Buffer Pool 的总大小
Dictionary memory allocated 174948        #分配给 InnoDB 数据字典的总内存
Buffer pool size   8191        #能容纳多少个缓存页
Free buffers       1            #free链表中剩余空闲缓存页
Database pages     8002        #LRU链表中存在的缓存页
Old database pages 2935        #LRU链表冷数据区的缓存页数量
Modified db pages  0            #flush链表中缓存耶的数量
Pending reads 0                        #等待从磁盘上加载进缓存页的数量
Pending writes: LRU 0, flush list 0, single page 0    #等待刷入磁盘的缓存页的数量
Pages made young 75102, not young 5069225        #LRU链表里面,冷数据区被移动到热数据区的缓存页数量;冷数据区里面,1秒内被访问但是没有进入热数据区的缓存页数量
0.00 youngs/s, 0.00 non-youngs/s        #每秒从冷数据区进入到热数据区的缓存页的数量; 每秒在冷数据区被访问了但是没有进入热数据区的缓存页数量
Pages read 1332762, created 336490, written 965723        #已经读取,创建,写入的缓存页的数量
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: 8002, unzip_LRU len: 0            #LRU 链表缓存页的数量; 
I/O sum[0]:cur[0], unzip sum[0]:cur[0]