Buffer Pool详解

一、简介

​ mysql的数据都是存放在磁盘下的,为了加快cpu从磁盘i/o读取数据的效率,Innodb存储引擎在cpu和磁盘中间添加了一个缓冲区buffer pool。当一个请求进来,会先从buffer pool中去看需要的查询结果数据是否已经存在,存在则直接返回,不存在,则从磁盘读取记录所在页的数据,加载到buffer pool中缓存起来。

1.buffer pool大小

​ 可以通过以下命令查看缓冲池的大小:

show variables like 'innodb_buffer_pool_size'

​ Buffer pool的默认值是128M,我们可以通过变量innodb_buffer_pool_size的大小来设置,最小不能小于5M,若是设置为5M以下,则系统会自动设置为5M。在专用数据库中,buffer pool的大小一般设置为服务器内存的60%。

二、缓存页

1.使用页为存储单位

​ mysql在磁盘中是按页为单位进行数据存储的,默认的页大小是16kb,当我们查询一条数据时,会把这条数据所属的这一页数据都加载进来;buffer pool中缓存数据也是按页为单位进行存储的,默认大小也是16kb。

2.使用控制块关联缓存页

​ 缓存页存放的是数据的具体记录,为了便于定位缓存页、记录缓存页信息,buffer pool中使用控制块来维护这些信息,控制块与缓存页是一一对应的,在mysql服务启动的时候,会完成buffer pool的初始化,申请的内存空间会被划分为若干的控制块和缓存页,此时的控制块记录着对应的缓存页地址,缓存页是空数据的状态。对应关系图如下:
在这里插入图片描述
控制块记录的信息包括:缓存页在buffer pool的地址、该页所属的表空间编号、页号、链表节点信息、锁信息。

每个控制块大约占缓存页的5%,而系统设置的innodb_buffer_pool_size的大小不包括控制块,所以Innodb存储引擎向操作系统申请的内存空间,会比设置的innodb_buffer_pool_size大5%左右。控制块排在前面,缓存页排在后面,他们之间会存在没有分配完的内存碎片。

3.使用哈希记录缓存页

​ 当存储引擎执行sql,获取到结果值所在的表空间号、页号后,需要知道结果页是否已经缓存在buffer pool中,此时不能去遍历所有的控制块看是否已经存在,这样会特别耗时;Innodb是这样处理的:维护一个哈希表,以表空间号+页号作为key,缓存页内存地址为value,在时间复杂度为O(1)的情况下即可判断结果页是否在缓存页中。若是存在,则直接使用此缓存页的数据;若是不存在,则从磁盘中加载对应的页到内存中,找一个空闲控制块,把页的表空间号、页号添加到控制块中,根据控制块的缓存页地址找到缓存页,把查询的此页数据添加到缓存页中。

4.预读机制加载缓存页

​ innodb提供了预读(read_ahead)加载缓存页的机制,就是当innodb执行一个请求查询到某个页数据后,会把之后有可能使用到的其它页数据也读取加载到buffer pool中。触发预读的方式分为两种:随机预读、线性预读。

​ 在开始说预读之前,先了解一下mysql区(extent)的概念。区(extent):区是页的集合,一个区内包含物理上连续的64个页,一个页的默认大小是16kb,一个区的默认大小是64*16kb=1mb。一个表空间划分为多个区,每256个区划分为一个组。

​ 区的结构图是这样的:
在这里插入图片描述
​ 表空间结构图是这样:
在这里插入图片描述
随机预读

​ 如果Buffer pool已经缓存了某个区(extent)连续的13个页面,不管这13个页面是顺序读取还是间隔读取进来的,会触发一次异步读取本区下的其它页加载到buffer pool的请求。innodb使用系统变量innodb_random_read_ahead来设置是否开启随机预读,默认是OFF关闭的状态,如果想要开启,可以通过修改启动参数,或者通过SET GLOBAL把该参数值设置为ON。

线性预读

​ 如果Innodb顺序访问了某个区(extent)下的页面数量超过某个值,则会触发预读机制,异步读取下一个区中全部的页面到buffer pool的请求。此值由系统变量innodb_read_ahead_threshold预读因子来控制,默认为56,此系统变量的设置范围为0-64,因为一个区最多有64个页。若是想改变此值,可以通过修改启动参数,或者通过SET GLOBAL设置该参数值。

三、Free链表

​ 当我们从磁盘读取一页的数据,需要把它加载到buffer pool中时,需要知道哪些缓存页是空闲的,哪些缓存页已经被占用了,此时不能去遍历所有的缓存页看是否空闲。innodb是这样处理的:使用一个空闲链表即free链表来存放还处于空闲状态的缓存页对应的控制块,当需要把页的数据加入到buffer pool时,从free链表中获取一个控制块,通过控制块记录的缓存页地址,找到缓存页,把查询到的页数据加入到缓存页中,把此页所属的表空间编号、页号信息写入此缓存页对应的控制块,最后从free链表中移出此控制块。

​ 当mysql启动,完成buffer pool的初始化,划分好控制块和缓存页对后,会把所有的控制块加入到free链表中,因为mysql刚启动,所有的缓存页都是空闲状态,这也是free链表的初始化。当有新的页需要加入到buffer pool,则从free链表选择一个控制块移除;当存放在buffer pool的缓存页被淘汰,则会把此缓存页对应的控制块加入到free链表中,标识此控制块对应的缓存页现在已经空闲。

​ free链表的效果图是这样:
在这里插入图片描述
​ 在free链表中定义了一个基节点,它记录了链表的start头节点地址、end尾节点地址、count=n当前链表中的节点数量。

四、Flush链表

​ 当innodb执行更新sql的时候,更新的是buffer pool中对应的缓存页数据。当更新完缓存页的数据后,此时更新的这一页数据与从磁盘中加载来时的页数据已经不一致了,也不能每次更新完缓存页的记录,立马同步更新到磁盘中的对应页去,因为磁盘的io速率是远远不及内存的。innodb是这样处理的:当修改完缓存页的数据后,不会立马把修改同步到磁盘对应页中,而是使用一张冲刷链表即flush链表记录此变动缓存页对应的控制块,在未来的某个时间点把变动页从flush链表中刷新到磁盘去,然后把此控制块从flush链表中移除。

​ flush链表的效果图是这样:
在这里插入图片描述

五、LRU链表

​ buffer pool的大小是有限的,一直往里面加入缓存页,free链表会有移除为空的时候,当free链表为空,有新的页数据需要加入进来,就需要对已经加载到buffer pool中的缓存页进行淘汰处理,系统肯定希望淘汰最不频繁使用的缓存页。innodb是这样处理的:使用LRU(Least Recently Used)最近最少使用算法来淘汰缓存页,LRU链表记录访问过的缓存页对应的控制块。

​ LRU链表的工作流程是这样的:当访问某个页时,如果此页不在buffer pool中,则从磁盘加载此页数据到缓存页中,把该缓存页对应的控制块加入到LRU链表的头部;当此页已经在buffer pool中,则把该页对应的控制块移动到LRU链表头部。

​ 这样LRU链表的尾部就是最近最少使用的控制块,当缓存页不够的时候,可以从LRU链表尾部淘汰控制块,连带淘汰控制块对应的缓存页。

1.LRU优化

​ 这样每次有访问缓存页就把对应的控制块放到LRU链表的头部,会有一些问题。比如通过预读机制加载进来的页数据、全表扫描(没有where过滤条件)加载进来的页数据,使用频率可能不高,可能会把使用频率较高的缓存页数据淘汰掉,甚至会使buffer bool中的缓存页进行一次大清洗,而使用buffer pool就是为了提高缓存的命中率,减少对磁盘io的次数。

​ Innodb是这样优化LRU的:把LRU链表按照一定比例分成两截,第一部分存储使用频率非常高的缓冲页对应的控制块,这一部分链表也叫热数据,或者young区域;第二部分存储使用频率不是很高的缓存页对应的控制块,这一部分链表也叫冷数据,或者old区域。Innodb规定,当某个页初次加载到buffer pool中时,缓存页对应的控制块加到LRU链表的old区域的头部,这样通过预读加载进来的页,放到old区域,不影响young区域的数据,后续会从old区域逐渐被淘汰;当再次访问old区域控制块对应的缓存页时,此控制块会从old区域移动到young区域的头部,此缓存页作为热数据处理;当young区域放满后,又有新的控制块需要从old区域移动到young,则young区域的链尾移动到old区域的链头;当再次访问young区域控制块对应的缓存页时,此控制块移动到young区域的头部。

​ LRU链表的效果图是这样:
在这里插入图片描述
​ young和old区域的划分比例由一个系统变量innodb_old_blocks_pct来控制,此值设置了old区域所占的百分比,默认情况下,old区域在LRU链表中所占的比例是37%,大约占LRU链表的3/8,这个比例可以通过修改启动参数,或者通过SET GLOBAL设置该参数值。可以通过此命令查看old区域占比:

show variables like 'innodb_old_blocks_pct'

2.全表扫描优化

​ 但是此种方式对于全表扫描的操作会存在问题,全表扫描的时候,一个页有多条记录需要访问,每访问一条记录就算是访问一次缓存页,则会多次访问此缓存页,还是存在把young区域大清洗的情况,针对这个问题,innodb对每次访问old区域控制块时,都在控制块中记录下当前访问时间,当再次访问时,会比较这次时间与上次访问时间间隔,若是时间间隔小于某个值,则不会把此控制块从old区域移动到young区域,此时间间隔值由系统变量innodb_old_blocks_time,默认值是1000,单位毫秒,即默认1S。这样在进行全表扫描时,多次访问一个页面的时间间隔不会超过1S,即不会把此页对应的控制块移动到young区域头部。

3.young区域优化

​ 当再次访问young区域控制块对应的缓存页时,此控制块移动到young区域的头部,这样每次访问young区域都要进行移动,开销太大了。可以使用一些优化措施,只有被访问的控制块所处的位置在young区域1/4之后,才移动此控制块到young的头部,这样也能降低调整LRU链表的频率。

六、脏页刷新

​ Innodb有专门的后台线程每隔一段时间把有变动的缓存页,即存放在flush链表中控制块对应的缓存页,刷新到磁盘对应的页中,这样不影响系统正常的访问请求。刷新有三种方式:

1.BUF_FLUSH_LRU:从LRU链表的old区域刷新一部分控制块对应的缓存页到磁盘中,后台线程会定时从LRU链表的尾部开始扫描,一次扫描的数量由系统变量innodb_lru_scan_depth来定义,如果扫描到的控制块存在于flush链表,即控制块对应的缓存页有变动,则把缓存页的数据刷新到磁盘中。之所以选择从LRU链表old区域尾部去扫描刷新,是因为当有新的页需要添加到buffer pool中,而当前已经没有空闲缓存页,需要从LRU中去淘汰缓存页腾出空间,若是淘汰的缓存页有变动,则需要等此缓存页刷新到磁盘才能淘汰,这样等待的时间太久,异步定时刷新LRU中控制块对应缓存页有变动的数据到磁盘中,可以大大提高效率。

2.BUF_FLUSH_LIST:后台线程会定时从flush链表刷新一部分控制块对应的缓存页到磁盘中,flush链表中存的都是有变动的缓存页对应的控制块。

3.BUF_FLUSH_SINGLE_PAGE:当有新的页需要添加到buffer pool中,而当前已经没有空闲缓存页,会从LRU链表的old区域尾部淘汰一个控制块对应的缓存页,若是此控制块存在于flush链表中,表示此缓存页有变动还没有同步到磁盘,则需要先把此缓存页刷新到磁盘中,再从LRU中淘汰对应的控制块。

七、多个Buffer pool

​ Buffer pool是Innodb向操作系统申请的一块连续的内存空间,在多线程环境下,访问buffer pool的各种链表都需要加锁处理,在多线程并发的情况下,会严重影响请求的速度。Innodb支持同时使用多个buffer pool,每个buffer pool都称为一个实例,他们独立去申请内存空间,独立管理各种链表,在多线程并发访问的情况下互不影响,提高并发能力。

​ 多个buffer pool实例的效果图是这样的:
在这里插入图片描述
​ 可以在mysql服务启动的时候设置系统变量innodb_buffer_pool_instances的值来修改buffer pool的实例数。每个buffer pool实际占用的内存空间=innodb_buffer_pool_size / innodb_buffer_pool_instances。

​ 并不是buffer pool的实例数越多越好,因为管理每一个buffer pool都需要花销。innodb规定:当innodb_buffer_pool_size小于1G时,设置的innodb_buffer_pool_instances大于1会被重置为1,也就是说要想设置多个实例,innodb_buffer_pool_size的值要大于1G,innodb_buffer_pool_instances的最大值是64。

八、Chunk单位

​ 在mysql 5.7.5之前,buffer pool的大小只能在mysql启动之前通过innodb_buffer_pool_size设置,在mysql运行过程中是不允许进行修改的。在mysql5.7.5及之后的版本中支持运行过程中修改innodb_buffer_pool_size的值,每次修改都需要重新向服务器申请连续的内存空间,把旧的buffer pool数据放到新的buffer pool中,这样的操作是特别费时的。innodb使用chunk为单位来申请连续的内存空间,一个buffer pool由多个chunk组成,每个chunk里面存放控制块和缓存页对,一个chunk的默认大小是128M。这样在运行过程中修改buffer pool的大小,只用增加或者删除chunk的数量,使chunk的累计大小等于新的buffer pool值即可达到修改的效果。

​ chunk的效果图是这样的:
在这里插入图片描述
​ chunk的大小由系统变量innodb_buffer_pool_chunk_size来设置,不过chunk的大小只能在mysql系统启动之前设置,运行过程中是不允许设置的,因为在运行过程中设置会导致所有的buffer pool中的chunk都需要变动,都需要重新申请chunk,再把旧的chunk里的数据放到新chunk中,是非常耗时的。

配置chunk值需要注意的点:

1.innodb_buffer_pool_size的值必须是innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances的整数倍,为了保证每个buffer pool的chunk数量相同;当innodb_buffer_pool_size不是他们两乘积的整数倍时,会自动把innodb_buffer_pool_size的值设置为他们乘积的整数倍。

2.当innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances > innodb_buffer_pool_size时,一个buffer pool实例都分不到一个chunk的大小,此时会重新计算innodb_buffer_pool_chunk_size = innodb_buffer_pool_size/innodb_buffer_pool_instances 。

猜你喜欢

转载自blog.csdn.net/ZHANGLIZENG/article/details/128068247