MySQL调节磁盘和CPU的矛盾--InnoDB的缓存

一、缓存的重要性

对于使用InnoDB作为存储引擎的表来说,不管是用于存储用户数据的索引(包括聚簇索引和二级索引),还是各种系统数据,都是以页的形式存放在表空间中的,而所谓的表空间只不过是InnoDB对文件系统上一个或几个实际文件的抽象,也就是说我们的数据说到底还是存储在磁盘上的。

InnoDB存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省去磁盘IO的开销了。

二、InnoDB的缓冲池

为了缓存磁盘中的页,在MySQL服务器启动的时候就向操作系统申请了一片连续的内存,叫做Buffer Pool (缓冲池 )。

默认情况下 Buffer Pool 只有128M大小,可以在启动时配置 innodb_buffer_pool_size 参数的值来修改缓冲池的大小。

# 268435456的单位是字节,也就是256M
[server]
innodb_buffer_pool_size = 268435456

Buffer Pool 也不能太小,最小值为5M (当小于该值时会自动设置成5M )。

1. 缓冲池的内部组成

Buffer Pool 中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是16KB。

为了更好的管理这些在Buffer Pool中的缓存页,每一个缓存页都创建了一些所谓的控制信息,这些控制信息包括该页所属的表空间编号、页号、缓存页在Buffer Pool中的地址、链表节点信息、一些锁信息以及 LSN 信息。

每个缓存页对应的控制信息占用的内存大小是相同的,我们就把每个页对应的控制信息占用的一块内存称为一个控制块吧,控制块和缓存页是一一对应的,它们都被存放到Buffer Pool中,其中控制块被存放到Buffer Pool的前边,缓存页被存放到 Buffer Pool 后边。

如图:在这里插入图片描述
每一个控制块都对应一个缓存页,那在分配足够多的控制块和缓存页后,可能剩余的那点儿空间不够一对控制块和缓存页的大小,这个用不到的内存空间就被称为碎片了。当然,如果把Buffer Pool的大小设置的刚刚好的话,也可能不会产生碎片。

每个控制块大约占用缓存页大小的5%,而我们设置的innodb_buffer_pool_size并不包含这部分控制块占用的内存空间大小,也就是说InnoDB在为Buffer Pool向操作系统申请连续的内存空间时,这片连续的内存空间一般会比innodb_buffer_pool_size的值大5%左右。

2. free链表的管理

最初启动MySQL服务器的时候,需要完成对Buffer Pool的初始化过程,就是先向操作系统申请BufferPool的内存空间,然后把它划分成若干对控制块和缓存页。

问:初始化后,并没有真实的磁盘页被缓存到 BufferPool 中(因为还没有用到),之后随着程序的运行,会不断的有磁盘上的页被缓存到 Buffer Pool 中。那么,从磁盘上读取一个页到Buffer Pool中的时候该放到哪个缓存页的位置呢?或者说怎么区分BufferPool中哪些缓存页是空闲的,哪些已经被使用了呢?
答:我们可以把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表也可以被称作free链表 (或者说空闲链表)。刚刚完成初始化的Buffer Pool中所有的缓存页都是空闲的,所以每一个缓存页对应的控制块都会被加入到free链表中。

如图所示,假设缓冲池可以容纳的缓存页数量为n:
在这里插入图片描述

  • free链表定义了一个基节点 ,里边儿包含着链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。
  • 链表的基节点占用的内存空间并不包含在为 Buffer Pool 申请的一大片连续内存空间之内,而是单独申请的一块内存空间。

每当需要从磁盘中加载一个页到Buffer Pool中时,就从free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上(就是该页所在的表空间、页号之类的信息),然后把该缓存页对应的free链表节点从链表中移除,表示该缓存页已经被使用了。

3. 缓存页的哈希处理

问:当我们需要访问某个页中的数据时,就会把该页从磁盘加载到Buffer Pool中,如果该页已经在Buffer Pool中的话直接使用就可以了。那么问题也就来了,我们怎么知道该页在不在Buffer Pool中呢?难不成需要依次遍历 Buffer Pool 中各个缓存页么?
答:我们是根据表空间号 + 页号来定位一个页的,也就相当于表空间号 + 页号是一个key,缓存页就是对应的value。使用哈希表就可以通过key快速的找到value。

在需要访问某个页的数据时,先从哈希表中根据表空间号 + 页号看看有没有对应的缓存页,如果有,直接使用该缓存页就好,如果没有,那就从 free链表中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置就可以了。

4. flush链表的管理

如果我们修改了Buffer Pool中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页。 最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上,但是频繁的往磁盘中写数据会严重的影响程序的性能。所以每次修改缓存页后,我们并不着急立即把修改同步到磁盘上,而是在未来的某个时间点进行同步。

问:如果不立即同步到磁盘的话,那之后再同步的时候我们怎么知道Buffer Pool中哪些页是脏页呢?
答:创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫flush链表

在这里插入图片描述

5. LRU链表的管理

如果需要缓存的页占用的内存大小超过了Buffer Pool大小,也就是free链表中已经没有多余的空闲缓存页的时候该怎么办?
可以把某些旧的缓存页从Buffer Pool中移除,然后再把新的页放进来。

假设我们一共访问了n次页,那么被访问的页已经在缓存中的次数除以 n 就是所谓的缓存命中率,缓存命中率越高越好。

①简单的LRU链表

问:当Buffer Pool中不再有空闲的缓存页时,就需要淘汰掉部分最近很少使用的缓存页。不过,我们怎么知道哪些缓存页最近频繁使用,哪些最近很少使用呢?
答:我们可以再创建一个链表,由于这个链表是为了按照最近最少使用的原则去淘汰缓存页的,所以这个链表可以被称为LRU链表 (Least Recently Used)

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

只要我们使用到某个缓存页,就把该缓存页调整到 LRU链表 的头部,这样LRU链表尾部就是最近最少使用的缓存页喽。

②划分区域的LRU链表

InnoDB提供了一种服务预读( read ahead )。就是InnoDB认为执行当前的请求可能之后会读取某些页面,就预先把它们加载到 Buffer Pool 中。根据触发方式的不同预读又可以细分为线性预读和随机预读。

如果预读到Buffer Pool中的页成功的被使用到,那就可以极大的提高语句执行的效率。可是如果用不到呢?这些预读的页都会放到LRU链表的头部,但是如果此时Buffer Pool的容量不太大而且很多预读的页面都没有用到的话,这就会导致处在LRU链表尾部的一些缓存页会很快的被淘汰掉,会大大降低缓存命中率

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

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

  • 存储使用频率非常高的缓存页,所以这一部分链表也叫做==热数据 ==,或者称young区域。
  • 存储使用频率不是很高的缓存页,所以这一部分链表也叫做==冷数据 ==,或者称old区域。
    在这里插入图片描述

我们是按照某个比例将LRU链表分成两半的,不是某些节点固定是young区域的,某
些节点固定是old区域的,随着程序的运行,某个节点所属的区域也可能发生变化。

默认情况下,old 区域在 LRU链表 中所占的比例是 37% ,也就是说 old 区域大约占 LRU链
表 的 3/8 。这个比例我们是可以设置的,我们可以在启动时修改 innodb_old_blocks_pct 参数来控制old区域在LRU链表中所占的比例:

#在服务器运行期间,我们也可以修改这个系统变量的值,不过需要注意的是,这个系统变量属于全局变量,一经修改,会对所有客户端生效
[server]
innodb_old_blocks_pct = 40

当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓存页时,该缓存页对应的控制块会被放到old区域的头部。这样针对预读到Buffer Pool却不进行后续访问的页面就会被逐渐从old区域逐出,而不会影响youn 区域中被使用比较频繁的缓存页。

问:在进行全表扫描时,虽然首次被加载到Buffer Pool的页被放到了old区域的头部,但是后续会被马上访问到,每次进行访问的时候又会把该页放到young区域的头部,这样仍然会把那些使用频率比较高的页面给顶下去怎么办?
答:在对某个处在old区域的缓存页进行第一次访问时在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部。

间隔时间是由系统变量innodb_old_blocks_time 控制。.

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

③LRU链表的优化

对于young区域的缓存页来说,我们每次访问一个缓存页就要把它移动到LRU链表的头部,这样开销太大了。

优化策略:只有被访问的缓存页位于young区域的1/4的后边,才会被移动到LRU链表头部,这样就可以降低调整LRU链表的频率,从而提升性能。

6. 刷新脏页到磁盘

后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。

主要的两种方法:

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

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

有时系统特别繁忙时,也可能出现用户线程批量的从flush链表中刷新脏页的情况,很显然在处理用户请求过程中去刷新脏页是一种严重降低处理速度的行为,这属于一种迫不得已的情况。

7. 多个缓存池实例

Buffer Pool本质是InnoDB向操作系统申请的一块连续的内存空间,在多线程环境下,访问Buffer Pool中的各种链表都需要加锁处理,在Buffer Pool特别大而且多线程并发访问特别高的情况下,单一的Buffer Pool可能会影响请求的处理速度。所以在Buffer Pool特别大的时候,我们可以把它们拆分成若干个小的Buffer Pool,每个Buffer Pool都称为一个实例,它们都是独立的,独立的去申请内存空间,独立的管理各种链表,在多线程并发访问时并不会相互影响,从而提高并发处理能力。

在服务器启动的时候通过设置innodb_buffer_pool_instances的值来修改Buffer Pool实例的个数:

[server]
innodb_buffer_pool_instances = 2

在这里插入图片描述
不过也不是说Buffer Pool实例创建的越多越好,分别管理各个Buffer Pool也是需要性能开销的,当innodb_buffer_pool_size的值小于1G的时候设置多个实例是无效的,InnoDB会默认把innodb_buffer_pool_instances 的值修改为1。

8. innodb_buffer_pool_chunk_size

在 MySQL 5.7.5 之前, Buffer Pool 的大小只能在服务器启动时通过配置 innodb_buffer_pool_size 启动参数来调整大小,在服务器运行过程中是不允许调整该值的。不过设计在 5.7.5 以及之后的版本中支持了在服务器运行过程中调整 Buffer Pool 大小的功能。

每次当我们要重新调整 BufferPool 大小时,都需要重新向操作系统申请一块连续的内存空间,然后将旧的 Buffer Pool 中的内容复制到这一块新空间,这是极其耗时的。所以MySQL 不再一次性为某个Buffer Pool 实例向操作系统申请一大片连续的内存空间,而是以一个chunk为单位向操作系统申请空间。也就是说一个Buffer Pool实例其实是由若干chunk组成的,一个chunk就代表一片连续的内存空间,里边儿包含了若干缓存页与其对应的控制块。
在这里插入图片描述
在服务器运行期间调整 Buffer Pool 的大小时就是以 chunk 为单位增加或者删除内存空间,而不需要重新向操作系统申请一片大的内存,然后进行缓存页的复制。

chunk的大小是我们在启动操作 MySQL服务器时通过innodb_buffer_pool_chunk_size启动参数指定的,它的默认值是128M。不过需要注意的是,innodb_buffer_pool_chunk_size的值只能在服务器启动时指定,在服务器运行过程中是不可以修改的。

三、配置Buffer Pool时的注意事项

  • innodb_buffer_pool_size 必须是 innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances 的倍数(保证每一个Buffer Pool实例中包含的chunk数量相同)。
  • 如果在服务器启动时, innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances 的值已经大于 innodb_buffer_pool_size 的值,那么 innodb_buffer_pool_chunk_size 的值会被服务器自动设置为innodb_buffer_pool_size/innodb_buffer_pool_instances的值。

查看Buffer Pool的状态信息
sql SHOW ENGINE INNODB STATUS

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值