本文主要有以下内容:
- Buffer Pool 结构介绍
- Free、Flush、Lru 链表介绍
写下本文的原因主要是因为在总结事务相关知识的时候,提到了这一块的相关知识,本来打算 事务 + Buffer Pool 一起总结的,但是内容太多,就分成两篇!下周上事务相关的总结知识吧!
Buffer Pool
为什么需要 Buffer Pool:因为 CPU 和磁盘交互非常耗费时间,因此为了提高效率,会将数据页缓存在 Buffer Pool中,这样在继续访问相同的数据页时,便不会再从磁盘加载数据页。
Buffer Pool:是一片连续的内存空间,可以在启动MySQL时在 server
下通过此innodb_buffer_pool_size = 268435456
指定,单位是字节,用于缓存数据页。
Buffer Pool 的内部结构
在 Buffer Pool 中,每一页的大小和数据页的大小一致,默认为 16 kb,每一个缓存页由一个控制块所控制。实际结构如下图:
由于是一片连续的内存空间,因此很有可能出现一块内存大小不足以满足 一个页的 + 控制块的大小,自然而然这片区域就没法用到。因此就被称为碎片区。
Free 链表
为了更好的管理这些缓存页,我们就必须先知道当前缓存页的状态,如是否空闲,当前缓存页是否被修改等,因此设计了 Free 链表,可以帮我们快速定位当前缓存页中哪些缓存页还没有被使用,是可以拿来使用的!其结构如下:
- 链表基结点:记录了链表开始的位置、结束的位置以及链表中节点的数量。占据 40 个字节。不包含在 buffer pool 申请的内存空间里。
- 每个控制块大约占用缓存页大小的 5%,在 MySQL5.7.21 这个版本中,每个控制块占用的大小是 808 字节。而之前设置的 innodb_buffer_pool_size 并不包含这部分控制块占用的内存空间大小,这片连续的内存空间一般会比innodb_buffer_pool_size 的值大5%左右。
当需要使用一个缓存页时,从free链表
中取一个空闲的缓存页,并且把该缓存页对应的控制块
的信息填上(就是该页所在的表空间、页号之类的信息),然后把该缓存页对应的 free链表
节点从链表中移除。
在访问某一个页中的数据时,我们根据 表空间 + 页号
的方式来定位一个页。把 表空间 + 页号
作为 key 值,缓存页作为value进行哈希处理,这样就可以快速判断某一个页面是否在 buffer pool 中,如果没在则加载即可。
Flush 链表
加载到内存中的数据页,有可能在运行过程中其数据会发生修改,如update
操作,这样它所携带的数据就和磁盘文件中对应的数据不一致,这样的缓存页称之为 脏页。为了管理这些被修改的脏页,因此就有了 Flush 链表。其结构如下所示:
其链表节点和 Free 链表的基节点类似,记录了链表开始的位置、结束的位置以及链表中节点的数量。
LRU 链表
通过上面的描述,我们目前至少知道了在 buffer pool 中会存在两个链表,一个负责管理还可以使用的数据页,另一个负责被修改了的数据页。随着程序的不断运行,当 Free 链表中的缓存页耗尽时,该怎么办?
了解过 Redis 或者在大学期间学过操作系统的可能会想到如下解决方法:
- 该页不在
Buffer Pool
中,在把该页从磁盘加载到Buffer Pool
中的缓存页时,就把该缓存页对应的控制块
作为节点塞到链表的头部。 - 该页已经缓存在
Buffer Pool
中,则直接把该页对应的控制块
移动到LRU链表
的头部。
这样做好像可以,但如果我提出 预读服务 + 全表扫描
阁下该如何应对?
预读服务:
- 线性预读:设计
InnoDB
的大佬提供了一个系统变量innodb_read_ahead_threshold
,如果顺序访问了某个区(extent
)的页面超过这个系统变量的值,就会触发一次异步
读取下一个区中全部的页面到Buffer Pool
的请求,默认为 56 。- 随机预读:如果
Buffer Pool
中已经缓存了某个区的13个连续的页面,不论这些页面是不是顺序读取的,都会触发一次异步
读取本区中所有的其它页面到Buffer Pool
的请求。设计InnoDB
的大佬同时提供了innodb_random_read_ahead
系统变量,它的默认值为OFF
。
除此之外还有在执行全表扫描语句时,如果该表中的数据页很多,就有可能出现 buffer pool 中所有的数据页都被修改的情况,
上面所提到的情况,直接影响就是:
- 加载到
Buffer Pool
中的页不一定被用到。- 如果非常多的使用频率偏低的页被同时加载到
Buffer Pool
时,可能会把那些使用频率非常高的页从Buffer Pool
中淘汰掉。
综上即出现劣页驱逐良页的现象,出现这种情况就和我们设计 buffer pool 的初衷矛盾了!
划分区域的链表使用方法
为了更好的使用 buffer pool 中的各个缓存页,提高数据的访问速度,并采用了如下的方式组织 LRU 链表:这是第三条链表!
- 按照某个比例将LRU链表分成两半的,不是某些节点固定是young区域的,某些节点固定是old区域的,随着程序的运行,某个节点所属的区域也可能发生变化
- 默认情况下,
old
区域在LRU链表
中所占的比例是37%
,也就是说old
区域大约占LRU链表
的3/8
,在启动时可通过innodb_old_blocks_pct = 40
进行设置。- 热数据区:存储使用频率非常高的缓存页,所以这一部分链表也叫做
热数据
,或者称young区域
。- 冷数据区:存储使用频率不是很高的缓存页,所以这一部分链表也叫做
冷数据
,或者称old区域
。
采用这样的方式我们就可以减少劣页驱逐良页。且可以针对我们上面提到的两种可能降低缓存命中率的情况进行优化
- 针对预读的页面可能不进行后续访情况的优化:当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓存页时,该缓存页对应的控制块会被放到 old 区域的头部。这样针对预读到
Buffer Pool
却不进行后续访问的页面就会被逐渐从old
区域逐出,而不会影响young
区域中被使用比较频繁的缓存页。- 针对全表扫描时,短时间内访问大量使用频率非常低的页面情况的优化:在进行全表扫描时,虽然首次被加载到
Buffer Pool
的页被放到了old
区域的头部,对某个处在old
区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间。如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部。
预读优化过程:
上面所提到的方式有效的优化了缓存命中率降低的问题,另外我们还可以对热数据区域部分进行优化!
对于
young
区域的缓存页来说,我们每次访问一个缓存页就要把它移动到LRU链表
的头部,这样开销太大,毕竟在young
区域的缓存页都是热点数据,也就是可能被经常访问的,频繁的对LRU链表
进行节点移动操作不太好。我们可以采用:只有被访问的缓存页位于young
区域的1/4
的后边,才会被移动到LRU链表
头部,这样就可以降低调整LRU链表
的频率。
注:除了上面提到的链表,还有其他链表,这里就不提了
上面提到的内容中还有几件事没有落地:
- 对应的被修改的数据何时刷到磁盘?
- 多个 buffer Pool 会加载同一个数据页吗?
何时刷盘
MySQL 为了不影响用户的使用,后台有专门的线程负责将脏页刷盘:
- BUF_FLUSH_LRU:后台线程会定时从
LRU链表
尾部开始扫描一些页面,扫描的页面数量可以通过系统变量innodb_lru_scan_depth
来指定,如果从里边儿发现脏页,会把它们刷新到磁盘。 - BUF_FLUSH_LIST:后台线程也会定时从
flush链表
中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。 - BUF_FLUSH_SINGLE_PAGE:后台线程刷新脏页的进度比较慢,导致用户线程在准备加载一个磁盘页到
Buffer Pool
时没有可用的缓存页,这时就会尝试看看LRU链表
尾部有没有可以直接释放掉的未修改页面,如果没有的话会不得不将LRU链表
尾部的一个脏页同步刷新到磁盘。
你可能会疑惑脏页不是存在于 flush 链表吗?在 LRU 链表中为何会存在脏页?我承认阁下很强,倘如我拿出此图,阁下该如何应对?
缓冲池中的页不仅会被读取,还会被修改。修改的页发生在 LRU 链表中,当 LRU 链表中的页被修改后,会形成脏页。此时缓冲池中的页和磁盘上的页数据不一致。数据库就会将脏页刷新回磁盘。需要注意的是,脏页既存在于 LRU 链表中,也存在于 flush 链表中。LRU 链表用于管理缓冲池中页的可用性,flush 链表则用来管理将页刷新回磁盘,两者互不影响。
多个 BufferPool 实例
Buffer Pool
本质是InnoDB
向操作系统申请的一块连续的内存空间,在多线程环境下,访问Buffer Pool
中的各种链表都需要加锁处理什么的,在Buffer Pool
特别大而且多线程并发访问特别高的情况下,单一的Buffer Pool
可能会影响请求的处理速度。所以在Buffer Pool
特别大的时候,我们可以把它们拆分成若干个小的Buffer Pool
,每个Buffer Pool
都称为一个实例
,它们都是独立的,独立的去申请内存空间,独立的管理各种链表,独立的等等,所以在多线程并发访问时并不会相互影响,从而提高并发处理能力。在服务器启动的时候通过设置innodb_buffer_pool_instances
的值来修改Buffer Pool
实例的个数。
同一个数据页只会被分配到一个固定的 Buffer Pool 实例,不会被分配到别处!
另外:设计InnoDB
的大佬们规定:当innodb_buffer_pool_size的值小于1G的时候设置多个实例是无效的,InnoDB会默认把innodb_buffer_pool_instances 的值修改为1。
参考资料
- 从根上理解MySQL
- 从根上理解MySQL
- 从根上理解MySQL