十、InnoDB的Buffer Pool

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

InnoDB存存储引擎在处理客户端的请求时,如果需要访问某个页的数据,就把完整的页的数据加载到内存中,也就是说只需要访问一个页的一条记录,也需要把整个页的数据加载到内存中,将整个页的数据加载内存中后就可以进行读写访问了,而且在读写访问之后不着急把该页对应的内存空间释放,而是将页存起来,这样将来有请求再次访问该页面时,就可以省下磁盘I/O的开销
从上面这句话可以获得两个信息:
1、页是磁盘和内存交互的基本单位,哪怕只是读一条数据,也会把该数据对应的页完整的加载到Buffer Pool中
2、页使用完后,不会立马从内存中丢弃,而是暂存在内存中,下一访问时直接使用,减少IO开销


一、Buffer Pool

为了缓存磁盘中的页,mysql服务器在启动的时候就向操作系统申请了一块连续的内存空间,这块内存空间就叫做Buffer Pool
默认情况Buffer Pool大小为128mb,可以通过innodb_buffer_pool_size设置大小,但是最小也得5mb,低于5mb会自动设置为5mb

内部组成:
缓冲页:Buffer Pool对应的一片连续的内存空间被划分为若干了页,页的大小也是16kb,我们成这些页为缓冲页
控制块:每个缓冲页都对应一部分控制信息,而且额这部分控制信息大小是相同的,我们把每个页对应的控制信息对应的一块内存称之为控制块。控制块和缓冲页是一一对应的。其中控制块放到Buffer Pool的前面,缓冲页放到后面
碎片: 在分配足够多的控制块和缓冲页后,剩下的一点空间可能不够分配一对控制块和缓冲页的大小,自然就用不到了,这块用不到的内存就叫做碎片。
在这里插入图片描述
在debug模式下,每个控制块大概占控制页大小的5%(非debug会更小),因此innodb_buffer_pool_size在申请一块连续的空间作为Buffer Pool时,实际申请单到的会比innodb_buffer_pool_size大5%左右

二 、free链表

最初mysql启动的时候,需要申请一块连续的内存空间,作为Buffer Pool,但是刚开始,这里面是没有数据的,随着程序的进行,才会不断地有页被加入。那么问题来了,从磁盘读取一个页到Buffer Pool中时,该放到哪个缓冲页的位置?或者怎么区分哪个缓冲页是空闲的,哪些已经被使用了?因此必须要记录下哪些缓冲页是可用的,这个时候缓冲页对应的控制块就派上用场了。
把所有空闲的控制页对应的控制块作为一个节点放到链表中,这个链表就是free链表,也称为空闲链表。
刚刚完成初始化的buffer pool中,所有的缓冲页都是空闲的,所以每一个缓冲页的对应的控制块都会加入到free链表
在这里插入图片描述
每当从磁盘中读取一个页到Buffer Pool中时,就从free链表中取一个空闲的缓冲页,并把该缓冲页对应的控制块信息填上,然后把该缓冲页对应的free链表节点从链表中移除,表示该缓冲页已经被使用

三、 缓冲页的哈希处理

乍一听可能有点懵,什么是缓冲页的哈希处理?其实是这样的,当我们需要访问某个数据页的内容的时候,首先会判断我们页在Buffer Pool中有没有,没有的话,我们才去磁盘中找,但是Buffer Pool中可能存在很多个缓冲页,哪个是我们想要的呢,如果一个一个遍历是不是效率也太低了,因此在mysql启动的时候,就在内存中维护了一个hash表,当磁盘中的页被加载到Buffer Pool之后,就把表空间号+页号作为key,缓冲页的控制块就作为value。
在需要访问某个数据页的时候,先从hash表中根据表空间+页号看看是否有对应的缓冲页

四、 flush链表

如果我们修改了Buffer Pool中某个缓冲页的数据,这个页就和磁盘上对应的页,就不一样了,这样的缓冲页也叫做脏页。通常我们这么认为的,我们修改了一个页的数据,就得把数据的内容更新到磁盘上,但是我修改一个就更新一个,那岂不是要非常多的io开销?所以每次修改数据页后,我们并不是立刻把缓冲页刷新到磁盘,而是在未来的某个时间进行刷新。Flush链表就是存储脏页的链表,凡是被修改过的缓冲页,它的控制块就会加入到这个链表中,在未来某个时间点会把flush链表对应的脏页更新到磁盘

五 、LRU链表

LRU链表解决的是缓冲区Buffer Pool大小不够的情况:Buffer Pool对应的大小空间是有限的,需要缓存的页占用的内存大小超过了Buffer Pool的大小,也就是说free 链表中,已经没有空闲的缓冲页了,但是我们还要往Buffer Pool中放数据页,这时候就得移除哪些使用不频繁的页,LRU链表就是管理我们的数据页的

1 简单的LRU
当Buffer中不再有空闲的列表时,我们就得淘汰最近很少使用的页,那我们怎么知道哪些是最近频繁使用的页,哪些是不经常使用的页呢?我们可以创建一个链表,由于这个链表是按照最近很少使用的原则去淘汰数据页的,所以这个链表可以称为LRU链表
如果该页不在Buffer Pool中,在把该页从磁盘中加载到Buffer Pool中的缓冲页时,就把该页对应的控制块作为节点塞到LRU链表的头部
如果该页已经被加载到Buffer Pool中,则直接把该页对应的控制块移动到LRU链表的头部
也就说,只要我们使用到某个缓冲页就把某个缓冲页的控制块作为节点放到LRU链表的头部,这样LRU尾部就是最近使用不频繁或者说相对不频繁的页,当空闲的缓冲页用完后,我们就在LRU的尾部找些缓冲页淘汰就好了

2 划分区的LRU链表
1) 简单的LRU链表存在两个问题
问题1——预读

InnoDB提供了一个服务——预读,前面我们说的只有当我们用到某个页的时候,才会将其从磁盘加载到Buffer Pool中,用不到就不加载,所谓预读就是InnoDb认为执行当前的请求,可能在后面读取某些页面,也是就预先把这些页面加载到Buffer Pool中
预读并不是一直发生的,它是由触发机制的,根据触发机制把预读分为:
线性预读:InnoDB提供了一个系统变量innodb_read_ahead_threshold,如果顺序访问某个区的页面超过了这个系统变量值,就会触发异步读取下一个区的全部页到Buffer Pool的请求。注意异步读取意味从磁盘加载这些被预读的页面是,并不影响到当前共工作线程的执行。这个值是可以修改的。
随机预读:如果某个区中13个连续的页面(区中的热页面,头1/4内的)都被加载到了Buffer Pool中,无论这些页面是不是顺序读取的,都会触发一次异步读取本区的所有其他页面到BufferPool中的请求,这个随机预读默认是关闭的,可以通过innodb_random_read_ahead系统变量打开

其实预读本身是一个好事,如果预读带Buffer Pool中的页被成功的使用到,就可以极大地提高语句执行的效率,不过这些预读的页都会放到LRU链表的头部,如果此时Buffer Pool的容量不够大,而且很多预读的页面没有使用到,就会导致处于LRU链表尾部的一些缓冲页会很快被淘汰掉,从而降低Buffer Pool的命中率

问题2——全表扫描
全表扫描意味着把将要访问聚簇索引的全部叶子节点对应的页,如果需要访问的页面特变多,而Buffer Pool又不能全部容纳他们的话,就意味者需要将其他语句在执行过程中用到的页面“排挤”出Buffer Pool,之后在其他语句重新执行时,又得重新从磁盘加载页面到Buffer Pool。
我们在业务中一般不对很大的表执行全表扫描的操作,这是一个很耗时的操作,只有在特定的场景下偶尔对很大的表执行全表扫描操作。由于很大的表执行全表扫描的操作可能要把Buffer Pool中的缓冲页换一次,这会严重影响到其他查询对Buffer Pool的使用,从而降低了Buffer Pool的命中率。

综上可能降低Buffer Pool命中率的两种情况如下:
加载到Buffer Pool中的页不一定被使用到(预读)
如果有非常多的使用频繁偏低的页被同时加载到Buffer pool中,则可能那些使用频繁非常高的页从Buffer Pool中淘汰掉(全表扫描)

2)为了解决上面两个问题,mysql就把LRU链表按照一定比例分成了两截
一部分存储使用频率非常高的缓冲页,这一部分链表也成为热数据,或者称为young 区域
另一部分存储使用频率不是很高的缓冲页,这一部分叫做冷数据,或者称为old区

需要特别注意的是,我们是按照某个比例把LRU链表分成两半的,而不是某些节点固定位于young区,某些节点固定位于old区,随着程序的运行某个节点所属的区域可能发生变化。同时这个比例可以通过innodb_old_blocks_pct修改
在这里插入图片描述

3)把LRU链表分区划分怎么解决上述的两个问题的?
1、解决预读:当磁盘上某个缓冲页在初次加载到Buffer Pool中的某个缓冲页时,该缓冲页对应的控制块会放到old区域的头部。这样一来,预读到Buffer Pool却不进行后续访问的页就逐渐从old区域中移除,而不会影响到young区中使用比较频繁的缓冲页
2、解决全表扫描(短时间内访问大量使用频率非常低的页面的优化)
在进行全表扫描时,虽然首次加载到Buffer Pool 中的页放到了Old区域的头部,但是后去会马上被访问到,每次进行访问时,又会把该页放到young区域的头部,这样仍然会把那些使用频率比较高的页面“排挤”下去。有人会想,是否可以在第一次访问该页面时,不将其从old区域移动到young区域的头部,而是在后续再将其移动过去?这是行不通的,这是因为InnoDB中每次去页面读取一条记录时,都算是访问一次页面,而一个页面可能包含很多条记录,也就是说读取完某个页面的记录,就相当于访问了这个页面好几次。
全表扫描有一个特点,就是它的执行频率非常低,而且在进行全表扫描的过程中,即使某个页面有很多条记录,尽管每次读取一条记录都算是访问一次页面,但是这过程所花费的时间是非常少的。所以我们只要规定在某个处于old区域的缓冲页进行第一次访问时,就在它对应的控制块中记录下这个访问时,如果后续的访问的时间与第一次访问的时间在某个时间间隔内,那么该页面就不会从old区域移动到young区域的头部,否则将它们移动到young区域的头部,这个时间间隔是由系统变量 innodb_old_blocks_time 控制的
默认是1000ms,也就是说对于从磁盘加载到LRU链表的old区域的页面来说,如果第一次和最后一次访问该页面的时间间隔小于1S,那么该页是不会加载到young区域的,这就解决的全表扫描带来的问题

3、更进一步优化LRU链表
只有被访问的区域位于young区域的1/4后面时,才会被移动到LRU链表的头部。
其实无论怎么优化都是尽量提高Buffer Pool的命中率

六 刷新脏页到磁盘

Mysql后台有专门的线程负责每隔一段时间就把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求,刷新方式主要有一下两种:

1、从LRU链表的冷数据中刷新一部分页面到磁盘

2、从flush链表中刷新一部分数据到磁盘

七、多个Buffer Pool实例

Buffer Pool的本质是InnoDB系统申请的一块连续的内存空间,在多线程环境下,访问Buffer Pool中的各种链表需要加锁处理。在Buffer Pool特别大并且多线程并发访问量特别高的请款下,单一的Buffer Pool可能会影响到请求处理速度,所以在Buffer Pool特别大时,可以把他们拆分为若干个小的Buffer Pool,每个Buffer Pool都称为一个实例,它们都是独立的——独立的申请内存空间,独立地管理各种链表。。。。在多线程并发访问时不会相互影响,从而提高了并发处理能力,通过innodb_buffer_pool_instances 的值来修改Buffer Pool实例的个数
那每个Buffer Pool占用的空间就是Buffer Pool的总大下除以实例个数
但是当innodb_buffer_pool_size值小于1G时,设置多少个实例都是无效的,InnoDB会默认把innodb_buffer_pool_instances设置为1。
2个Buffer Pool就是这样:
在这里插入图片描述

八 、innodb_buffer_pool_chunk_size

mysql 5.7.5版本之前,只能在服务器启动时通过配置innodb_buffer_pool_size启动选项来调整Buffer Pool的大小;在服务器启动过程中时不允许调整该值的。不过在5.7.5以及版本之后,支持在服务器运行时调整Buffer Pool大小的功能。但是有一个问题,就是每次调节Buffer Pool大小时,都需要向操作系统申请一块连续的内存空间,然后将旧Buffer Pool中的内容复制到这块新空间;这是极其耗时的。所以,mysql决定不在一次性为某个Buffer Pool实例向操作系统申请一大片连续的内存空间,而是以一个chunk为单位向操作系统申请空间。也就是说Buffer Pool其实是由若干个chunk组成的。一个chunk就代表一个连续的内存空间,里面包含了若干了缓冲页与其对应的控制块。
在这里插入图片描述

图中的Buffer pool 有两个实例组成,每个实例又包含两个chunk。
有了chunk的存在,我们就可以在运行期间动态的增加或删除空间。
默认一个chunk的大小时128MB,可以通过innodb_buffer_pool_chunk_size在启动时修改,但是一旦修改后,运行期间就不能再修改了

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值