参考资料
https://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool.html
https://relph1119.github.io/mysql-learning-notes/#/mysql/
InnoDB 内存结构
内存结构如下图
查看InnoDB运行状态
SHOW ENGINE INNODB STATUS
通过上述命令查看InnoDB运行时的状态信息
Buffer Pool
buffer pool是主内存中的一个区域,在其中,InnoDB可以缓存访问到的表和索引数据。通过缓存访问频繁的数据页和索引页,可以加快数据的读、写性能。
InnoDB中所有数据页的读写操作都需要通过buffer pool进行
- Innodb 读操作,先从buffer_pool中查看数据的数据页是否存在,如果不存在,则将page从磁盘读取到buffer pool中;存在则直接从buffer pool中读取。
- Innodb 写操作,先把数据和日志写入 buffer pool 和 log buffer,再由后台线程以一定频率将 buffer 中的内容刷到磁盘。
缓冲池的大小是影响 InnoDB 性能的关键因素,在专用服务器上,通常会将高达80%的物理内存分配给缓冲池。较大的缓冲池可以容纳更多经常访问的数据,从而减少磁盘 I/O 并提高查询性能。通过参数innodb_buffer_pool_size控制,默认是128MB。
buffer pool内部组成
Buffer Pool 中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是 16KB 。为了更好的管理Buffer Pool 中的缓存页,每一个缓存页都有一个控制信息块 ,这些控制信息包括该页所属的表空间编号、页号、缓存页在 Buffer Pool 中的地址、链表节点信息、一些锁信息以及 LSN 信息。
Free 链表
从磁盘上加载数据页到buffer pool中时,应该如何进行内存分配呢?怎么知道buffer pool中,那些缓存页已经使用,那些缓存页是空闲可分配的?
这些工作由Free链表完成,Free链表负责记录buffer pool中可用的缓存页,这个过程由通过将缓存页的控制块作为Free链表中的一个节点完成。
从图中可以看出,Free链表中定义了一个基节点 ,包含着链表的头节点地址,尾节点地址,以及链表中节点的数量等信息。在刚刚初始化的buffer pool中,缓存页都是空闲的,缓存页对应的控制块都会被添加Free链表中。
Free链表创建完之后,每当需要从磁盘中加载一个页到Buffer Pool中时,就从free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上(该页所在的表空间、页号),然后把该缓存页对应的 free链表节点从链表中移除。
Flush链表
在进行更新操作时,只修改buffer pool中的缓存页,而不是直接落盘。修改后的缓存页叫做脏页,与磁盘上的数据不一致。后台线程会定期的把脏页刷新到磁盘上,这就需要先对脏页做一个管理,可以查找到buffer pool中的脏页。对应的管理就是Flush链表,每当缓存页被修改后,就将其对应的控制块作为一个节点加入Flush链表。
LRU链表
Buffer Pool 中内存是有限的,如果需要缓存的数据页占用的内存大小超过了 Buffer Pool 大小,也就 是 free链表 中已经没有多余的空闲缓存页。在这种时候,就需要将一些不常使用的缓存页进行回收,只保留经常使用的缓存页。
简单LRU链表
如何实现回收不常使用的数据,而保留最近使用的数据呢?那自然是创建一个LRU链表进行缓存页的管理了。
LRU链表处理逻辑为:
- 在将数据页从磁盘加载到 Buffer Pool中时,把该数据页对应的控制块作为节点放到LRU链表的头部。
- 如果数据页已经缓存在Buffer Pool中,每次访问时把数据页对应的控制块移动到LRU链表的头部。
也就是说,只要我们使用到某个缓存页,就把该缓存页调整到LRU链表的头部,这样LRU链表尾部就是最近最少使用的缓存页。当Buffer Pool中的空闲缓存页使用完时,到LRU链表的尾部找些缓存页淘汰即可释放内存。
分区域LRU
但如果只是简单LRU链表的话,在某些情况下,可能导致buffer pool中缓存命中率偏低。
例如,在进行全表扫描时,会将该表所有的数据页加载到buffer pool中,在数据足够时,会直接将buffer pool填满。并且这些数据全部被加载到LRU链表的头部,在进行回收时,会将buffer pool中之前的数据页全部被删除。这严重影响到了其他查询对buffer pool的使用,降低了缓存命中率。
于是乎,分区域的LRU来了。InnoDB中,将LRU链表分为两个区域:
- 一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做 热数据 ,或者称 young区域 。
- 一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做 冷数据 ,或者称 old区域 。
young区域和old区域按比例进行划分,默认情况下,old区域占整个LRU链表的37%。可以通过系统参数innodb_old_blocks_pct 进行配置。
在将LRU链表分区域后,优化处理逻辑如下
当磁盘上的数据页初次加载到Buffer Pool中时,该缓存页对应的控制块会被放到old区域的头部,并且在第一次访问该缓存页后,不会移动到young区域。
只有在对该缓存页进行两次及以上访问,并且访问间隔时间大于1s时,才将其放到young区域。
数据页哈希
如何判断数据页是否已经被加载到buffer pool中呢?我们知道一个数据页由表空间号和页号唯一标识,所以在buffer pool中可以通过唯一标识进行查找。
最终的方法是,通过数据页的表空间号和页号计算哈希值,然后哈希查找加快查找速度。
脏页的刷新
后台线程会定期的将buffer pool中的脏页刷新到磁盘上,这个操作叫做Checkpoint,主要有两种刷新路径:
- 从 Flush链表中刷新:后台线程定时从flush链表中刷新一部分页面到磁盘,刷新的速率取决于系统负载量。
- 从LRU链表中刷新:在通过LRU链表回收缓存页时,如果回收的缓存页是脏页数据,则将其刷新到磁盘。
多个buffer pool实例
Buffer Pool本质是InnoDB向操作系统申请的一块连续的内存空间,在并发环境下,更新Buffer Pool中的各种链表需要加锁进行并发控制。在 Buffer Pool 空间大且并发量高的情况下, 单一的 Buffer Pool 可能会影响请求的处理速度。
所以在空间足够的情况下,InnoDB默认有8个buffer pool实例,空间足够的条件是buffer pool内存大于1GB,否则只有一个实例。可以通过参数innodb_buffer_pool_instances指定buffer pool实例数量。
buffer pool实例之间是独立的,独立的申请内存空间、独立管理其中的链表信息等。
查看运行时的Buffer Pool状态
SHOW ENGINE INNODB STATUS
其中,BUFFER POOL相关的信息如下
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 13218349056;
Dictionary memory allocated 4014231
Buffer pool size 786432
Free buffers 8174
Database pages 710576
Old database pages 262143
Modified db pages 124941
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 6195930012, not young 78247510485
108.18 youngs/s, 226.15 non-youngs/s
Pages read 2748866728, created 29217873, written 4845680877
160.77 reads/s, 3.80 creates/s, 190.16 writes/s
Buffer pool hit rate 956 / 1000, young-making rate 30 / 1000 not 605 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 710576, unzip_LRU len: 118
I/O sum[134264]:cur[144], unzip sum[16]:cur[0]
字段含义解释
- Total memory allocated :代表 Buffer Pool 向操作系统申请的连续内存空间大小,包括全部控制块、缓
存页、以及碎片的大小。
- Dictionary memory allocated :为数据字典信息分配的内存空间大小,注意这个内存空间和 Buffer Pool
没啥关系,不包括在 Total memory allocated 中。
- Buffer pool size :代表该 Buffer Pool 可以容纳多少缓存 页 ,注意,单位是 页 !
- Free buffers :代表当前 Buffer Pool 还有多少空闲缓存页,也就是 free链表 中还有多少个节点。
- Database pages :代表 LRU 链表中的页的数量,包含 young 和 old 两个区域的节点数量。
- Old database pages :代表 LRU 链表 old 区域的节点数量。
- Modified db pages :代表脏页数量,也就是 flush链表 中节点的数量。
- Pending reads :正在等待从磁盘上加载到 Buffer Pool 中的页面数量。当准备从磁盘中加载某个页面时,会先为这个页面在 Buffer Pool 中分配一个缓存页以及它对应的控制块,然后把这个控制块添加到 LRU 的 old 区域的头部,但是这个时候真正的磁盘页并没有被加载进来, Pending reads 的值会跟着加1。
- Pending writes LRU :即将从 LRU 链表中刷新到磁盘中的页面数量。
- Pending writes flush list :即将从 flush 链表中刷新到磁盘中的页面数量。
- Pending writes single page :即将以单个页面的形式刷新到磁盘中的页面数量。
- Pages made young :代表 LRU 链表中曾经从 old 区域移动到 young 区域头部的节点数量。这里需要注意,一个节点每次只有从 old 区域移动到 young 区域头部时才会将 Pages made young 的值加1,也就是说如果该节点本来就在 young 区域,由于它符合在 young 区域1/4后边的要求,下一次访问这个页面时也会将它移动到 young 区域头部,但这个过程并不会导致 Pages made young 的值加1。
- Page made not young :在将 innodb_old_blocks_time 设置的值大于0时,首次访问或者后续访问某个处在 old 区域的节点时由于不符合时间间隔的限制而不能将其移动到 young 区域头部时, Page made not young 的值会加1。这里需要注意,对于处在 young 区域的节点,如果由于它在 young 区域的1/4处而导致它没有被移动到 young 区域头部,这样的访问并不会将 Page made not young 的值加1。
- youngs/s :代表每秒从 old 区域被移动到 young 区域头部的节点数量。
- non-youngs/s :代表每秒由于不满足时间限制而不能从 old 区域移动到 young 区域头部的节点数量。
- Pages read 、 created 、 written :代表读取,创建,写入了多少页。后边跟着读取、创建、写入的速率。
- Buffer pool hit rate :表示在过去某段时间,平均访问1000次页面,有多少次该页面已经被缓存到Buffer Pool 了。
- young-making rate :表示在过去某段时间,平均访问1000次页面,有多少次访问使页面移动到 young 区域的头部了。需要大家注意的一点是,这里统计的将页面移动到 young 区域的头部次数不仅仅包含从 old 区域移动到young 区域头部的次数,还包括从 young 区域移动到 young 区域头部的次数(访问某个 young 区域的节点,只要该节点在 young 区域的1/4处往后,就会把它移动到 young 区域的头部)。not (young-making rate) :表示在过去某段时间,平均访问1000次页面,有多少次访问没有使页面移动到 young 区域的头部。需要注意的一点是,这里统计的没有将页面移动到 young 区域的头部次数不仅仅包含因为设置了innodb_old_blocks_time 系统变量而导致访问了 old 区域中的节点但没把它们移动到 young 区域的次数,还包含因为该节点在 young 区域的前1/4处而没有被移动到 young 区域头部的次数。
- LRU len :代表 LRU链表 中节点的数量。
- unzip_LRU :代表 unzip_LRU链表 中节点的数量(由于我们没有具体唠叨过这个链表,现在可以忽略它的值)。
- I/O sum :最近50s读取磁盘页的总数。
- I/O cur :现在正在读取的磁盘页数量。
- I/O unzip sum :最近50s解压的页面数量。
- I/O unzip cur :正在解压的页面数量。
Change Buffer
change buffer是一种特殊的数据结构,专用于提高数据新增和更新时的性能,是buffer pool中的一部分。
change buffer默认为buffer pool大小的25%,最大可配置为50%。可以通过innodb_change_buffer_max_size配置
- 当数据库主要用于查询时,减少change buffer的大小
- 当数据库中新增、更新较多时,增加change buffer的大小
https://dev.mysql.com/doc/refman/5.7/en/innodb-change-buffer.html
适用场景
在事务中,存在对二级非唯一索引数据的新增、更新时
- 如果对应的数据页在buffer pool中,直接更新buffer pool中的数据页。
- 如果对应的数据页不在buffer pool中,将更改信息写入change buffer,信息为在某某数据页(通过表空间号和页号)对某行记录进行了某某更改。后续处理逻辑为
- 其它读取请求将对应的数据页加载到buffer pool时,将change buffer中的更改合并到其中,然后该数据页被标记为脏页,等待后续落盘。
- 后台刷新,purge操作会在change buffer空间不足、系统空闲或者缓慢关闭时,加载相应的二级索引页到buffer pool中,然后将change buffer中的更改合并到buffer pool中。
为什么是二级索引(非唯一)?
二级索引在进行新增、更新时,索引值的查询位置往往是随机的。而这可能会引起索引中记录的移动(因为索引结构是颗B+树,需要保证平衡),从而导致更多的磁盘IO访问。
而要提升数据库的处理速度和并发性能,就得尽量减少磁盘IO访问。通过change buffer进行延时更新,便可以达到提高处理速度和并发性能。
为什么不是唯一索引?
因为主键和唯一索引有唯一性约束,需要将数据加载到内存中进行唯一性校验,不适用于change buffer。
Log Buffer
Log buffer是InnoDB内存中的一个区域,专门用于管理redo log。默认大小是16MB,可以通过innodb_log_buffer_size进行配置。更大的log buffer可以保证在一个大事务中不需要在事务提交之前将redo log写入磁盘上。如果有大量的update、insert或者delete操作,应该增加log buffer的大小。
标签:缓存,buffer,young,链表,InnoDB,内存,LRU,pool,结构 From: https://www.cnblogs.com/cd-along/p/18107315