你好,我是田哥
在上一篇文章:一条查询SQL是如何执行的?更新、新增、删除呢?
我们聊到了Buffer Pool,很多朋友估计还是不是很了解,本文咱们就来聊聊。
对于innoDB存储引擎来说,数据是存储在磁盘上,而执行引擎想要操作数据,必须先将磁盘的数据加载到内存中才能操作。当数据从磁盘中取出后,缓存内存中,下次查询同样的数据的时候,直接从内存中读取,这样大大提高了查询性能。
MySQL InnoDB Architecture的体系结构图在这里:Mysql5.7版本官方InnoDB结构图,虽然是英文的,但是解释的最明白的往往是官方文档。
内存结构(In-Memory Structures)主要是针对的是数据及其操作,主要分为:
磁盘结构(On-Disk Structures)主要针对的是表和表空间,主要分为以下结构:
先看波关于Buffer Pool的概念:
Buffer Pool是MySQL服务在启动的时候向操作系统申请的一片连续地址的内存空间,其本质就是一片内存,默认大小是 128M,可以在启动服务的时候,通过 innodb_buffer_pool 这个参数设置buffer pool的大小,单位是字节(B),最小值是5MB。
那么Buffer Pool这段内存地址到底有什么,可以确定的就是肯定有16KB数据页,这里叫缓冲页。除此之外还有,索引页,undo 页,插入缓存、自适应哈希索引、锁信息
因为buffer pool被划分为某干个数据页,其数据页大小和表空间使用的页大小一致,为了更好的管理buffer pool中的缓冲页,innoDB为每个缓冲页都创建了一个控制信息。
这些控制信息主要包括该缓冲页的【表空间编号、页号、缓冲页在buffer pool中的地址、链表节点信息】,存储这些控制信息控制块。
缓冲页和控制块是一一对应的,其中控制块在buffer pool前面,而缓冲页在buffer后面。
什么是碎片?
当剩余空间不够一对控制块和缓冲页的大小时,这样的空间称为碎片
怎么查看MySQL实例的Buffer Pool信息呢?
show variables like '%innodb_buffer_pool_size%'; 查看buffer pool的size
show global status like '%innodb_buffer_pool%'; 查看相关参数,详细的参数代表的意思,大家自己去搜搜。
Buffer Pool 中的页有三种状态:
接下来我们分别看看三种链表是如何进行管理的。
初始化完的buffer pool时所有的页都是空闲页,所有空闲的缓冲页对应的控制块信息作为一个节点放到Free链表中。
要注意Free链表是一个个控制块,而控制块的信息中有缓存页的地址信息。
在有了free链表之后,当需要加载磁盘中的页到buffer pool中时,就去free链表中取一个空闲页所对应的控制块信息,根据控制块信息中的表空间号、页号找到buffer pool里对应的缓冲页,再将数据加载到该缓冲页中,随后删掉free链表该控制块信息对应的节点。
如何在buffer pool中快速查找缓冲页(数据页)呢?
这里就可以对缓冲页进行Hash处理,用表空间号、页号做为Key,缓冲页的控制块就是value维护一个Hash表,根据表空间号、页号做为Key去查找有没有对应的缓冲信息,如果没有就需要去free 链表中取一个空闲的缓冲页控制快信息,随后将磁盘中的数据加载到该缓冲页位置。
修改了buffer pool中缓冲页的数据,那么该页和磁盘就不一致了,这样的页就称为【脏页】,它不是立马刷入到磁盘中,而是由后台线程将脏页写入到磁盘。
Flush链表就是为了能知道哪些是脏页而设计的,它跟Free链表结构图相似,区别在于控制块指向的是脏页地址。
对于频繁访问的数据和很少访问的数据我们对与它的期望是不一样的,很少访问的数据希望在某个时机淘汰掉,避免占用buffer pool的空间,因为缓冲空间大小是有限的。
MySQL设计了根据LRU算法设计了LRU链表来维护和淘汰缓冲页。
LRU 算法简单来说,如果用链表来实现,将最近命中(加载)的数据页移在头部,未使用的向后偏移,直至移除链表。这样的淘汰算法就叫做 LRU 算法,但是简单的LRU算法会带来两个问题:预读失效、Buffer Pool污染
预读机制:当数据页从磁盘加载到 Buffer Pool 中时,会把相邻的数据页也提前加载到 Buffer Pool 中,这样做的好处就是减少未来可能的磁盘IO。
预读失效:当预读机制提前加载的数据页一直未被访问,这就是失效
好,那么结合简单的LRU算法来看,可能预读页呗加载到LRU链表头部,当Buffer Pool空间不够时,会把经常访问的位于LRU链表的尾部数据页给淘汰清理掉,这样缓冲就失效了。
Buffer Pool的LRU算法中InnoDB 将LRU链表按照5:3的比例分成了young区域和old区域。链表头部的5/8是young区(被高频访问数据),链表尾部的3/8区域是old区域(低频访问数据),箭头朝下的是未被访问的数据,朝上的是被访问的数据。
这样做的目的是,在预读的时候或访问不存在的缓冲页时,先加入到 old 区域的头部,当页被真正访问的时候,才将页插入 young 区域的头部。
我们来看下图:
我们已经默认情况下**innodb_buffer_pool_size是128M, **此时的innodb_buffer_pool_instances的大小也就是实例是1个。因为innodb_buffer_pool_size 小于1G时,设置innodb_buffer_pool_instances是无效的,都会是1。
当一个buffer pool在多线程访问的时候,各个链表都会加锁处理,这样一来,多线程访问时,性能就会降低。
可以通过innodb_buffer_pool_instances参数来设置实例的个数。每个buffer pool实例的大小计算公式:innodb_buffer_pool_size / innodb_buffer_pool_instances,每个实例都有其对应的链表管理,互不干扰。
如何修改运行中MySQL的Buffer Pool的大小?
MySQL 5.7.5之前:是不允许在运行时调整buffer pool大小的,只能在服务器启动之前,通过innodb_buffer_pool_size大小来调整。
MySQL 5.7.5之后:是以chunk为单位来修改Buffer Pool的大小,比如innodb_buffer_pool_chunk_size默认大小是128M,调整Buffer Pool大小就以chunk为单位来增加或减少Buffer Pool大小。
我们应该要有这么一个概念就是:一个Buffer Pool可能有多个buffer pool实例,而每个实例由多个chunk组成,一个chunk是一块连续的内存空间,一个chunk默认大小是128M。
如下图,这样理解是不是就很清晰啦?
磁盘太慢, Buffer Pool本质上是向操作系统申请一块连续的内存空间作为缓冲区。
缓冲区由控制块和缓冲页组成,两者是一一对应的关系,而碎片是指不足以填充一组控制块和缓冲页的内存空间。
Buffer Pool使用Free List链表管理空闲页、LRU List链表管理正常页、Flush List链表管理脏页(被修改的页),而脏页并不是立即刷新的,而是先加入Flush List,而后再刷到磁盘中。
通过改进LRU算法,LRU链表分为young区域和old区域,预读的缓冲页,先也放到old区域的head头部。
可以根据Buffer Pool的大小来这是多实例,Buffer Pool小于1G,默认是1个实例。
MySQL 5.7.5之后可以在运行时修改 Buffer Pool大小,主要是通过chunk来调整。