一文读懂 InnoDB 缓冲池(buffer pool) 工作原理

缓冲池的用处

对于使用 InnoDB 作为存储引擎的表来说,不管是用于存储用户数据的索引,还是各种系统数据,都是以页的形式存放在表空间中的,而所谓的表空间只是 InnoDB 对文件系统上一个或几个实际文件的抽象,也就实际数据说到底还是存储在磁盘上的。

磁盘的速度很慢,怎么能配得上“快如闪电”的CPU 呢?

InnoDB 存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中。

也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。

缓冲池内部组成

缓冲池中默认的缓存页大小和在磁盘上默认的页大小是一样的,一般是16KB。

为了更好的管理这些在缓冲池中的缓存页,InnoDB为每一个缓存页都创建了一些所谓的控制信息。

这些控制信息包括该页所属的表空间编号、页号、缓存页在缓冲池中的地址、链表节点信息、一些锁信息。

缓冲池的一些参数:
SHOW VARIABLES LIKE 'innodb_buffer_pool%';
free 链表
当最初启动MySQL服务器的时候,此时并没有真实的磁盘页被缓存到缓冲池中,之后随着程序的运行,会不断的有磁盘上的页被缓存到缓冲池中。

从磁盘上读取一个页到缓冲池中的时候该放到哪个缓存页的位置呢?

思路:区分缓冲池中哪些缓存页是空闲的,哪些已经被使用了。

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

flush 链表

如果我们修改了缓冲池中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页(dirty page)。

最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上,但是频繁的往磁盘中写数据会严重的影响程序的性能。

所以,Innodb 创建了一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,在未来的某个时间点进行同步。这个链表叫做 flush 链表。

缓存不够的窘境

缓冲池对应的内存大小毕竟是有限的,如果需要缓存的页占用的内存大小超过了缓冲池大小,也就是已经没有多余的空闲缓存页的时候怎么办?

把某些旧的缓存页从缓冲池中移除,然后再把新的页放进来。

移除哪些缓存页?这就需要引入缓存淘汰机制了。

缓存淘汰机制

缓存淘汰有以下两个目的:

  • 实现淘汰
  • 使缓存命中率高

缓存淘汰机制比较常用的是用 LRU (Least recently used)算法。

传统LRU

LRU 的两种情况:

(1)页已经在缓冲池里,那就只做“移至”LRU头部的动作,而没有页被淘汰;

(2)页不在缓冲池里,除了做“放入”LRU头部的动作,还要做“淘汰”LRU尾部页的动作;

在 InnoDB 中,传统的 LRU 会遇到两个问题:

(1)预读失效;

(2)缓冲池污染;

什么是预读失效?

由于预读 (Read-Ahead),提前把页放入了缓冲池,但最终 MySQL 并没有从页中读取数据,称为预读失效。

如何对预读失效进行优化?

要优化预读失效,思路是:

(1)让预读失败的页,停留在缓冲池 LRU 里的时间尽可能短;

(2)让真正被读取的页,才挪到缓冲池 LRU 的头部;

以保证,真正被读取的热数据留在缓冲池里的时间尽可能长。

具体方法是:

(1)将LRU分为两个部分:

  • new 区(new sublist)
  • old 区(old sublist)

(2)两个区首尾相连,即:new 区的尾(tail)连接着 old 区的头(head);

(3)新页(例如被预读的页)加入缓冲池时,只加入到 old 区头部:
如果数据真正被读取(预读成功),才会加入到 new 区的头部
如果数据没有被读取,则会比 new 区里的“热数据页”更早被淘汰出缓冲池

改进版缓冲池LRU能够很好的解决“预读失败”的问题。

查看系统变量 innodb_old_blocks_pct 的值来确定old区域在LRU链表中所占的比例
SHOW VARIABLES LIKE 'innodb_old_blocks_pct';

什么是 MySQL 缓冲池污染?
当某一个SQL语句,要批量扫描大量数据时,可能导致把缓冲池的所有页都替换出去,导致大量热数据被换出,MySQL性能急剧下降,这种情况叫缓冲池污染。

例如,有一个数据量较大的用户表,当执行
select * from user where name like "%test%";

要优化缓冲池污染,思路是:

(1)不让批量扫描的大量数据进入到 new 区;

(2)让真正被读取的页,才挪到缓冲池 LRU 的头部;

具体实现:
加入了一个“old 区停留时间”的机制:
在 old 区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续再次访问的时间与第一次访问的时间在某个时间间隔内(即该缓存页在 old 区的存在时间在某个时间间隔内),那么该页面就不会被从old 区移动到 new 区的头部。

上述的全表扫描执行:

(1) 扫描过程中,需要新插入的数据页,都被放到old区

(2) 一个数据页会有多条记录,因此一个数据页会被访问多次

(3) 由于是顺序扫描,数据页的第一次被访问和最后一次被访问的时间间隔不会超过1S,因此还是会留在old区

(4) 继续扫描,之前的数据页再也不会被访问到,因此也不会被移到 new 区,最终很快被淘汰

这个间隔时间是由系统变量 innodb_old_blocks_time 控制的。

SHOW VARIABLES LIKE 'innodb_old_blocks_time';

配置缓冲池时的注意事项
innodb_buffer_pool_size
innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances 的倍数(这主要是想保证每一个 缓冲池 实例中包含的 chunk 数量相同)。

查看Buffer Pool的状态信息

SHOW ENGINE INNODB STATUS\G

一些参数如下:

Total memory allocated :代表 Buffer Pool 向操作系统申请的连续内存空间大小,包括全部控制块、缓存页、以及碎片的大小。

Buffer pool size:代表该 Buffer Pool 可以容纳多少缓存页,单位是页

Free buffers:代表当前 Buffer Pool 还有多少空闲缓存页,也就是 free 链表中还有多少个节点。

Database pages:代表 LRU 链表中的页的数量,包含 new 和 old 两个区域的节点数量。

Old database pages:代表 LRU 链表 old 区域的节点数量。

Modified db pages:代表脏页数量,也就是 flush 链表中节点的数量。

总结

1、磁盘太慢,用内存作为缓存很有必要。

2、缓冲池本质上是InnoDB向操作系统申请的一段连续的内存空间,可以通过innodb_buffer_pool_size 来调整它的大小。

3、InnoDB 使用了许多链表来管理缓冲池。

4、缓冲池的常见管理算法是 LRU

5、InnoDB 对普通 LRU 进行了优化:分为 new 区和 old 区,加入“停留时间”机制。

更多好文,公众号持续更新

file

阅读 474

推荐阅读
代码与自由
用户专栏

公众号:代码与自由。与你并行的后端精进之路

272 人关注
18 篇文章
专栏主页