什么是Buffer Pool
为了缓存磁盘中的页,
MySQL
服务器启动的时候就向操作系统申请了一片连续的内存,他们给这片内存起了个名,叫做Buffer Pool
(中文名是缓冲池
)。innodb_buffer_pool_size
参数的值,它表示Buffer Pool
的大小。
Buffer Pool内部组成
Buffer Pool
中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是16KB
。
为了更好的管理这些在Buffer Pool
中的缓存页,每一个缓存页都创建了一些所谓的控制信息
,这些控制信息包括该页所属的表空间编号、页号、缓存页在Buffer Pool
中的地址、链表节点信息、一些锁信息以及LSN
信息,称之为 控制块
,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中。
free链表的管理
- 申请和分配好
Buffer Pool
之后,如何使用和管理呢?从磁盘上读取一个页到Buffer Pool
中的时候该放到哪个缓存页的位置呢?或者说怎么区分Buffer Pool
中哪些缓存页是空闲的,哪些已经被使用了呢?
初始化的时候,我们可以把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表也可以被称作
free链表
(或者说空闲链表)如下图所示:
链表的基节点占用的内存空间并不包含在为Buffer Pool
申请的一大片连续内存空间之内,而是单独申请的一块内存空间。
每当需要从磁盘中加载一个页到Buffer Pool
中时,就从free链表
中取一个空闲的缓存页,并且把该缓存页对应的控制块
的信息填上(就是该页所在的表空间、页号之类的信息),然后把该缓存页对应的free链表
节点从链表中移除,表示该缓存页已经被使用了~
缓存页的哈希处理
- 当我们需要访问某个页中的数据时,就会把该页从磁盘加载到
Buffer Pool
中,如果该页已经在Buffer Pool
中的话直接使用就可以了。那么问题也就来了,我们怎么知道该页在不在Buffer Pool
中呢?难不成需要依次遍历Buffer Pool
中各个缓存页么?
我们可以根据
表空间号 + 页号
来定位一个页的,也就相当于表空间号 + 页号
是一个key
,缓存页
就是对应的value
,来构建一个hash表。
flush链表的管理
flush链表的由来
- 如果我们修改了
Buffer Pool
中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页
(英文名:dirty page
)。当然,最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上,但是频繁的往磁盘中写数据会严重的影响程序的性能。 - 如果不立即同步到磁盘的话,那之后再同步的时候我们怎么知道
Buffer Pool
中哪些页是脏页
,哪些页从来没被修改过呢?总不能把所有的缓存页都同步到磁盘上吧,假如Buffer Pool
被设置的很大,比方说300G
,那一次性同步这么多数据岂不是要慢死!所以,我们不得不再创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫flush链表
。
如下图所示:
LRU链表的管理
出现LRU链表的原因
当然是Buffer Pool
内存不够用了
Buffer Pool
对应的内存大小毕竟是有限的,如果需要缓存的页占用的内存大小超过了Buffer Pool
大小,也就是free链表
中已经没有多余的空闲缓存页,会淘汰最近最少使用的缓存页。
mysql中的LRU链表
简单的LRU链表
- 如果该页不在
Buffer Pool
中,在把该页从磁盘加载到Buffer Pool
中的缓存页时,就把该缓存页对应的控制块
作为节点塞到链表的头部。 - 如果该页已经缓存在
Buffer Pool
中,则直接把该页对应的控制块
移动到LRU链表
的头部。
划分区域的LRU链表
简单的LRU链表
用了没多长时间就发现问题了,因为存在这两种比较尴尬的情况:
-
InnoDB
提供了一个看起来比较贴心的服务——预读
(英文名:read ahead
)。所谓预读
,就是InnoDB
认为执行当前的请求可能之后会读取某些页面,就预先把它们加载到Buffer Pool
中。根据触发方式的不同,预读
又可以细分为下边两种:- 线性预读:
mysql
提供了一个系统变量innodb_read_ahead_threshold
,如果顺序访问了某个区(extent
)的页面超过这个系统变量的值,就会触发一次异步
读取下一个区中全部的页面到Buffer Pool
的请求 - 随机预读:如果
Buffer Pool
中已经缓存了某个区的13个连续的页面,不论这些页面是不是顺序读取的,都会触发一次异步
读取本区中所有其的页面到Buffer Pool
的请求。设计InnoDB
的大叔同时提供了innodb_random_read_ahead
系统变量,它的默认值为OFF
,也就意味着InnoDB
并不会默认开启随机预读的功能。
预读
本来是个好事儿,如果预读到Buffer Pool
中的页成功的被使用到,那就可以极大的提高语句执行的效率。可是如果用不到呢?这些预读的页都会放到LRU
链表的头部,但是如果此时Buffer Pool
的容量不太大而且很多预读的页面都没有用到的话,这就会导致处在LRU链表
尾部的一些缓存页会很快的被淘汰掉,也就是所谓的劣币驱逐良币
,会大大降低缓存命中率。 - 线性预读:
-
有大表的全表扫描语句出现
表中记录非常多的话,那该表会占用特别多的
页
,当需要访问这些页时,会把它们统统都加载到Buffer Pool
,后果可想而知。
基于以上两种情况,mysql
把这个LRU链表
按照一定比例分成两截,分别是:
- 一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做
热数据
,或者称young区域
。 - 另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做
冷数据
,或者称old区域
。
变量innodb_old_blocks_pct
的值来确定old
区域在LRU链表
中所占的比例,old
区域在LRU链表
中所占的比例是37%
。我们可以在启动时修改innodb_old_blocks_pct
参数来控制old
区域在LRU链表
中所占的比例:
[server]
innodb_old_blocks_pct = 40
有了这个被划分成young
和old
区域的LRU
链表之后,设计InnoDB
的大叔就可以针对我们上边提到的两种可能降低缓存命中率的情况进行优化了:
-
当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓存页时,该缓存页对应的控制块会被放到old区域的头部。这样针对预读到
Buffer Pool
却不进行后续访问的页面就会被逐渐从old
区域逐出,而不会影响young
区域中被使用比较频繁的缓存页。 -
针对全表扫描时,短时间内访问大量使用频率非常低的页面情况的优化
在进行全表扫描时,虽然首次被加载到
Buffer Pool
的页被放到了old
区域的头部,但是后续会被马上访问到,每次进行访问的时候又会把该页放到young
区域的头部,这样仍然会把那些使用频率比较高的页面给顶下去。所以我们只需要规定,在对某个处在
old
区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部。上述的这个间隔时间是由系统变量innodb_old_blocks_time
控制的,默认值是1000
,单位是毫秒。
更进一步优化LRU链表
我们每次访问一个缓存页就要把它移动到LRU链表
的头部,这样开销是不是太大啦,毕竟在young
区域的缓存页都是热点数据,也就是可能被经常访问的,这样频繁的对LRU链表
进行节点移动操作开销是很大的。
为了解决这个问题其实我们还可以提出一些优化策略,比如只有被访问的缓存页位于
young
区域的1/4
的后边,才会被移动到LRU链表
头部,这样就可以降低调整LRU链表
的频率,从而提升性能(也就是说如果某个缓存页对应的节点在young
区域的1/4
中,再次访问该缓存页时也不会将其移动到LRU
链表头部)。
刷新脏页到磁盘
后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。主要有两种刷新路径:
-
从
LRU链表
的冷数据中刷新一部分页面到磁盘。后台线程会定时从
LRU链表
尾部开始扫描一些页面,扫描的页面数量可以通过系统变量innodb_lru_scan_depth
来指定,如果从里边儿发现脏页,会把它们刷新到磁盘。这种刷新页面的方式被称之为BUF_FLUSH_LRU
。 -
从
flush链表
中刷新一部分页面到磁盘。后台线程也会定时从
flush链表
中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。这种刷新页面的方式被称之为BUF_FLUSH_LIST
。
多个Buffer Pool实例
在多线程环境下,访问Buffer Pool
中的各种链表都需要加锁处理啥的,在Buffer Pool
特别大而且多线程并发访问特别高的情况下,单一的Buffer Pool
可能会影响请求的处理速度。所以在Buffer Pool
特别大的时候,我们可以把它们拆分成若干个小的Buffer Pool
,每个Buffer Pool
都称为一个实例
,它们都是独立的,独立的去申请内存空间,独立的管理各种链表,独立的吧啦吧啦,所以在多线程并发访问时并不会相互影响,从而提高并发处理能力。
我们可以在服务器启动的时候通过设置innodb_buffer_pool_instances
的值来修改Buffer Pool
实例的个数,比方说这样:
[server]
innodb_buffer_pool_instances = 2
这样就表明我们要创建2个Buffer Pool
实例,示意图就是这样: