玩命加载中 . . .

17-InnoDB的BufferPool


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

17.2.2 Buffer Pool内部组成

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

每一个缓存页都创建了一些所谓的控制信息,包括该页所属的表空间编号、页号、缓存页在 Buffer Pool 中的地址、链表节点信息等

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

17.2.3 free链表的管理

Buffer Pool 的初始化过程,就是先向操作系统申请 Buffer Pool 的内存空间,然后把它划分成若干对控制块和缓存页

把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表也可以被称作 free 链表

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

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

17.2.4 缓存页的哈希处理

可以根据表空间号 + 页号来定位一个页的,也就相当于表空间号 + 页号是一个key,缓存页就是对应的value

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

17.2.5 flush链表的管理

如果我们修改了 Buffer Pool 中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页

每次修改缓存页后,并不着急立即把修改同步到磁盘上,而是在未来的某个时间点进行同步

创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫 flush 链表

17.2.6 LRU链表的管理

2、简单的LRU链表

如果缓冲区满了,就需要按照一定规则淘汰一些很久没用过的页面,可以使用 LRU (Least Recently Used) 算法

创建一个 LRU 链表,按照最近最少使用的原则去淘汰缓存页

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

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

所以当 Buffer Pool 中的空闲缓存页使用完时,到 LRU 链表的尾部找些缓存页淘汰就可以了

3、划分区域的LRU链表

一些可能降低命中率的情况

  • 情况1:预读

预读,就是 InnoDB 认为执行当前的请求可能之后会读取某些页面,就预先把它们加载到 Buffer Pool 中

  1. 线性预读:如果顺序访问了某个区的页面超过系统变量innodb_read_ahead_threshold的值,就会触发一次异步读取下一个区中全部的页面到 Buffer Pool 的请求

  2. 随机预读:如果某个区的13个连续的页面都被加载到了 Buffer Pool 中,无论这些页面是不是顺序读取的,都会触发一次异步读取本区中所有其他页面到 Buffer Pool 中的请求

  • 情况2:全表扫描

全表扫描意味着将访问该表的聚簇索引的所有叶子节点对应的页,如果需要访问的页特别多,而 Buffer Pool 又不能容纳它们,就需要把其他语句在执行过程中用到的页面排挤出 Buffer Pool

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

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

为此,LRU 链表被按照一定比例分成两截

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

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

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

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

  • 针对全表扫描时,短时间内访问大量使用频率非常低的页面情况的优化:在对某个处在 old 区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从 old 区域移动到 young 区域的头部,否则就将它移动到 young 区域的头部

mysql> show variables like 'innodb_old_blocks_time';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000  |
+------------------------+-------+
1 row in set (0.03 sec)

这个innodb_old_blocks_time的默认值是1000,它的单位是毫秒,也就意味着对于从磁盘上被加载到 LRU 链表的 old 区域的某个页来说,如果第一次和最后一次访问该页面的时间间隔小于 1s ,那么该页是不会被加入到 young 区域的

4、更进一步优化LRU链表

只有被访问的缓存页位于 young 区域的 1/4 的后边,才会被移动到 LRU 链表头部

17.2.8 刷新脏页到磁盘

后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,刷新方式有两种

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

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

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

后台线程也会定时从 flush 链表中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。这种刷新页面的方式被称之为BUF_FLUSH_LIST

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

17.2.9 多个Buffer Pool实例

在 Buffer Pool 特别大的时候,我们可以把它们拆分成若干个小的 Buffer Pool,每个 Buffer Pool 都称为一个实例

17.2.10 innodb_buffer_pool_chunk_size

MySQL 5.7.5 以及之后的版本中,支持了在服务器运行过程中调整 Buffer Pool 大小的功能,但是有一个问题,就是每次当我们要重新调整 Buffer Pool 大小时,都需要重新向操作系统申请一块连续的内存空间,然后将旧的 Buffer Pool 中的内容复制到这一块新空间,这是极其耗时的。

所以不再一次性为某个 Buffer Pool 实例向操作系统申请一大片连续的内存空间,而是以一个所谓的 chunk 为单位向操作系统申请空间。也就是说一个 Buffer Pool 实例 其实是由若干个 chunk 组成的,一个 chunk 就代表一片连续的内存空间,里面包含了若干缓存页与其对应的控制块

这样在服务器运行期间调整 Buffer Pool 的大小时就是以 chunk 为单位增加或者删除内存空间,而不需要重新向操作系统申请一片大的内存,然后进行缓存页的复制操作


文章作者: kunpeng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kunpeng !
  目录