对于使用 InnoDB 存储引擎的表来说,不管是用于存储用户数据的索引(包括聚簇索引和二级索引),
还是各种系统数据,都是以页的形式存放在表空间中的,
表空间只不过是 InnoDB 存储引擎对文件系统上一个或几个实际文件的抽象,
也就是说:我们的数据说到底还是存储在磁盘上的。
但是我们都知道,磁盘的处理速度非常慢,CPU 的处理速度非常快。


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

Buffer Pool 是什么

设计 InnoDB 存储引擎的人为了缓存磁盘中的页,
在 MySQL 服务器启动的时候就向操作系统申请了一片连续的内存,这片连续的内存叫:Buffer Pool。


默认情况下 Buffer Pool 只有 128M 大小。
可以在启动 MySQL 服务器的时候配置 innodb_buffer_pool_size 参数的值,它表示 Buffer Pool 的大小,
比如:innodb_buffer_pool_size = 268435456
268435456 的单位是字节,也就是指定 Buffer Pool 的大小为 256M。
需要注意的是:Buffer Pool 的最小值为 5M,当参数的值 < 5M 时,该值时会自动设置成 5M。

Buffer Pool 的内部组成

Buffer Pool 中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是 16KB。
为了更好的管理这些在 Buffer Pool 中的缓存页,
设计 InnoDB 的人为每一个缓存页都创建了一些控制信息,这些控制信息包括:该页所属的表空间编号、页号、缓存页在 Buffer Pool 中的地址、链表节点信息、锁信息、LSN 信息,还有一些其他的控制信息。
每个缓存页对应的控制信息占用的内存大小是相同的,
我们就把每个页对应的控制信息占用的一块内存称为一个控制块吧
控制块和缓存页是一一对应的,它们都被存储在 Buffer Pool 中,
其中控制块被存储在 Buffer Pool 的前边,缓存页被存储在 Buffer Pool 后边,
所以整个 Buffer Pool 对应的内存空间看起来如下图所示:
图片.png
每一个控制块都对应一个缓存页,在分配足够多的控制块和缓存页后,可能剩余的空间不够一对控制块和缓存页的大小,这个无法被使用的那点内存空间就被称为碎片。
如果 Buffer Pool 的大小设置的刚刚好的话,也可能不会产生碎片。

小贴士:
每个控制块大约占用缓存页大小的 5%,在 MySQL5.7.21 这个版本中,每个控制块占用的大小是 808 字节。 我们设置的 innodb_buffer_pool_size 并不包含控制块占用的内存空间大小, 也就是说 InnoDB 在为 Buffer Pool 向操作系统申请连续的内存空间时,这片连续的内存空间一般会比innodb_buffer_pool_size 的值大 5% 左右。

free 链表的管理

当最初启动 MySQL 服务器的时候,需要完成对 Buffer Pool 的初始化过程,
就是先向操作系统申请 Buffer Pool 的内存空间,然后把该内存空间划分成若干对控制块和缓存页。
但是此时并没有真实的磁盘页被缓存到 Buffer Pool 中(因为还没有用到),
随着程序的运行,会不断的有磁盘上的页被缓存到 Buffer Pool 中。
那么问题来了:从磁盘上读取一个页到 Buffer Pool 中的时候,页应该放到哪个缓存页的位置呢?
或者说:怎么区分 Buffer Pool 中哪些缓存页是空闲的,哪些缓存页已经被使用了
最好在某个地方记录一下 Buffer Pool 中哪些缓存页是可用的,这时缓存页对应的控制块就派上用场了,
可以把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表被称为 free 链表(或者说空闲链表)。
刚刚完成初始化的 Buffer Pool 中所有的缓存页都是空闲的,所以每一个缓存页对应的控制块都会被加到 free 链表中,假设该 Buffer Pool 中可容纳的缓存页数量为 n,增加了 free 链表的效果图如下图所示:
图片.png
从图中可以看出,为了更好的管理这个 free 链表,为这个链表定义了一个基节点,基节点包含着链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。
需要注意的是:链表的基节点占用的内存空间并不包含在为 Buffer Pool 申请的连续内存空间内,
链表的基节点存储在单独申请的一块内存空间。

小贴士:
链表基节点占用的内存空间并不大,在 MySQL5.7.21 这个版本里,每个基节点只占用 40 字节大小。 后边我们即将介绍许多不同的链表,它们的基节点和 free 链表的基节点的内存分配方式是一样的, 都是单独申请的一块 40 字节大小的内存空间,并不包含在为 Buffer Pool 申请的连续内存空间内。

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

缓存页的哈希处理

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


所以我们可以用表空间号 + 页号作为 key,缓存页作为 value 创建一个哈希表,
当需要访问某个页的数据时,先从哈希表中根据表空间号 + 页号查找对应的缓存页,
如果有,直接使用该缓存页,
如果没有,那就从 free 链表中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。

flush 链表的管理

如果我们修改了 Buffer Pool 中某个缓存页的数据,那缓存页中的数据就和磁盘上的页的数据不一致了,
这样的缓存页也被称为脏页 (dirty page)。
每次修完改缓存页后,InnoDB 并不着急立即把修改同步到磁盘上,而是在未来的某个时间点进行同步。
但是如果没有立即同步到磁盘,而是之后再同步的话,
InnoDB 怎么知道 Buffer Pool 中哪些页是脏页,哪些页从来没有被修改过。
所以,设计 InnoDB 的人创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到 flush 链表中,这个链表的节点对应的缓存页都是需要被刷新到磁盘上的。
链表的构造和 free 链表相似。

LRU 链表的管理

缓存不够的窘境
Buffer Pool 对应的内存大小是有限的,如果需要缓存的页占用的内存大小超过了 Buffer Pool 的大小,
也就是说:free 链表中已经没有空闲缓存页了。
这时候就需要把某些旧的缓存页从 Buffer Pool 中移除,然后再把新的页放进来。
那问题来了:当没有空闲缓存页时,应该移除哪些缓存页呢?
InnoDB 采用了 LRU 算法 (Least Recently Used),优先移除那些最近最少使用的缓存页。

简单的 LRU 链表

InnoDB 怎么知道哪些缓存页最近频繁使用,哪些缓存页最近很少使用呢?
神奇的链表再一次派上了用场,可以再创建一个链表,这个链表是为了按照最近最少使用的原则去淘汰缓存页的,所以这个链表可以被称为 LRU 链表。
当需要访问某个页时,可以这样处理 LRU 链表:

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

当 Buffer Pool 中的空闲缓存页使用完时,淘汰 LRU 链表尾部的缓存页。

划分区域的 LRU 链表

简单的 LRU 链表存在某些问题,因为存在这两种比较尴尬的情况:

  • 情况一:InnoDB 提供了预读 (read ahead) 功能。

所谓预读就是: InnoDB 认为执行当前的请求后,可能会读取某些其他的页面,就预先把它们加载到
Buffer Pool中。根据触发方式的不同,预读又可以细分为两种:线性预读、随机预读

  • 情况二:有的小伙伴可能会写一些需要扫描全表的查询语句。

扫描全表意味着:将访问该表所在的所有页。
假设该表中记录非常多的话,那该表会占用特别多的页,当需要访问这些页时,会把它们都加载到
Buffer Pool中,这也就意味 Buffer Pool 中的所有页都被换了一次血,其他查询语句在执行时又得执行
一次从磁盘加载到 Buffer Pool 的操作。
这严重影响到其他查询对 Buffer Pool 的使用,从而大大降低了缓存命中率。
预读本来是好事,如果预读到 Buffer Pool 中的页成功被使用了,那就可以极大的提高语句执行的效率。
如果此时 Buffer Pool 的容量不太大,而且很多预读的页面都没有被用到的话,
这就会导致处在 LRU 链表尾部的一些缓存页会很快的被淘汰掉,会大大降低缓存命中率。


线性预读
设计 InnoDB 的大叔提供了一个系统变量 innodb_read_ahead_threshold,该变量的值默认为 56,可以在 MySQL 服务器启动时通过启动参数设置,或者服务器运行过程中直接调整该系统变量的值,
它是一个全局变量,必须使用 set global 命令来修改。

如果顺序访问某个区 (extent) 的页面超过这个系统变量的值,
就会触发一次 异步读取下一个区中全部的页面到 Buffer Pool 的请求。
异步读取意味着:从磁盘中加载这些被预读的页面并不会影响到当前工作线程的正常执行。

小贴士: InnoDB 是怎么实现异步读取的 在 Windows 或者 Linux 平台上,可能是直接调用操作系统内核提供的 AIO 接口, 在其它类 Unix 操作系统中,使用了一种模拟 AIO 接口的方式来实现异步读取。 其实就是让别的线程去读取需要预读的页面。


随机预读
如果 Buffer Pool 中已经缓存了某个区的 13 个连续的页面,
不论这些页面是不是顺序读取的,都会触发一次异步读取本区中所有其的页面到 Buffer Pool 的请求。
设计 InnoDB 的大叔同时提供了 innodb_random_read_ahead 系统变量,
该变量的默认值为 OFF,也就意味着:InnoDB 默认不开启随机预读的功能,
如果想开启随机预读功能,可以通过修改启动参数 或者 使用 set global 命令把该变量的值设为 on。


总结一下上边说的,可能降低 Buffer Pool 命中率的两种情况:

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

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

  • 一部分存储使用频率非常高的缓存页,所以这一部分链表叫做热数据,也称 young 区域
  • 另一部分存储使用频率不是很高的缓存页,所以这一部分链表叫做冷数据,也称 old 区域

图片.png

需要注意的是:InnoDB 是按照某个比例将 LRU 链表分成两半的,
不是某些节点固定是 young 区域的,某些节点固定是 old 区域的,
随着程序的运行,某个节点所属的区域也可能发生变化。
对于 InnoDB 存储引擎来说,划分成两截的比例是通过系统变量 innodb_old_blocks_pct 的值确定的,
该变量的值确定了 old 区域在 LRU 链表中所占的比例,
比如:show variables like ‘innodb_old_blocks_pct’;
图片.png
从结果可以看出来,默认情况下,old 区域在 LRU 链表中所占的比例是 37%,
也就是说:old 区域大约占 LRU 链表的 3 / 8。
这个比例我们是可以设置的,我们可以在启动时修改 innodb_old_blocks_pct 参数来控制 old 区域在 LRU 链表中所占的比例,比如:innodb_old_blocks_pct = 40
这样我们在启动 MySQL 服务器后,old 区域占 LRU 链表的比例就是 40%。
在 MySQL 服务器运行期间,也可以修改这个系统变量的值,
这个系统变量属于全局变量,一经修改,会对所有客户端生效,
所以我们只能这样修改:set global innodb_old_blocks_pct = 40;
有了这个被划分成 young 和 old 区域的 LRU 链表之后,
设计 InnoDB 的人就可以针对我们上边提到的两种可能降低缓存命中率的情况进行优化了:

  • 针对:预读的页面可能不进行后续访问的情况的优化:

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

  • 针对:全表扫描时,短时间内访问大量使用频率非常低的页面情况的优化

在进行全表扫描时,虽然首次被加载到 Buffer Pool 的页被放到了 old 区域的头部,但是后续会被马上
访问到,每次进行访问的时候,又会把该页放到 young 区域的头部,这样仍然会把那些使用频率比较
高的页面给顶下去。
全表扫描的特点是:它的执行频率非常低,而且在执行全表扫描的过程中,即使某个页面中有很多条
记录,也就是去多次访问这个页面所花费的时间也是非常少的。
所以我们只需要规定,在对某个处在 old 区域的缓存页进行第一次访问时,就在它对应的控制块中记
录下来这个访问时间,如果后续的访问时间与第一次的访问时间在某个时间间隔内,那么该页面就不
会被从 old 区域移动到 young 区域的头部,否则将它移动到 young 区域的头部。
上述的这个间隔时间由系统变量 innodb_old_blocks_time 控制,默认值为 1000,单位为 毫秒。
show variables like ‘innodb_old_blocks_time’;
图片.png
对于从磁盘上被加载到 LRU 链表的 old 区域的某个页来说,
如果第一次和最后一次访问该页面的时间间隔 < 1s(很明显在一次全表扫描的过程中,多次访问一个
页面中的时间不会超过 1s),那么该页是不会被加入到 young 区域的。
可以在服务器启动或运行时设置 innodb_old_blocks_time 的值。
需要注意的是:如果把 innodb_old_blocks_time 的值设为 0,那么我们每次访问一个页面时就会把该
页面放到 young 区域的头部。
综上所述,正是因为将 LRU 链表划分为 young 和 old 区域这两个部分,
又添加了 innodb_old_blocks_time 这个系统变量,才使得预读机制和全表扫描造成的缓存命中率降低的问题得到了遏制,
因为用不到的预读页面以及全表扫描的页面都只会被放到 old 区域,而不影响 young 区域中的缓存页。

进一步优化 LRU 链表

对于 young 区域的缓存页来说,我们每次访问一个缓存页就要把它移动到 LRU 链表的头部,
这样开销太大,毕竟在 young 区域的缓存页都是热点数据,也就是可能被经常访问的数据,
这样频繁的对 LRU 链表进行节点移动操作不太好,为了解决这个问题提出一些优化策略,
比如:只有被访问的缓存页位于 young 区域的 1/4 的后边,才会被移动到 LRU 链表头部,
这样就可以降低调整 LRU 链表的频率,从而提升性能。
也就是说:如果某个缓存页对应的节点在 young 区域的头 1/4 初,再次访问该缓存页时也不会将其移动到LRU 链表头部。

小贴士:
之前介绍随机预读的时候曾说,如果 Buffer Pool 中有某个区的 13 个连续的页面,就会触发随机预读, 这是不严谨的,还必须要求这 13 个页面是非常热的页面, 所谓的非常热指的是:这13 个连续的页面在整个 young 区域的头 1/4 处。

还有其他的针对 LRU 链表的优化措施。
小册子没有介绍更多。

其他的一些链表

为了更好的管理 Buffer Pool 中的缓存页,除了上边提到的一些措施,
设计 InnoDB 的人还引进了一些其他的链表,比如:

  • unzip LRU 链表用于管理解压页
  • zip clean 链表用于管理没有被解压的压缩页
  • zip free 数组中每一个元素都代表一个链表,数组中的这些链表组成伙伴系统来为压缩页提供内存空间
  • 等等

为了更好的管理这个 Buffer Pool 引入了各种链表或其他数据结构,具体的使用方式小册子没有详细介绍。

刷新脏页到磁盘

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

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

后台线程会定时从 LRU 链表尾部开始扫描一些页面,扫描的页面数量可以通过系统变量
innodb_lru_scan_depth 来指定,如果从里面发现脏页,会把它们刷新到磁盘。
这种刷新页面的方式被称为 buf_flush_lru

  • 从 flush 链表中刷新一部分页面到磁盘。

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

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

多个 Buffer Pool 实例

Buffer Pool 本质是 InnoDB 向操作系统申请的一块连续的内存空间,
在多线程环境下,访问 Buffer Pool 中的各种链表都需要加锁处理,
在 Buffer Pool 很大而且多线程并发访问很高的情况下,单一的 Buffer Pool 可能会影响请求的处理速度。
所以在 Buffer Pool 很大的时候,我们可以把大 Buffer Pool 拆分成若干个小的 Buffer Pool,
每个小的 Buffer Pool 称为一个实例,每个小的 Buffer Pool 都是独立的,独立的去申请内存空间,
独立的管理各种链表等,所以在多线程并发访问时并不会相互影响,从而提高并发处理能力。
我们可以在服务器启动的时候通过设置启动参数 innodb_buffer_pool_instances 的值来修改 Buffer Pool 实例的个数,比如:innodb_buffer_pool_instances = 2
这样就表明我们要创建 2 个 Buffer Pool 实例,如下图所示:、
图片.png

小贴士:
为了简便,只把各个链表的基节点画出来了,这些链表的节点就是每个缓存页对应的控制块

每个 Buffer Pool 实例实际占的内存空间:innodb_buffer_pool_size / innodb_buffer_pool_instances
也就是总共的大小 / 实例的个数,结果就是每个 Buffer Pool 实例占用的大小。


Buffer Pool 实例的数量并不是越多越好,管理各个 Buffer Pool 需要性能开销,
设计 InnoDB 的人规定:当 innodb_buffer_pool_size 的值 < 1G 的时候设置多个实例是无效的,
即总共的大小 < 1G,InnoDB 默认把 innodb_buffer_pool_instances 的值修改为 1。
鼓励在总共的大小 >= 1G 的时候设置多个 Buffer Pool 实例。

innodb_buffer_pool_chunk_size

在 MySQL 5.7.5 之前,Buffer Pool 的大小只能在服务器启动时通过配置 innodb_buffer_pool_size 启动参数来调整大小,在服务器运行过程中不允许调整该值。
在 MySQL 5.7.5 以及之后的版本中支持了在服务器运行的过程中调整 Buffer Pool 大小的功能。
那么问题就来了:每当我们要重新调整 Buffer Pool 大小时,都需要重新向操作系统申请一块连续的内存空间,然后将旧的 Buffer Pool 中的内容复制到这一块新的内存空间里,这个操作是极其耗时的。
所以设 计MySQL 的人决定不再一次性为某个 Buffer Pool 实例向操作系统申请一大片连续的内存空间,
而是以一个 chunk 为单位,向操作系统申请空间。
也就是说一个 Buffer Pool 实例由若干个 chunk 组成,
一个 chunk 就代表一片连续的内存空间,里面包含了若干缓存页与其对应的控制块,如下图所示:
图片.png
上图代表的 Buffer Pool 由 2 个实例组成的,每个实例中又包含 2 个 chunk。
正是因为有了 chunk 的概念,我们在服务器运行期间调整 Buffer Pool 的大小时,就是以 chunk 为单位增加或者删除内存空间,而不需要重新向操作系统申请一片大的内存,然后进行缓存页的复制。
这个 chunk 的大小是我们在启动 MySQL 服务器时通过 innodb_buffer_pool_chunk_size 启动参数指定的,它的默认值是 134217728,也就是 128M。
不过需要注意的是,innodb_buffer_pool_chunk_size 的值只能在服务器启动时指定,在服务器运行过程中是不可以修改的。
innodb_buffer_pool_chunk_size 的值并不包含缓存页对应的控制块的内存空间大小,
所以实际上 InnoDB 向操作系统申请连续内存空间时,每个 chunk 的大小要比 innodb_buffer_pool_chunk_size 的值大一些,约 5%。

配置 Buffer Pool 的注意事项

  • innodb_buffer_pool_size 必须是 innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances 的倍数,这主要是为了保证:每一个 Buffer Pool 实例中包含的 chunk 数量相同)。
  • 如果我们指定的 innodb_buffer_pool_size 大于 2G 并且不是 2G 的整数倍,那么服务器会自动的把该值调整为 2G 的整数倍。
  • 如果在服务器启动时,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 的缓存页除了用来缓存磁盘上的页面以外,还可以存储锁信息、自适应哈希索引等信息,这些内容等之后遇到了再详细介绍。

    查看 Buffer Pool 的状态信息

    设计 MySQL 的人给我们提供了 show engine innodb status; 语句来查看关于 InnoDB 存储引擎运行过程中的一些状态信息,其中就包括 Buffer Pool 的一些信息,为了突出重点,下面只把输出中关于 Buffer Pool的部分提取了出来:
    图片.png图片.png
    图片.png ```sql (…省略前边的许多状态)

BUFFER POOL AND MEMORY

Total memory allocated 13218349056; Dictionary memory allocated 4014231 Buffer pool size 786432 Free buffers 8174 Database pages 710576 Old database pages 262143 Modified db pages 124941 Pending reads 0 Pending writes: LRU 0, flush list 0, single page 0 Pages made young 6195930012, not young 78247510485 108.18 youngs/s, 226.15 non-youngs/s Pages read 2748866728, created 29217873, written 4845680877 160.77 reads/s, 3.80 creates/s, 190.16 writes/s Buffer pool hit rate 956 / 1000, young-making rate 30 / 1000 not 605 / 1000 Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s LRU len: 710576, unzip_LRU len: 118

I/O sum[134264]:cur[144], unzip sum[16]:cur[0]

(…省略后边的许多状态) ``` 详细看一下每个值都代表什么意思
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 中的页面数量。
当准备从磁盘中加载某个页面时,会先为这个页面在 Buffer Pool 中分配一个缓存页以及对应的控制块,
然后把这个控制块添加到 LRU 的 old 区域的头部,但是这个时候真正的磁盘页并没有被加载进来,Pending reads 的值会跟着加 1。


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区域的头部了。
需要注意的是:这里统计的将页面移动到 young 区域的头部的次数,
不仅包含从 old 区域移动到 young 区域头部的次数,
还包括从 young 区域移动到 young 区域头部的次数。
访问某个 young 区域的节点,只要该节点在 young 区域的 1/4 后,就会把它移动到 young 区域的头部。


not young-making rate:表示在过去某段时间,平均访问 1000 次页面,有多少次访问没有使页面移动到 young 区域的头部。
需要注意的是,这里统计的没有将页面移动到 young 区域的头部次数不仅包含因为设置了innodb_old_blocks_time 系统变量而导致访问了 old 区域中的节点但没把它们移动到 young 区域的次数,
还包含因为该节点在 young 区域的前 1/4 处,而没有被移动到 young 区域头部的次数。


LRU len:代表 LRU 链表中节点的数量。


unzip_LRU:代表 unzip_LRU 链表中节点的数量。


I/O sum:最近 50s 读取磁盘页的总数。
I/O cur:现在正在读取的磁盘页数量。
I/O unzip sum:最近 50s 解压的页面数量。
I/O unzip cur:正在解压的页面数量。

总结

  1. 磁盘太慢,用内存作为缓存很有必要。
  2. Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空间,可以通过innodb_buffer_pool_size 参数来调整它的大小。
  3. Buffer Pool 向操作系统申请的连续内存由控制块和缓存页组成,每个控制块和缓存页都是一一对应的,在填充足够多的控制块和缓存页的组合后,Buffer Pool 剩余的空间可能产生不够填充一组控制块和缓存页的空间,这部分空间不能被使用,也被称为碎片。
  4. InnoDB 使用了许多链表来管理 Buffer Pool。
  5. free 链表中每一个节点都代表一个空闲的缓存页,在将磁盘中的页加载到 Buffer Pool 时,会从 free 链表中寻找空闲的缓存页。
  6. 为了快速定位某个页是否被加载到 Buffer Pool,使用表空间号 + 页号作为 key,缓存页作为 value,建立哈希表。
  7. 在 Buffer Pool 中被修改的页称为脏页,脏页并不是立即刷新,而是被加入到 flush 链表中,等待之后的某个时刻同步到磁盘上。
  8. LRU 链表分为 young 和 old 两个区域,可以通过 innodb_old_blocks_pct 来调节 old 区域所占的比例。首次从磁盘上加载到 Buffer Pool 的页会被放到 old 区域的头部,在 innodb_old_blocks_time 间隔时间内访问该页不会把它移动到 young 区域头部。在 Buffer Pool 没有可用的空闲缓存页时,会首先淘汰掉 old 区域的一些页。
  9. 我们可以通过指定 innodb_buffer_pool_instances 来控制 Buffer Pool 实例的个数,每个 Buffer Pool 实例中都有各自独立的链表,互不干扰。
  10. 自 MySQL 5.7.5 版本之后,可以在服务器运行过程中调整 Buffer Pool 大小。每个 Buffer Pool 实例由若干个 chunk 组成,每个 chun k的大小可以在服务器启动时通过启动参数调整。
  11. 可以用下边的命令查看 Buffer Pool 的状态信息:show engine innodb status;