mysql(二) Buffer Pool(缓冲池)的内部结构

上一篇  mysql(一)  中以一个update语句为例简单的介绍了innodb的大体结构,然而还是存在很多细节问题的,就如上一篇文末提及的几个问题。该篇主要对于InnoDB进行比较详细的分析

一、Buffer Pool (缓冲池)的内存结构

一、配置Buffer Pool的大小

默认128M(mysql的一个内存组件),通过innodb_buffer_pool_size配置其大小。

set global innodb_buffer_pool_size = 4227858432; ##单位字节

1、在知道怎么配置Buffer Pool的大小之前先了解下Buffer Pool的大概结构

InnoDB存储引擎中可以设置多个Buffer Pool,而每个Buffer Pool是由多个chunk(默认128M)组成。

chunk的大小可以通过innodb_buffer_pool_chunk_size 来设置。也就是Buffer Pool的整体结构如下:

其中每个Buffer Pool的大小默认是128M,
每个chunk(缓冲数据块)的默认大小是128M。
每个Buffer Pool的总大小 = chunk大小*buffer pool数量*N倍

例如:如果缓冲池以2GB(2147483648字节)初始化,4个缓冲池实例,1GB(1073741824字节)的数据块大小,那么,数据块大小将被截断为等于innodb_buffer_pool_size / innodb_buffer_pool_instances=500MB的值,如下所示:
 mysqld --innodb_buffer_pool_size=2147483648 --innodb_buffer_pool_instances=4 --innodb_buffer_pool_chunk_size=1073741824;(也可通过配置文件进行配置)

其中innodb_buffer_pool_size总的缓冲池大小为2G,配4个缓冲池,每个chunk大小为1G。

此时就可以通过chunk来动态的扩容buffe pool 的大小了。

二、每个Buffer Pool的内部结构

1、缓存页、数据页(每个Buffer pool都由多个描述数据+缓存页填满,这些缓存页有空的也有有值的)

每个buffer pool中存在多个缓存页(默认16k),且每个缓存页对应一个描述数据(用来描述对应的缓存页的,大小相当于缓存页的5%,所以当你设置一个buffer pool为128M时,实际大小会大于128M,多出的部分就是描述数据的大小)。而当缓存页进行刷盘到磁盘后,其名字就变成了数据页。具体看下图

此时提出一个问题:Buffer Pool会存在一些内存碎片吗?怎么减少这个情况?

——————答案当然是会有内存碎片,那么要减少这种情况需要尽量的将这些缓存页和描述信息紧凑的存放,这样可以尽量的减少内存碎片的情况。
 


2、free链表(记录空的缓存页)

之前讲过,mysql的增删改查操作都是在buffer pool中进行的,当sql执行需要数据时先看buffer pool中是否有该缓存页数据,如果没有会去磁盘中进行IO随机读写数据页缓存到Buffer pool中(数据页到了buffer pool就变成了缓存页,都是16k)

mysql刚开启的适合,这些缓存页都是空的。只有我们进行增删改查时需要数据才从磁盘中随机IO读取数据页到缓存页中。

那么这时会有个问题,你怎么知道Buffer pool中哪个缓存页是空的?————————————mysql通过一条双向链表free将空的缓存页连起来,每次要空的缓存页就去free链表中拿。下面讲下free链表的具体结构

上面说到空的缓存页会被free双向链表记录起来(不是存储缓存页,而是存储每个缓存页的描述信息的地址),每个free链的节点都记录着每个缓存页的描述信息的地址,如果需要缓存页就从该链中拿。有空了的缓存页也会加入该链表中。

除了记录每个缓存页的链表节点外,该链表还存在一个基础节点,该基础节点用来记录该链表中的节点个数。

这里有一个误区,可能大家会认为这个free链是一个独立的物理空间,其实这是错误的,free链其实就是存在缓存页的描述信息中的,用java语言来表达就相当于该链表的pre、next是描述信息类的一个属性,是属于描述信息的。整个链表只有基础节点是不属于buffer pool的(该节点是记录有几个空缓存页节点的)

说到这里,可以用一个例子来形象化这个free链表的作用:当我们在执行一个sql需要某个数据时,可能会有两个疑问

1、我们怎么知道buffer pool有没有缓存该数据?

这时候会出现两个概念:表空间、数据页号。表空间是在buffer pool的概念(我们平常认知的概念就是对应所谓的表+行,在mysql层面是表空间+数据页),每个表空间在磁盘中对应一个表空间文件(这个会在下节进行说明)。数据页号表示在对应的表空间文件中的哪个数据页号。

每次从磁盘中加载数据页时,mysql会有一个哈希结构表,该哈希表中key值为表空间+数据页号,value为缓存页地址。此时如果要看某个数据在缓存中有没有,此时要使用该数据的表空间+数据页号(在mysql存储的层面每个数据都有对应的表空间、数据页号等数据,还有其他的数据在下节进行说明)进行查询即可。那么如果没有缓存该数据,此时就要看第2个问题了。

2、磁盘的数据页怎么到buffer pool的缓存页时,你怎么知道哪些缓存页是空的?

此时free链表就派上用场了,此时你可以去free链中拿取一个节点,找到对应的空缓存页,然后将数据页存到该空缓存页中,然后将该节点从链表中除去。

此时需要再提出一个疑问,问题1中的表+行、表空间+数据页有什么区别和联系?


3、flush链表(解决脏数据页)

上一篇文章中说到每次在buffer pool更新的数据,不会即时的更新到磁盘中,而是由其他线程后面再进行IO刷盘操作。那么此时磁盘的数据就不是最新的数据,此时对应的磁盘数据就是所谓的脏数据页。(这里可以思考下怎么不进行实时刷盘)

既然不是实时的刷盘,那一段时间后,进行刷盘的线程怎么知道buffer pool中哪些缓存页需要刷到磁盘的数据页。此时就需要一条flush双向链表了。

flush是一条用来记录那些需要刷盘的缓存页链表,该flush链也是和free链表类似,都是存在描述数据中的,也存在一个基础节点来统计节点数量(基础节点和其他节点不同,基础节点不属于buffer pool)

这样每次刷盘的线程就会去


4、lru链(淘汰旧的缓存页)(管理已用的缓冲页)

当上面的free链表节点被取完了,即buffer pool中不存在空闲的缓存页了,此时则需要对部分冷数据进行淘汰以便空出空的缓存页来。而mysql再次利用lru双向链表来解决这个问题。

淘汰缓存页并不是将该缓存页丢弃,而是将该缓存页的数据刷新到磁盘,然后空出缓存页空间给别人使用。那么该怎么对那些缓存页进行淘汰呢?


4.1、假如我们来设计这个链表

我们可以从多个方面进行设计,比如缓存命中率:命中率越高的证明经常用,所以我们可以淘汰命中率低的,将命中率高的放在链表头,按照命中率依次排序。最后去淘汰链表尾节点即可。

又比如lru的英文信息————最近最少使用:我们可以将最近使用的节点放在链表头,依次按使用前后排序,最后去淘汰链表尾即可(mysql的lru链表设计类似,不过多了其他的设置)

4.2、mysql中lru链表的设计

其实mysql就是依据lru的算法(最近最少使用)进行设计的,只是在设计的过程中会遇到一些问题,而mysql为解决这些问题对lru链进行改造。下面我们看看mysql是如何设计lru链表的

4.2.1 存在的问题

(1)、mysql的预读机制导致的部分数据没用,却可以排在链表前端。

mysql的预读即读取磁盘的一个数据页时,在某些情况下会把相邻的数据页也一起读进buffer pool中。例如我们要读取数据页A,此时因为某些情况将A相邻的B也读进来了,按照我们上面讲的lru思想是要将A和B都加到链表的前端部分去,此时就出现了B没人调用却放在链表前端的情况,导致最后淘汰的是比B还常用的数据。

我们先不说mysql怎么解决这个问题的,先来看看什么时候会触发mysql预读机制:

———(1-1)、有一个参数innodb_read_ahead_threshold,其默认值为56。意思是访问一个区(后面会将什么是区的概念)里的数据页超过56个,此时会触发预读机制,把下一个相邻区的所有数据页都加载到buffer pool中。

———(1-2)、如果buffer pool中缓存了一个区的13个连续的数据页,且这些数据页是被频繁访问的,此时会触发预读机制将该区的所有数据页都加载到buffer pool中。该机制是可以通过参数innodb_random_read_ahead来控制,默认是OFF(关闭的)

所以默认情况下第一种情况还是会发生的,这就导致了我们前面说的问题(没用的数据被读到lru链的前端)

(2)全表扫描导致的常用缓存页被淘汰

全部扫描即类似:select * from users

此时没有加上where条件,会导致直接把一个表中的所有数据页都加载到bufferpool中,而此时可能真正需要的是部分数据,全表扫描的数据太多,此时会淘汰掉较多常使用的缓存页,这时就会出现上面的问题。

4.2.2 解决方法

mysql为了解决上面提到的问题,对lru链设置了冷热数据区域。利用冷热分离的思想进行设计,下面讲解具体设计

所以mysql中的lru链被分为两部分,一部分热数据,一部分冷数据(占37%)。这个冷热数据的比例可以通过innodb_old_blocks_pct参数控制,默认是37(即冷数据占37%)

那么此时从磁盘中读取的缓存页放在链表的哪里呢?
当数据页第一次被加载进buffer pool时,会将该缓存页放在冷数据链部分的头部,那么冷数据的缓存页满足什么情况下会被加载到热数据区域?

mysql中设定了一个规则,一个innodb_old_blocks_time参数,默认是1000(也就是1000毫秒)

如果一个数据页被加载到缓存页后,1s(innodb_old_blocks_time参数)之后访问了这个缓存页,该缓存页就会被放到热数据的链表头部。(1s之后才能表示该数据可能会被常用)(1s内访问是不会放到链表头部去的)

通过上面的机制,此时就不会将一些不用的缓存页放到链表头(放在冷数据区域链表头),导致大量的常用缓存页被淘汰。冷数据区域只有常用(刚加入的超过1s后调用或者加入超过1s的)的缓存页再被调用才会被放到热数据区域的链表头。(即冷数据区域的缓存页需要距离加入时间超过1s且被再次调用才会回到热数据链头)

那么热数据区域的被访问了会不会立即被调到热数据链表头?——————答案是否定的,由于热数据区域是常被访问且长度较长,所以频繁的调整可能性能会较差,此时mysql则对热数据区域进行了进一步的设定优化

—————————————————————————————————mysql中规定了,只有访问热数据区域的后3/4的缓存页才会被调到热数据的链表头部,而前1/4不会调动。(这样就尽量的减少了节点的移动了)

这里提出问题:

1、为什么mysql要有预读机制?


5、将lru链的尾部节点进行刷盘操作

lru链尾部节点的刷盘时机:
1)、当没有空的缓存页时会触发对链表尾部的几个缓存页进行刷新

2)、mysql中有一个定时任务,定时的去将lru链表的尾部的几个缓存页进行刷盘操作(定时刷盘对lru、flush、free进行更新)


总结:

对于free、flush、lru链表,在整个buffer pool的运行中是动态配合运行的。

1)、当我们执行sql需要数据时,先去buffer pool的哈希表查看是否有该缓存页(表空间+数据页号),如没有此时需要从磁盘中随机IO拿到对应的数据页,然后此时查看free中是否有空的缓存页节点,

2)、有空缓存页则直接拿

3)、free中没有空的缓存页节点,此时则会触发lru链表的缓存页淘汰机制

4)、拿到缓存页空间后将数据页数据存至缓存页中,然后对缓存页数据进行各种操作。

5)、如果对于缓存页进行了更新操作,此时会将节点加载到flush链表中。

6)、在上面的任何过程,mysql都可能出现对lru链的节点淘汰(定时任务),每次lru的节点淘汰后会将对应得flush和free和lru链节点进行更新。

所以整个过程看上去是动态的:没有缓存页找free,free没有了找lru,lru淘汰后更新free和flush,定时lru淘汰,定时刷盘更新flush和lru和free。

这里提出一个问题:
如果缓存页满了,此时要对lru进行淘汰再从磁盘中读取数据页来,为什么要这么做,性能不会低吗?

————就像上面说的,mysql有个定时任务会定时的去对lru链的尾部节点进行淘汰,这样就能尽量的减少出现这种问题。还有就是在资源充足的情况下尽量为buffer pool分配足够的空间。

一、这里提出几个生产中buffer pool出现的几个问题:

1、buffer pool在访问的时候需要加锁吗?

没错,多个线程访问同一个buffer pool时需要进行加锁(对缓存页、各种链表等资源进行加锁),等一个线程执行完成后释放锁资源后下一个线程才能使用

2、并发访问时,要进行加锁,这样mysql的性能还能好吗?

其实这些操作都是在内存中进行的,性能还是可以的。但是如果此时这些操作中存在对磁盘的操作的,那么性能就会下降很多。(读磁盘的数据页那是没办法的必须要做的)(这也是为什么不对缓冲页进行即时同步刷盘的原因,因为每次操作都要进行IO刷盘这样就会较大的影响其性能)

3、使用多个buffer pool优化并发能力

在mysql中,如果给的buffer pool总大小小于1G,则只能分配一个buffer pool,只有超过1G才能分为多个buffer pool。

所以在你buffer pool的总大小比较大时,可以配置多个buffer pool从而提高并发处理能力

二、运行期间通过chunk动态扩容buffer pool

buffer pool可以基于chunk进行动态扩容。一个buffer pool中可以有多个chunk,同一个buffer pool下的多个chunk公用一套free、flush、lru链表

我们可以使用show engine innodb status命令查看innodb的一些信息

下面是对于buffer pool相关的一些信息的解释:

这里要尤为注意

1)、free 、lru、flush几个链表的数量,冷热数据转移情况、缓存页读写情况——这些代表着buffer pool的使用情况

2)、buffer pool的千次访问缓存命中率,这个命中率越高,说明大量的操作是基于缓存来执行的,代表性能越高

3)、磁盘IO情况,磁盘IO越多,说明数据库的性能越差。

下一篇将数据页和缓存页的具体结构

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值