【MySQL】 InnoDB的Buffer Pool
【MySQL】 InnoDB的Buffer Pool
Metadata
title: 【MySQL】 InnoDB的Buffer Pool
date: 2023-06-24 14:49
tags:
- 行动阶段/完成
- 主题场景/数据存储
- 笔记空间/KnowladgeSpace/ProgramSpace/BasicsSpace
- 细化主题/数据存储
categories:
- 数据存储
keywords:
- 数据存储
description: 【MySQL】 InnoDB的Buffer Pool
概述
- 磁盘太慢,用内存作为缓存很有必要。
- Buffer Pool 本质上是InnoDB 向操作系统申请的一段连续的内存空间,可以通过 innodb_buffer_pool_size 来调整它的大小。
- Buffer Pool 向操作系统申请的连续内存由控制块和缓存页组成,每个控制块和缓存页都是一一对应的,在填充足够多的控制块和缓存页的组合后, Buffer Pool 剩余的空间可能产生不够填充一组控制块和缓存页,这部分空间不能被使用,也被称为碎片。
- InnoDB 使用了许多链表来管理Buffer Pool 。
- free链表中每一个节点都代表一个空闲的缓存页,在将磁盘中的页加载到Buffer Pool 时,会从free链表中寻找空闲的缓存页。
- 为了快速定位某个页是否被加载到Buffer Pool ,使用表空间号 + 页号作为key ,缓存页作为value ,建立哈希表。
- 在Buffer Pool 中被修改的页称为脏页,脏页并不是立即刷新,而是被加入到flush链表中,待之后的某个时刻同步到磁盘上。
- LRU链表分为young 和old 两个区域,可以通过innodb_old_blocks_pct 来调节old 区域所占的比例。首次从磁盘上加载到Buffer Pool 的页会被放到old 区域的头部,在innodb_old_blocks_time 间隔时间内访问该页不会把它移动到young 区域头部。在Buffer Pool 没有可用的空闲缓存页时,会首先淘汰掉old 区域的一些页。
- 我们可以通过指定innodb_buffer_pool_instances 来控制Buffer Pool 实例的个数,每个Buffer Pool 实例中都有各自独立的链表,互不干扰。
- 自MySQL 5.7.5 版本之后,可以在服务器运行过程中调整Buffer Pool 大小。每个Buffer Pool 实例由若干个chunk 组成,每个chunk 的大小可以在服务器启动时通过启动参数调整。
缓存的重要性
即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。
将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省去磁盘IO 的开销了。
InnoDB的Buffer Pool
什么个Buffer Pool
为了缓存磁盘中的页,在MySQL 服务器启动的时候就向操作系统申请了一片连续的内存,他们给这片内存起了个名,叫做Buffer Pool (中文名是缓冲池)
默认情况下Buffer Pool 只有128M 大小
Buffer Pool内部组成
控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前边,缓存页被存放到 Buffer Pool 后边
free链表的管理
最好在某个地方记录一下Buffer Pool中哪些缓存页是可用的
把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中
缓存页的哈希处理
创建一个哈希表
- 用表空间号 + 页号作为key
- 缓存页作为value
先从哈希表中根据表空间号 + 页号看看有没有对应的缓存页,如果有,直接使用该缓存页就好,
如果没有,那就从free链表中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。
flush链表的管理
如果我们修改了Buffer Pool 中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页(英文名: dirty page )
最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上,但是频繁的往磁盘中写数据会严重的影响程序的性能
凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫flush链表
LRU链表的管理
缓存不够的窘境
如果需要缓存的页占用的内存大小超过了Buffer Pool 大小
假设我们一共访问了n 次页,那么被访问的页已经在缓存中的次数除以n 就是所谓的缓存命中率,我们的期望就是让缓存命中率越高越好
简单的LRU链表
我们可以再创建一个链表,由于这个链表是为了按照最近最少使用的原则去淘汰缓存页的,所以这个链表可以被称为LRU链表(LRU的英文全称:Least Recently Used)
划分区域的LRU链表
两种比较尴尬的情况
- 情况一: InnoDB 提供了一个看起来比较贴心的服务—— 预读(英文名: read ahead )
- InnoDB 认为执行当前的请求可能之后会读取某些页面,就预先把它们加载到Buffer Pool 中。
- 预读又可以细分为下边两种
- 线性预读
- 提供了一个系统变量innodb_read_ahead_threshold
- 如果顺序访问了某个区( extent )的页面超过这个系统变量的值,就会触发一次异步读取下一个区中全部的页面到BufferPool 的请求
- 随机预读
- 如果Buffer Pool 中已经缓存了某个区的13个连续的页面,不论这些页面是不是顺序读取的,都会触发一次异步读取本区中所有其的页面到Buffer Pool 的请求。
- 线性预读
如果此时Buffer Pool 的容量不太大而且很多预读的页面都没有用到的话,这就会导致处在LRU链表尾部的一些缓存页会很快的被淘汰掉,会大大降低缓存命中率。
- 情况二: 可能会写一些需要扫描全表的查询语句
- 当需要访问这些页时,会把它们统统都加载到Buffer Pool 中,Buffer Pool 中的所有页都被换了一次血,其他查询语句在执行时又得执行一次从磁盘加载到Buffer Pool的操作。
- 而这种全表扫描的语句执行的频率也不高,每次执行都要把Buffer Pool 中的缓存页换一次血,这严重的影响到其他查询对 Buffer Pool 的使用,从而大大降低了缓存命中率。
总结一下上边说的可能降低Buffer Pool 的两种情况:
- 加载到Buffer Pool 中的页不一定被用到。
- 如果非常多的使用频率偏低的页被同时加载到Buffer Pool 时,可能会把那些使用频率非常高的页从Buffer Pool 中淘汰掉。
因为有这两种情况的存在,所以设计InnoDB 的大叔把这个LRU链表按照一定比例分成两截,分别是:
- 一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做热数据,或者称young区域。
- 另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做冷数据,或者称old区域。
我们是按照某个比例将LRU链表分成两半的,不是某些节点固定是young区域的,某些节点固定是old区域的,随着程序的运行,某个节点所属的区域也可能发生变化。
有了这个被划分成young 和old 区域的LRU 链表之后,设计InnoDB 的大叔就可以针对我们上边提到的两种可能降低缓存命中率的情况进行优化了:
针对预读的页面可能不进行后续访情况的优化
当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓存页时,该缓存页对应的控制块会被放到old区域的头部。
这样针对预读到Buffer Pool 却不进行后续访问的页面就会被逐渐从old 区域逐出,而不会影响young 区域中被使用比较频繁的缓存页。针对全表扫描时,短时间内访问大量使用频率非常低的页面情况的优化
- 首次被加载到Buffer Pool 的页被放到了old 区域的头部,但是后续会被马上访问到,每次进行访问的时候又会把该页放到young 区域的头部,这样仍然会把那些使用频率比较高的页面给顶下去
- 规定:
- 在对某个处在old 区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部
更进一步优化LRU链表
只有被访问的缓存页位于young 区域的1/4 的后边,才会被移动到LRU链表头部,这样就可以降低调整LRU链表的频率,从而提升性能(也就是说如果某个缓存页对应的节点在young 区域的1/4 中,再次访问该缓存页时也不会将其移动到LRU 链表头部)。
尽量高效的提高 Buffer Pool 的缓存命中率
其他的一些链表
- unzip LRU链表用于管理解压页
- zip clean链表用于管理没有被解压的压缩页
- zip free数组中每一个元素都代表一个链表
- 它们组成所谓的伙伴系统来为压缩页提供内存空间等等
刷新脏页到磁盘
后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。
主要有两种刷新路径:
从LRU链表的冷数据中刷新一部分页面到磁盘。
后台线程会定时从LRU链表尾部开始扫描一些页面,扫描的页面数量可以通过系统变量innodb_lru_scan_depth 来指定,如果从里边儿发现脏页,会把它们刷新到磁盘。这种刷新页面的方式被称之为 BUF_FLUSH_LRU 。从flush链表中刷新一部分页面到磁盘。
后台线程也会定时从flush链表中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。这种刷新页面的方式被称之为BUF_FLUSH_LIST 。
有时候后台线程刷新脏页的进度比较慢,导致用户线程在准备加载一个磁盘页到Buffer Pool 时没有可用的缓存页,这时就会尝试看看LRU链表尾部有没有可以直接释放掉的未修改页面,
如果没有的话会不得不将LRU链表尾部的一个脏页同步刷新到磁盘(和磁盘交互是很慢的,这会降低处理用户请求的速度)。这种刷新单个页面到磁盘中的刷新方式被称之为BUF_FLUSH_SINGLE_PAGE 。
多个Buffer Pool实例
当innodb_buffer_pool_size的值小于1G的时候设置多个实例是无效的,InnoDB会默认把innodb_buffer_pool_instances 的值修改为1。
innodb_buffer_pool_chunk_size
Pool 的大小通过配置innodb_buffer_pool_size 启动参数来调整大小
innodb_buffer_pool_chunk_size的值只能在服务器启动时指定,在服务器运行过程中是不可以修改的。
配置Buffer Pool时的注意事项
- innodb_buffer_pool_size 必须是innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances 的倍数
- 如果在服务器启动时, 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中存储的其它信息
存储锁信息、自适应哈希索引等信息
查看Buffer Pool的状态信息
SHOW ENGINE INNODB STATUS\G
- 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 中的页面数量。
- Pending writes LRU :即将从LRU 链表中刷新到磁盘中的页面数量。
- Pending writes flush list :即将从flush 链表中刷新到磁盘中的页面数量。
- Pending writes single page :即将以单个页面的形式刷新到磁盘中的页面数量。
- Pages made young :代表LRU 链表中曾经从old 区域移动到young 区域头部的节点数量。
- Page made not young :在将innodb_old_blocks_time 设置的值大于0时,首次访问或者后续访问某个处在old 区域的节点时由于不符合时间间隔的限制而不能将其移动到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 区域的头部了。
- not (young-making rate) :表示在过去某段时间,平均访问1000次页面,有多少次访问没有使页面移动到young 区域的头部。
- LRU len :代表LRU链表中节点的数量。
- unzip_LRU :代表unzip_LRU链表中节点的数量(由于我们没有具体唠叨过这个链表,现在可以忽略它的值)。
- I/O sum :最近50s读取磁盘页的总数。
- I/O cur :现在正在读取的磁盘页数量。
- I/O unzip sum :最近50s解压的页面数量。
- I/O unzip cur :正在解压的页面数量。