MySQL(五):InnoDB 缓冲池(Buffer Pool)

1、简述

InnoDB 存储引擎是基于磁盘存储的,对数据记录的管理是以为单位的。由于CPU与磁盘之间在速度上的巨大差距,那么缓冲池就应运而生了,它的存在提高数据库的整体性能。

InnoDB 缓冲池是用于在内存中缓存数据和索引的存储区。与InnoDB Redo Log一起使用,并将数据页保留在磁盘上,它们构成了I / O的低层抽象:可以将数据视为页面,其中每个页面都通过其表空间ID和页面ID以及InnoDB进行标识可以以原子方式加载,修改和存储此类页面。在此抽象之上,才构建各种主索引和辅助索引的更复杂的结构,这些结构使用这些页面来存储树的节点。

2、数据页操作逻辑

2.1、读取页操作

在数据库中读取页的操作,首先将从磁盘读取的页存放在缓冲池中。下一次再读取数据页时,先判断该数据页是否在缓冲池中,如果在缓冲池中,会直接读取该数据页,否则,读取磁盘上的页。

2.2、修改页操作

对数据库中页的修改操作,会首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。这里有一点需要注意,页从缓冲池刷新到磁盘的操作并不是在每次页发生更新时触发的,而是通过 Checkpoint 机制刷新到磁盘。

在这里插入图片描述

综上所述,缓冲池的大小直接影响了数据库的整体性能。随着内存技术的成熟,内存成本也在不断下降,因此强烈建议在数据库专用服务器上,将尽可能多的物理内存分配给缓冲池。

3、缓冲池中数据页类型

缓冲池中不是只有缓冲数据页和缓存索引页,只是这部分占缓冲池很大的一部分而已。缓冲池中缓存的数据页类型有:
索引页、数据页、undo页、插入缓冲(insert buffer)、自适应哈希索引(adaptive hash index)、InnoDB存储的锁信息(lock info)、数据字典信息(data dictionary)等。
在这里插入图片描述

4、缓冲池组件

4.1、缓冲池实例(Buffer Pool Instance)

在每一个Buffer Pool Instance中,实际都会维护一个自己的Buffer Pool模块,InnoDB通过16KB Page的方式将数据从文件中读取到Buffer Pool中,并通过一个LRU List来缓存这些Page,经常访问的Page在LRU List的前面,不经常访问的Page在后面。InnoDB访问一个Page时,首先会从Buffer Pool中获取,如果未找到,则会访问数据文件,读取到Page,并将其put到LRU List中,当一个Instance的Buffer Pool中没有可用的空闲Page时,会对LRU List中的Page进行淘汰。

InnoDB启动时会加载配置srv_buf_pool_size和srv_buf_pool_instances,分别是Buffer Pool总大小和需要划分的Instance数量,当srv_buf_pool_size小于1G时,srv_buf_pool_instances会被重置为1,单个Buffer Pool Instance的大小计算规则为:size=srv_buf_pool_size/srv_buf_pool_instances,每个Buffer Pool Instance的大小均相等。在Mysql 8.0中,最大支持64个Buffer Pool Instance,实际Instance在初始化时,为了加快分配速度,会根据运行环境进行调整并行初始化的数量,详细流程见Buffer Pool初始化。

在每个Buffer Pool Instance中都有包含自己的锁,mutex,Buffer chunks,各个页链表,每个Instance之间都是独立的,支持多线程并发访问,且一个page只会被存放在一个固定的Instance中,后续会详细介绍这个算法。

在每个Buffer Pool Instance中还包含一个page_hash的hash table,通过这个page_hash能快速找到LRU List中的page,避免扫描整个LRU List,极大提升了Page的访问效率。

可以通过参数 innodb_buffer_pool_instances 对缓冲池实例进行配置,默认值是1.

mysql> SHOW VARIABLES LIKE 'innodb_buffer_pool_instances'
    -> ;
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| innodb_buffer_pool_instances | 1     |
+------------------------------+-------+
1 row in set (0.01 sec)

4.2、缓冲块(Buffer chunks)

Buffer chunks是每个Buffer Pool Instance中实际的物理存储块数组,一个Buffer Pool Instance中有一个或多个chunk,每个chunk的大小默认为128MB,最小为1MB,且这个值在8.0中时可以动态调整生效的。每个Buffer chunk中包含一个buf_block_t的blocks数组(即Page),Buffer chunk主要存储数据页和数据页控制体,blocks数组中的每个buf_block_t是一个数据页控制体,其中包含了一个指向具体数据页的*frame指针,以及具体的控制体buf_page_t,后面在数据结构中详细阐述他们的关系。

4.3、页链表

以下所有的链表中的每个节点都是数据页控制体(buf_page_t)

1、Free List: Free List中存放的都是未曾使用的空闲Page,InnoDB需要Page时从Free List中获取,如果Free List为空,即没有任何空闲Page,则会从LRU List和Flush List中通过淘汰旧Page和Flush脏Page来回收Page。在InnoDB初始化时,会将Buffer chunks中的所有Page加入到Free List中。

2、LRU List: 所有从数据文件中新读取进来的Page都会缓存在LRU List,并通过LRU策略对这些Page进行管理。LRU List实际划分为Young和Old两个部分,其中Young区保存的是较热的数据,Old区保存的是刚从数据文件中读取出来的数据,如果LRU List的长度小于512,则不会将其拆分为Young和Old区。当InnoDB读取Page时,首先会从当前Buffer Pool Instance的page_hash查找,并分为三种情况来处理:

如果在page_hash找到,即Page在LRU List中,则会判断Page是在Old区还是Young区,如果是在Old区,在读取完Page后会把它添加到Young区的链表头部
如果在page_hash找到,并且Page在Young区,需要判断Page所在Young区的位置,只有Page处于Young区总长度大约1/4的位置之后,才会将其添加到Young区的链表头部
如果未能在page_hash找到,则需要去数据文件中读取Page,并将其添加到Old区的头部
LRU List采用非常精细的LRU淘汰策略来管理Page,并且用以上机制避免了频繁对LRU 链表的调整。

3、Flush List: 所有被修改过且还没来得及被flush到磁盘上的Page(脏页),都会被保存在这个链表中。所有保存在Flush List上的数据都会在LRU List中,但在LRU List中的数据不一定都在Flush List中。在Flush List上的每个Page都会保存其最早修改的lsn,即oldest_modification,虽然一个Page可能被修改多次,但只记录最早的修改。Flush List上的Page会按照其各自的oldest_modification进行降序排序,链表尾部保存oldest_modification最小的Page,在需要从Flush List中回收Page时,从尾部开始回收。

4.4、Mutex

为保证各个页链表访问时的互斥,Buffer Pool中提供了对几个List的Mutex,如LRU_list_mutex用来保护LRU List的访问,free_list_mutex用来保护Free List的访问,flush_list_mutex用来保护Flush List的访问。

4.5、Page_hash

在每个Buffer Pool Instance中都会包含一个独立的Page_hash,其作用主要是为了避免对LRU List的全链表扫描,通过使用space_id和page_no就能快速找到已经被读入Buffer Pool的Page。

5、缓冲池LRU算法

为了提高大容量读取操作的效率,缓冲池分为多个页面,这些页面可以容纳多行数据。为了提高缓存管理的效率,缓冲池被实现为页面的链接列表。使用LRU算法的变体将很少使用的数据从缓存中淘汰掉。

普通的LRU(Latest Recent Used,最近最少使用)算法是,频繁访问的页在LRU列表的前端,很少使用的页在LRU列表的尾端。当缓冲池不能存放新读取的页时,将优先释放LRU列表尾端的页。

在InnoDB存储引擎中,LRU 列表中加入了 midpoint 位置。最新访问的页并不直接放入LRU列表的头部,而是放入LRU列表的midpoint位置。这个算法在InnoDB存储引擎中称为 midpoint insertion strategy(中点插入策略)。默认配置中,该位置在LRU列表长度的 5/8 处。midpoint 位置可以由参数 innodb_old_blocks_pct 控制。

mysql> show variables like 'innodb_old_blocks_pct';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37    |
+-----------------------+-------+
1 row in set (0.01 sec)

由上示例看到,参数 innodb_old_blocks_pct 的默认值是 37,表示新读取的页插入到LRU列表尾端的37%的位置(大概3/8位置)。

在InnoDB存储引擎中,midpoint 之后的列表称为old列表,之前的列表称为new列表。

在这里插入图片描述

该算法将大量页面保留在新的子列表中。旧的子列表包含较少使用的页面;这些页面是驱逐对象。

  • 默认情况下,该算法的运行方式如下:

  • 3/8的缓冲池专用于旧的子列表。

  • 列表的中点是新子列表的尾部与旧子列表的头相交的边界。

  • 当InnoDB将页面读入缓冲池时,它首先将其插入中点(旧子列表的头部)。

  • 访问旧子列表中的页面会使其“年轻”,并将其移至新子列表的开头。如果由于用户启动的操作而需要读取页面,则立即进行首次访问,并使页面年轻。如果由于预读操作而读取了该页面,则第一次访问不会立即发生,并且在退出该页面之前可能根本不会发生。

  • 随着数据库的运行,通过移至列表的末尾,缓冲池中未被访问的页面将“老化”。新的和旧的子列表中的页面都会随着其他页面的更新而老化。随着将页面插入中点,旧子列表中的页面也会老化。最终,未使用的页面到达旧子列表的尾部并被逐出。

默认情况下,查询读取的页面会立即移入新的子列表,这意味着它们在缓冲池中的停留时间更长。例如,针对mysqldump操作或不带WHERE子句的SELECT语句执行的表扫描会将大量数据带入缓冲池,并驱逐相当数量的旧数据。同样,由预读后台线程加载且仅访问一次的页面将移至新列表的开头。这些情况可能会将常用页面推到旧的子列表,在此它们会被逐出。

6、缓冲池配置

可以通过配置缓冲池的各个指标值以提高性能:

1、理想情况下,可以将缓冲池的大小设置为与实际一样大的值,从而为服务器上的其他进程留出足够的内存以运行而不会进行过多的分页。缓冲池越大,InnoDB越像内存数据库,从磁盘读取一次数据,然后在后续读取期间从内存访问数据。参见: Configuring InnoDB Buffer Pool Size

2、在具有足够内存的64位系统上,可以将缓冲池分成多个部分,以最大程度地减少并发操作之间的内存结构争用。通俗点说就是增加缓冲池实例的数量。参见:Configuring Multiple Buffer Pool Instances

3、可以将频繁访问的数据保留在内存中,而不必考虑操作突然导致的活动高峰,这些操作会将大量不经常访问的数据带入缓冲池。参见: Making the Buffer Pool Scan Resistant

4、可以控制何时以及如何执行预读请求,以异步方式将页面预取到缓冲池中,从而预期很快将需要这些页面。参见:Configuring InnoDB Buffer Pool Prefetching (Read-Ahead)

5、可以控制何时进行后台flush,以及是否根据工作负荷动态调整flush速率。参见:Configuring Buffer Pool Flushing

6、可以配置InnoDB如何保留当前的缓冲池状态,以避免服务器重启后的漫长的预热时间。参见:Saving and Restoring the Buffer Pool State

7、使用InnoDB标准监视器监视缓冲池

可以使用 SHOW ENGINE INNODB STATUS 访问 InnoDB Standard Monitor 输出提供有关缓冲池操作的度量。缓冲池指标位于InnoDB Standard Monitor输出的BUFFER POOL AND MEMORY部分中,如下内容:

----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 2198863872
Dictionary memory allocated 776332
Buffer pool size   131072
Free buffers       124908
Database pages     5720
Old database pages 2071
Modified db pages  910
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 4, not young 0
0.10 youngs/s, 0.00 non-youngs/s
Pages read 197, created 5523, written 5060
0.00 reads/s, 190.89 creates/s, 244.94 writes/s
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not
0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read
ahead 0.00/s
LRU len: 5720, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]

InnoDB Standard Monitor 输出的不是当前 INNODB 存储引擎的状态,而是过去某个时间范围内INNODB 存储引擎的状态。

7.1、InnoDB缓冲池指标

在这里插入图片描述

注意事项:

1、该youngs/s指标仅适用于旧页面。它基于对页面的访问次数而不是页面数。可以对给定页面进行多次访问,所有访问都计入在内。如果youngs/s在不进行大扫描时看到非常低的值,则可能需要减少延迟时间或增加用于旧子列表的缓冲池的百分比。增加百分比会使旧的子列表变大,因此该子列表中的页面需要更长的时间才能移到尾部,这增加了再次访问这些页面并使它们变年轻的可能性。

2、该non-youngs/s指标仅适用于旧页面。它基于对页面的访问次数而不是页面数。可以对给定页面进行多次访问,所有访问都计入在内。如果non-youngs/s在执行大表扫描时看不到较高的值(较高的youngs/s 值),请增加延迟值。

3、该young-making比率说明了对所有缓冲池页面的访问,而不仅仅是访问了旧子列表中的页面。该young-making速率和 not速率通常不会加总到整个缓冲池的命中率。旧子列表中的页面命中会导致页面移动到新的子列表,但是新子列表中的页面命中只会导致页面与列表的头部保持一定距离时才移动到列表的头部。

4、not (young-making rate)是由于innodb_old_blocks_time未满足所定义的延迟,或者由于新子列表中的页面命中并未导致页面移动到头部而导致页面访问未使页面年轻化的平均命中率 。此速率说明了对所有缓冲池页面的访问,而不仅仅是访问旧子列表中的页面。

2、参考文献

  1. 《高性能MySQL(第3版)》
  2. 《MySQL技术内幕:InnoDB存储引擎(第2版)》
  3. 《MySQL源码库》
  4. 《MySQL参考手册》
  5. 《MySQL实战45讲》
  6. 《MySQL 是怎样运行的:从根儿上理解 MySQL》
  7. 《数据库内核月报》
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值