数据库 - buffer pool

为减少磁盘与内存之间数据读写的开销,向操作系统申请一片内存空间,称为缓冲池。当需要访问某个页的数据时,把完整的页的数据全部加载到内存中,即使只访问一个页的一条记录,也要把整个页的数据加载到内存。读写之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来。

默认情况下大小为128M,可通过配置innodb_buffer_pool_size来改变。

内部组成
默认的缓存页大小和在磁盘上默认的页大小一样,都是16KB。每一个缓存页都创建了一些所谓的控制信息,这些控制信息包括该页所属的表空间编号、页号、缓存页在Buffer Pool中的地址、链表节点信息、一些锁信息以及LSN信息。每个缓存页对应的控制信息占用的内存大小是相同的,把每个页对应的控制信息占用的一块内存称控制块,控制块和缓存页是一一对应的,存放在Buffer Pool的前面。

在分配足够多的控制块和缓存页后,可能剩余的那点儿空间不够一对控制块和缓存页的大小,自然就用不到喽,这个用不到的那点儿内存空间就被称为碎片了。
在这里插入图片描述

小贴士:
每个控制块大约占用缓存页大小的5%。而设置的innodb_buffer_pool_size并不包含这部分控制块占用的内存空间大小,即InnoDB在为BufferPool向操作系统申请连续的内存空间时,这片连续的内存空间一般比innodb_buffer_pool_size的值大5%左右。

free链表的管理
把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表被称作free链表(或者说空闲链表)。每当需要从磁盘中加载一个页到Buffer Pool中时,就从free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上(就是该页所在的表空间、页号之类的信息),然后把该缓存页对应的free链表节点从链表中移除,表示该缓存页已经被使用了。效果如下:
在这里插入图片描述
为这个链表定义了一个基节点,包含着链表的头节点地址,尾节点地址,以及链表中节点的数量等信息。这里需要注意的是,链表的基节点占用的内存空间并不包含在为Buffer Pool申请的一大片连续内存空间之内,而是单独申请的一块内存空间。

小贴士:
链表基节点占用的内存空间并不大,在MySQL5.7.21这个版本里,每个基节点只占用40字节大小。后边介绍许多不同的链表,它们的基节点和free链表的基节点的内存分配方式是一样一样的,都是单独申请的一块40字节大小的内存空间,并不包含在为BufferPool申请的一大片连续内存空间之内。

哈希处理:判断需要的页是否在缓存池中
用表空间号 + 页号作为key,缓存页作为value创建一个哈希表,在需要访问某个页的数据时,先从哈希表中根据表空间号 + 页号看看有没有对应的缓存页,如果有,直接使用该缓存页,如果没有,就从free链表中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。

flush链表
如果修改了Buffer Pool中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页(英文名:dirty page)。每次修改缓存页后,并不着急立即把修改同步到磁盘上,而是在未来的某个时间点进行同步。

同步的时候,为便于知道Buffer Pool中哪些页是脏页,哪些页从来没被修改过,需要创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,称作flush链表。
在这里插入图片描述

LRU链表管理
如果需要缓存的页占用的内存大小超过了Buffer Pool大小,需要把某些旧的缓存页从Buffer Pool中移除,然后再把新的页放进来。优先移除未被修改的缓存页,具体要移除哪些缓存页呢?

简单的LRU链表
当Buffer Pool中不再有空闲的缓存页时,就要淘汰掉部分最近很少使用的缓存页。需要再创建一个链表去记录,被称为LRU链表(LRU的英文全称:Least Recently Used)。当要访问某个页时,可以这样处理LRU链表:

  • 如果该页不在Buffer Pool中,在把该页从磁盘加载到Buffer Pool中的缓存页,把该页对应的控制块作为节点塞在链表头部。
  • 如果该页已经缓存在Buffer Pool中,则直接把该页对应的控制块移动到LRU链表的头部。

划分区域的LRU链表
上边的这个简单的LRU链表存在两种比较尴尬的情况:

情况一:InnoDB提供了一个服务——预读(英文名:read ahead)。就是InnoDB认为执行当前的请求可能之后会读取某些页面,就预先把它们加载到Buffer Pool中。根据触发方式的不同,预读又可以细分为下边两种:

  • 线性预读
    设计InnoDB的大叔提供了一个系统变量innodb_read_ahead_threshold,如果顺序访问了某个区(extent)的页面超过这个系统变量的值,就会触发一次异步读取下一个区中全部的页面到Buffer Pool的请求。
  • 随机预读
    如果Buffer Pool中已经缓存了某个区的13个连续的页面(且这些页面都在young区域的头1/4处),不论这些页面是不是顺序读取的,都会触发一次异步读取本区中所有其他的页面到Buffer Pool的请求。提供了innodb_random_read_ahead系统变量,它的默认值为OFF。

如果预读到Buffer Pool中的页没有被用到,这些预读的页都会放到LRU链表的头部此时Buffer Pool的容量不太大而且很多预读的页面都没有用到的话,这就会导致处在LRU链表尾部的一些缓存页会很快的被淘汰掉,会大大降低缓存命中率。也就是所谓的劣币驱逐良币。

情况二:存在扫描全表的查询语句。
假设这个表中记录非常多的话,那该表会占用特别多的页,当需要访问这些页时,会把它们统统都加载到Buffer Pool中,Buffer Pool中的所有页都被换了一次血,其他查询语句在执行时又得执行一次从磁盘加载到Buffer Pool的操作。

总结一下上边说的可能降低Buffer Pool的两种情况:

  • 加载到Buffer Pool中的页不一定被用到。
  • 如果非常多的使用频率偏低的页被同时加载到Buffer Pool时,可能会把那些使用频率非常高的页从Buffer Pool中淘汰掉。

因为有这两种情况的存在,把这个LRU链表按照一定比例分成两截,分别是:

  • 一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做热数据,或者称young区域。
  • 另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做冷数据,或者称old区域。
    在这里插入图片描述
    注意:该链表按照某个比例分成两半的,不是某些节点固定是young区域的,某些节点固定是old区域的,随着程序的运行,某个节点所属的区域也可能发生变化。针对InnoDB存储引擎,可以通过查看系统变量innodb_old_blocks_pct的值来确定old区域在LRU链表中所占的比例:
mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_pct';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37    |
+-----------------------+-------+
1 row in set (0.01 sec)

默认情况下,old区域在LRU链表中所占的比例是37%,这个比例是可以设置的,在启动时修改innodb_old_blocks_pct参数来控制old区域在LRU链表中所占的比例:

innodb_old_blocks_pct = 40

在服务器运行期间,也可以修改这个系统变量的值,这个系统变量属于全局变量,一经修改,会对所有客户端生效:

SET GLOBAL innodb_old_blocks_pct = 40;

有了这个被划分成young和old区域的LRU链表之后,针对上边提到的两种可能降低缓存命中率的情况进行优化:

  • 针对预读的页面可能不进行后续访问情况的优化
    设计InnoDB的大叔规定,当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓存页时,该缓存页对应的控制块会被放到old区域的头部,不进行后续访问的页面会被逐渐从old区域逐出,而不会影响young区域中被使用比较频繁的缓存页。
  • 针对全表扫描时,短时间内访问大量使用频率非常低的页面情况的优化
    在对某个处在old区域的缓存页进行第一次访问时,就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部。上述的这个间隔时间是由系统变量innodb_old_blocks_time控制的:
mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_time';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000  |
+------------------------+-------+
1 row in set (0.01 sec)

这个innodb_old_blocks_time的默认值是1000毫秒,也就意味着对于从磁盘上被加载到LRU链表的old区域的某个页来说,如果第一次和最后一次访问该页面的时间间隔小于1s(很明显在一次全表扫描的过程中,多次访问一个页面中的时间不会超过1s),那么该页是不会被加入到young区域的。

综上所述,正是因为将LRU链表划分为young和old区域这两个部分,又添加了innodb_old_blocks_time这个系统变量,才使得预读机制和全表扫描造成的缓存命中率降低的问题得到了遏制,因为用不到的预读页面以及全表扫描的页面都只会被放到old区域,而不影响young区域中的缓存页。

刷新脏页到磁盘
后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,主要有两种刷新路径:

  • 从LRU链表的冷数据中刷新一部分页面到磁盘。
    后台线程会定时从LRU链表尾部开始扫描一些页面,扫描的页面数量可以通过系统变量innodb_lru_scan_depth来指定,如果发现脏页,会把它们刷新到磁盘。这种刷新页面的方式被称之为BUF_FLUSH_LRU
  • 从flush链表中刷新一部分页面到磁盘。
    后台线程也会定时从flush链表中刷新一部分页面到磁盘,刷新速率取决于系统是否繁忙。这种刷新页面的方式被称为BUF_FLUSH_LIST。

有时候后台线程刷新脏页的进度比较慢,导致用户线程在准备加载一个磁盘页到Buffer Pool时没有可用的缓存页,这时就会尝试看看LRU链表尾部有没有可以直接释放掉的未修改页面,如果没有的话,会不得不将LRU链表尾部的一个脏页同步刷新到磁盘。这种刷新单个页面到磁盘中的刷新方式被称之为BUF_FLUSH_SINGLE_PAGE。

多个Buffer Pool实例
Buffer Pool本质是InnoDB向操作系统申请的一块连续的内存空间,在多线程环境下,访问Buffer Pool中的各种链表都需要加锁处理,所以在Buffer Pool特别大时,可以把它们拆分成若干个小的Buffer Pool,每个Buffer Pool都称为一个实例,它们都是独立的,独立的去申请内存空间,独立的管理各种链表,在多线程并发访问时并不会相互影响,从而提高并发处理能力。在服务器启动的时候通过设置innodb_buffer_pool_instances的值来修改Buffer Pool实例的个数,比方说这样:innodb_buffer_pool_instances = 2

这样就表明要创建2个Buffer Pool实例,示意图就是这样:
在这里插入图片描述
那每个Buffer Pool实例实际占多少内存空间呢?总共的大小除以实例的个数,结果就是每个Buffer Pool实例占用的大小。

innodb_buffer_pool_size/innodb_buffer_pool_instances

不过也不是说Buffer Pool实例创建的越多越好,分别管理各个Buffer Pool也是需要性能开销的,当innodb_buffer_pool_size的值小于1G的时候设置多个实例是无效的,InnoDB会默认把innodb_buffer_pool_instances 的值修改为1。而我们鼓励在Buffer Pool大于或等于1G的时候设置多个Buffer Pool实例。

innodb_buffer_pool_chunk_size
在MySQL 5.7.5之前,Buffer Pool的大小只能在服务器启动时通过配置innodb_buffer_pool_size启动参数来调整大小,在服务器运行过程中是不允许调整该值的。

不过在MySQL5.7.5之后版本中支持了在服务器运行过程中调整Buffer Pool大小,但是有一个问题,就是每次重新调整Buffer Pool大小时,都需要重新向操作系统申请一块连续的内存空间,然后将旧的Buffer Pool中的内容复制到这一块新空间,这是极其耗时的。

所以MySQL设计者决定不再一次性为某个Buffer Pool实例向操作系统申请一大片连续的内存空间,而是以一个所谓的chunk为单位向操作系统申请空间。即Buffer Pool实例其实是由若干个chunk组成的,一个chunk就代表一片连续的内存空间,里边儿包含了若干缓存页与其对应的控制块,画个图表示就是这样:
在这里插入图片描述
正是因为chunk的概念,服务器运行期间调整Buffer Pool的大小时就是以chunk为单位增加或者删除内存空间,而不需要重新向操作系统申请一片大的内存,然后进行缓存页的复制。这个所谓的chunk的大小是在启动操作MySQL服务器时通过innodb_buffer_pool_chunk_size启动参数指定的,它的默认值128M。不过需要注意的是,innodb_buffer_pool_chunk_size的值只能在服务器启动时指定,在服务器运行过程中是不可以修改的。

小贴士:为什么不允许在服务器运行过程中修改innodb_buffer_pool_chunk_size的值?

因为innodb_buffer_pool_chunk_size的值代表InnoDB向操作系统申请的一片连续的内存空间的大小,如果你在服务器运行过程中修改了该值,就意味着要重新向操作系统申请连续的内存空间并且将原先的缓存页和它们对应的控制块复制到这个新的内存空间中,这是十分耗时的操作!

另外,这个innodb_buffer_pool_chunk_size的值并不包含缓存页对应的控制块的内存空间大小,所以实际上InnoDB向操作系统申请连续内存空间时,每个chunk的大小要比innodb_buffer_pool_chunk_size的值大一些,约5%。

配置Buffer Pool时的注意事项
innodb_buffer_pool_size必须是innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances的倍数(这主要是想保证每一个Buffer Pool实例中包含的chunk数量相同)。

Buffer Pool的状态信息
通过 SHOW ENGINE INNODB STATUS语句来查看关于InnoDB存储引擎运行过程中的一些状态信息

mysql> SHOW ENGINE INNODB STATUS\G

(...省略前边的许多状态)
----------------------
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 13218349056;
Dictionary memory allocated 4014231
Buffer pool size   786432
Free buffers       8174
Database pages     710576
Old database pages 262143
Modified db pages  124941
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 6195930012, not young 78247510485
108.18 youngs/s, 226.15 non-youngs/s
Pages read 2748866728, created 29217873, written 4845680877
160.77 reads/s, 3.80 creates/s, 190.16 writes/s
Buffer pool hit rate 956 / 1000, young-making rate 30 / 1000 not 605 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 710576, unzip_LRU len: 118
I/O sum[134264]:cur[144], unzip sum[16]:cur[0]
--------------
(...省略后边的许多状态)

mysql>
  • Total memory allocated:代表Buffer Pool向操作系统申请的连续内存空间大小,包括全部控制块、缓存页、以及碎片的大小。

  • Dictionary memory allocated:为数据字典信息分配的内存空间大小,这个内存空间和Buffer Pool没啥关系,不包括在Total memory allocated中。

  • Buffer pool size:代表该Buffer Pool可以容纳多少缓存页,注意,单位是页!

  • Free buffers:代表当前Buffer Pool还有多少空闲缓存页,也就是free链表中还有多少个节点。

  • Database pages:代表LRU链表中的页的数量,包含young和old两个区域的节点数量。

  • Old database pages:代表LRU链表old区域的节点数量。

  • Modified db pages:代表脏页数量,也就是flush链表中节点的数量。

  • Pending reads:正在等待从磁盘上加载到Buffer Pool中的页面数量。
    当准备从磁盘中加载某个页面时,会先为这个页面在Buffer Pool中分配一个缓存页以及它对应的控制块,然后把这个控制块添加到LRU的old区域的头部,但是这个时候真正的磁盘页并没有被加载进来,Pending reads的值会跟着加1。

  • Pending writes LRU:即将从LRU链表中刷新到磁盘中的页面数量。

  • Pending writes flush list:即将从flush链表中刷新到磁盘中的页面数量

  • Pending writes single page:即将以单个页面的形式刷新到磁盘中的页面数量。

  • Pages made young:代表LRU链表中曾经从old区域移动到young区域头部的节点数量。

注意:节点每次只有从old区域移动到young区域头部时才会将Pages made young的值加1,也就是说如果该节点本来就在young区域,由于它符合在young区域1/4后边的要求,下一次访问这个页面时也会将它移动到young区域头部,但这个过程并不会导致Pages made young的值加1。

  • Page made not young:在将innodb_old_blocks_time设置的值大于0时,首次访问或者后续访问某个处在old区域的节点时由于不符合时间间隔的限制而不能将其移动到young区域头部时,Page made not young的值会加1。

注意:对于处在young区域的节点,如果由于它在young区域的1/4处而导致它没有被移动到young区域头部,这样的访问并不会将Page made not young的值加1。

  • youngs/s:代表每秒从old区域被移动到young区域头部的节点数量。
  • non-youngs/s:代表每秒由于不满足时间限制而不能从old区域移动到young区域头部的节点数量。
  • Pages read、created、written:代表读取,创建,写入了多少页。后边跟着读取、创建、写入的速率。
  • Buffer pool hit rate:表示在过去某段时间,平均访问1000次页面,有多少次该页面已经被缓存到Buffer Pool了。
  • young-making rate:表示在过去某段时间,平均访问1000次页面,有多少次访问使页面移动到young区域的头部了。

注意:这里统计的将页面移动到young区域的头部次数不仅仅包含从old区域移动到young区域头部的次数,还包括从young区域移动到young区域头部的次数(访问某个young区域的节点,只要该节点在young区域的1/4处往后,就会把它移动到young区域的头部)。

  • not (young-making rate):表示在过去某段时间,平均访问1000次页面,有多少次访问没有使页面移动到young区域的头部。

注意:这里统计的没有将页面移动到young区域的头部次数,不仅包含因为设置了innodb_old_blocks_time系统变量而导致访问了old区域中的节点,但没把它们移动到young区域的次数,还包含因为该节点在young区域的前1/4处而没有被移动到young区域头部的次数。

  • LRU len:代表LRU链表中节点的数量。
  • unzip_LRU:代表unzip_LRU链表中节点的数量(由于我们没有具体唠叨过这个链表,现在可以忽略它的值)。
  • I/O sum:最近50s读取磁盘页的总数。
  • I/O cur:现在正在读取的磁盘页数量。
  • I/O unzip sum:最近50s解压的页面数量。
  • I/O unzip cur:正在解压的页面数量。
  • 27
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值