17.1 缓存的重要性
即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。
17.2 InnoDB的Buffer Pool
17.2.1 什么是Buffer Pool
MySQL 服务器启动时申请的用来缓存磁盘中的页的连接内存,叫做 Buffer Pool。默认 128 M,通过参数innodb_buff_pool_size
来调节它的大小。
17.2.2 Buffer Pool内部组成
Buffer Pool 中默认的缓存页大小和磁盘上默认页的大小一样,都是16KB。
控制信息:与缓存页一一对应,包含该页所属表空间编号、页号、缓存页在 Buffer Pool 中的地址,链表节点、锁以及 LSN 信息等。
控制块:存储控制信息的内存块,每个控制块占用的内存大小是相同的。
TIPS:每个控制块占用缓存页大小的5%
17.2.3 free链表的管理
MySQL 服务器启动时初始化 Buffer Pool,将所有空闲的缓存对应的控制块作为一个节点放到一个链表中,称为 free 链表。
17.2.4 缓存页的哈希处理
对于 Buffer Pool 中的所有页,使用表空间号 + 页号作为 key,缓存页作为 value 创建一个哈希表,在需要访问某个页的数据时,直接从哈希表中取即可。
17.2.5 flush链表的管理
如果修改了 Buffer Pool 中某个缓存页的数据,使得它和磁盘上的页不一致了,这样的页被称为脏页(dirty page)。
触发同步时,把脏页同步到磁盘。
把脏页加到一个称为 flush 的链表上。
17.2.6 LRU链表的管理
17.2.6.1 缓存不够的窘境
Buffer Pool 淘汰策略:Least recently used(LRU),最近最少使用
17.2.6.2 简单的LRU链表
- 如果该页不在 Buffer Pool 中,在把该页从磁盘加载到 Buffer Pool 中时,就把该页对应的控制块加到链表头部;
- 如果该页已经缓存在 Buffer Pool 中,则直接把该页对应的控制块移动到链表头部;
- 链表尾部的就是最近最少使用的缓存,当 Buffer Pool 空闲缓存页用完触发淘汰时,直接从链表尾部开始淘汰即可。
17.2.6.3 划分区域的LRU链表
两种特殊情况:
- 预读:InnoDB 认为执行当前的请求可能之后会读取某些页面,从而预先把它们加载到 Buffer Pool
- 某些查询语句导致一次性加载大量页到 Buffer Pool
这两种情况会导致以下结果从而降低 LRU 效率:
- 加载到 Buffer Pool 中的页不一定会被用到
- 如果大量低频使用的页被同时加载到 Buffer Pool,会导致高频使用的页被淘汰
为了应对以上的情况,InnoDB 将 LRU 链表按照一定比例分成两个部分:
- 热数据,或者称 young 区域,用于存储使用频率非常高的缓存页;
- 冷数据,或者称 old 区域,用于存储使用频率不是很高的缓存页。
old 区域的占比:
SHOW VARIABLES LIKE 'innodb_old_blocks_pct';
针对预计场景的优化
当磁盘上的某个页面初次加载到 Buffer Pool 时,其对应的控制块应放到 old 区域的头部。这样针对预读到 Buffer Pool 却不进行后续访问的页面就会逐渐从 old 区域淘汰,而不会影响 young 区的页面。
针对全表扫描,短时间内访问大量使用频率非常低的页面的场景优化
当磁盘上的某个页面初次加载到 Buffer Pool 时,其对应的控制块应放到 old 区域的头部。但是全表扫描时会访问页面中所有数据,相当于多次访问了这个页面,所以此时还不能将它放到 young 区。只有在对某个处在 old 区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与这个时间在某个时间间隔内,那么该页面就不会被从 old 区移动到 young 区。
SHOW VARIABLES LIKE 'innodb_old_blocks_time'
17.2.6.4 更进一步优化 LRU 链表
对于 young 区的缓存页,如果每次被访问就要往头部移动一次,开销太太。只有被访问的缓存页处于 young 区的后1/4,才会被移动到链表头部。
17.2.7 其他的一些链表
unzip LRU 链表,用于管理解压页
zip clean 链表,用于管理没有被解压的压缩页
zip free 数组,每个元素都代表一个链表
17.2.8 刷新脏页到磁盘
后台有专门的纯种每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户纯种处理正常的请求。
- 从 LRU 链表冷数据中刷新一部分页面到磁盘,这种方式被称为 BUF_FUSH_LRU。
- 从 flush 链表中刷新一部分页面到磁盘,这种方式被称为 BUF_FLUSH_LIST。
17.2.9 多个Buffer Pool实例
在多线程环境下,访问 Buffer Pool 的各种链表都需要加锁处理,如果并发特殊高,单一的 Buffer Pool 可能会影响请求的处理速度。所以在 Buffer Pool 特别大的时候,可以把它拆分成若干个小的 Buffer Pool,各个 Buffer Pool 之间相互独立。通过innodb_buffer_pool_instances
来控制 Buffer Pool 实例的个数。
PS:当innodb_buffer_pool_size
的值小于 1G 时,设置多个实例是无效的,InnoDB 会默认把innodb_buffer_pool_instances
修改成1。
17.2.10 innodb_buffer_pool_chunk_size
Buffer Pool 以 chunk 为基本单位向操作系统申请空间。一个 Buffer Pool 实例包含若干个 chunk。
innodb_buffer_pool_chunk_size
默认 128M,只能在服务器启动时指定,运行过程中不可修改。
17.2.11 配置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 的值。
17.2.12 Buffer Pool中存储的其他信息
除了缓存了磁盘上的页面以外,还有锁信息、自适应哈希索引等。
17.2.13 查看 Buffer Pool的状态信息
SHOW ENGINE INNODB STATUS
结果中的BUFFER POOL AND MEMORY
名称 | 含义 |
---|---|
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 pag | 即将以单个页面的形式刷新到磁盘中的页面数量。 |
Pages made young | LRU 链表中曾经从 old 区域移动到 young 区域头部的节点数量 |
Page made not young | 在将 innodb_old_blocks_time 设置的值大于0时,首次访问或者后续访问某个处在 old 区域的节点时由于不符合时间间隔的限制而不能将其移动到 young 区域头部时, Page made notyoung 的值会加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 | 正在解压的页面数量 |
17.3 总结
- 磁盘太慢,用内存作为缓存很有必要
- Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空间,可能通过
innodb_buffer_pool_size
来调整它的大小 - Buffer Pool 向操作系统申请的连续内存块由控制块和缓存页组成,它们一一对应。
- free 链表:Buffer Pool 中的空闲页
- 使用表空间号 + 页号作为 key,缓存页作为 value 建立哈希表,以快速定位某个页
- flush 链表:脏页,待同步
- LRU 链表:分为 young 区和 old 区,优化淘汰 old 区
- Buffer Pool 可以有多个实例
- Buffer Pool 实例由若干个 chunk 组成
- 使用
SHOW ENGINE INNODB STATUS
命令查看 Buffer Pool 的状态信息