InnoDB学习笔记二缓冲池(buffer pool)

文章目录

    • 一、buffer pool
      • 1. 缓冲池(buffer pool)
      • 2. Buffer Pool 高并发场景
      • 3. 调整 Buffer Pool
        • 修改配置调整
        • chunk 机制调整
      • 4. Buffer Pool内存
        • 如何设置 Buffer Pool 的总内存大小
        • 如何设置 Buffer Pool 的实例数
      • 5. Buffer Pool 的初始化
    • 二、buffer pool的缓存结构
      • 1. LRU list
        • 实现方式
        • 设计好处
        • unZip_LRU
      • 2、Free list
        • 结构
        • 使用流程
      • 3. Flush list
        • 脏页(dirty page)
        • 结构
    • 各种 List 关系
    • 总结

一、buffer pool

1. 缓冲池(buffer pool)

背景

InnoDB是基于磁盘存储的,并将其中的数据按页的方式进行管理。因此InnoDB可视为基于磁盘的数据库系统。由于CPU的速度和磁盘IO速度的巨大鸿沟,需要缓冲池(buffer pool)来提高数据库的整体性能

作用

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

架构

简要架构图,在 Buffer Pool 中,是以数据页为数据单位叫做缓存页

InnoDB学习笔记二缓冲池(buffer pool)_第1张图片

原理

  • 第一次读取数据时,首先从磁盘中读取数据页,并放到(FIX)缓冲池中。
  • 再次读取同样的数据时,先看缓冲池中是否有相同的数据页。有则命中,从缓冲池中读取。否则从磁盘读取
mysql> show variables like 'innodb_buffer_pool_size';
+-------------------------+---------+
| Variable_name           | Value   |
+-------------------------+---------+
| innodb_buffer_pool_size | 8388608 |
+-------------------------+---------+
1 row in set (0.02 sec)

缓冲池中缓存的数据页类型

  • 索引页
  • 数据页
  • undo页
  • 插入缓冲(Insert buffer)
  • 自适应哈希索引
  • 锁信息
  • 数据字典信息(data dictionary)

缓存页数据结构

缓存页都会对应着一个描述数据块,里面包含数据页所属的表空间、数据页的编号,缓存页在 Buffer Pool 中的地址等等。

描述数据块本身也是一块数据,它的大小大概是缓存页大小的5%左右,大概800个字节左右的大小。

描述如图所示:

InnoDB学习笔记二缓冲池(buffer pool)_第2张图片

缓冲池实例

即缓冲池的个数。每页根据哈希值分配到不同缓冲池实例减少资源竞争、支持更大的并发处理,加快查询速度

mysql> show variables like 'innodb_buffer_pool_instances';
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| innodb_buffer_pool_instances | 1     |
+------------------------------+-------+
1 row in set (0.03 sec)

​ 可通过配置文件修改实例个数

2. Buffer Pool 高并发场景

单个 Buffer Pool 的问题

如果 InnoDB 存储引擎只有一个 Buffer Pool,当高并发时,多个请求进来,那么为了保证数据的一致性(缓存页、free 链表、flush 链表、lru 链表等多种操作),必须得给缓冲池加锁了,每一时刻只能有一个请求获得锁去操作 Buffer Pool,其他请求只能排队等待锁释放。那么此时 MySQL 的性能是多么的低!

多个 Buffer Pool

在生产环境中,其实我们是可以给 MySQL 设置多个 Buffer Pool 来提升 MySQL 的并发能力的

如果Buffer Pool 分配的内存小于1GB,那么最多就只会给你一个 Buffer Pool

但是呢,如果你给 MySQL 设置的内存很大,此时你可以利用下面两个参数来设置 Buffer Pool 的总大小和总实例数,这样,MySQL 就能有多个 Buffer Pool 来支撑高并发了。

[server] 
#缓冲池大小 8G
innodb_buffer_pool_size = 8589934592 
#缓冲池的数量 4个 则每一个缓冲池大小2G
innodb_buffer_pool_instances = 4

每个 Buffer Pool 负责管理着自己的描述数据块和缓存页,有自己独立一套 free 链表、flush 链表和 lru 链表。

3. 调整 Buffer Pool

修改配置调整

在 MySQL 5.7 后,MySQL 允许我们动态调整参数 innodb_buffer_pool_size 的值来调整 Buffer Pool 的大小了。

假如就这样直接调大会存在啥问题?

假设调整前的配置:Buffer Pool 的总大小为8G,一共4个 Buffer Pool,每个大小为 2G。

[server] 
innodb_buffer_pool_size = 8589934592 
innodb_buffer_pool_instances = 4

假设给 Buffer Pool 调整到 16 G,就是说参数 innodb_buffer_pool_size 改为 17179869184。

此时,MySQL 会为 Buffer Pool 申请一块大小为16G 的连续内存,然后分成 4块,接着将每一个 Buffer Pool 的数据都复制到对应的内存块里,最后再清空之前的内存区域。那这是相当耗费时间的操作

chunk 机制调整

为了解决上面的问题,Buffer Pool 引入一个机制:chunk 机制。

  1. 每个 Buffer Pool 其实是由多个 chunk 组成的。每个 chunk 的大小由参数 innodb_buffer_pool_chunk_size 控制,默认值是 128M。
  2. 每个 chunk 就是一系列的描述数据块和对应的缓存页。
  3. 每个 Buffer Pool 里的所有 chunk 共享一套 free、flush、lru 链表。

得益于 chunk 机制,通过增加 Buffer Pool 的chunk个数就能避免了上面说到的问题。当扩大 Buffer Pool 内存时,不再需要全部数据进行复制和粘贴,而是在原本的基础上进行增减内存,

下面继续用上面的例子,介绍一下 chunk 机制下,Buffer Pool 是如何动态调整大小的。

调整前 `Buffer Pool` 的总大小为 8G,调整后的 `Buffer Pool` 大小为 16 G。
由于 `Buffer Pool` 的实例数是不可以变的,所以是每个 `Buffer Pool` 增加 2G 的大小,此时只要给每个 `Buffer Pool` 申请 (2000M/128M)个chunk就行了,但是要注意的是,新增的每个 chunk 都是连续的128M内存。

4. Buffer Pool内存

如何设置 Buffer Pool 的总内存大小

比较合理的比例,应该是 Buffer Pool 的内存大小占机器总内存的 50% ~ 60%,例如机器的总内存有16G,那么你给 Buffer Pool 分配个8G~10G左右就挺合理的了。

如何设置 Buffer Pool 的实例数

假设此时 Buffer Pool 的总大小为 16G,即 16384M,那么 Buffer Pool 的数量应该是多少个呢?

Buffer Pool 总大小 = (chunk 大小 * Buffer Pool数量)* chunk个数

16384 = ( 128 * Buffer Pool 数量)* n

64 个:也是可以的,但是每个 Buffer Pool 就只要2个 chunk。

16 个:也是可以的,每个 Buffer Pool 拥有8个个 chunk。

8 个:也是可以的,每个 Buffer Pool 拥有16个 chunk。

5. Buffer Pool 的初始化

  • MySQL 启动时,会根据参数 innodb_buffer_pool_size 的值来为 Buffer Pool 分配内存区域。

  • 然后会按照缓存页的默认大小 16k 以及对应的描述数据块的 800个字节 左右大小,在 Buffer Pool 中划分中一个个的缓存页和一个个的描述数据库块。

二、buffer pool的缓存结构

​ 上面我们知道缓冲池被设计为页的链表结构根据页的不同状态(未使用,已使用,已经使用未刷新到缓存):

  • FREE链表: 空闲列表,页属于缓存池但是并没有缓存mysql数据(查询出来的数据会使用该链表的节点存储数据并从free链表移除 进入到LRU链表)。
  • FLUSH链表: LRU列表中的数据一旦被修改(和磁盘不一致),则该页被标记为flush链表中的一员。
  • LRU链表: 采用最近最久未使用LRU的变种(新增了midPoint)内存淘汰策略。

1. LRU list

实现方式

​ 基于LRU(最近最久未使用)内存淘汰策略并新增了midPoint位置。新读取到的页并没有直接放在LRU列的首部,而是放在距离尾部37%的位置。这个算法称之为midpoint insertion stategy

  • midpoint之前的是new区域(热数据)midpoint之后的数据是不活跃数据,old区域。midpoint处,是新子列表的尾部与旧子列表的头相交的边界

  • 当InnoDB将页面读入缓冲池时,它首先将其插入中点(旧子列表的头部)。因为它是用户发起的操作(如SQL查询)所需的,或者是InnoDB自动执行的预读操作的一部分,所以这些页面可以被读取。

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

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

InnoDB学习笔记二缓冲池(buffer pool)_第3张图片

查看midpoint

mysql> show variables like 'innodb_old_blocks_pct';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37    |
+-----------------------+-------+
1 row in set (0.04 sec)
  • 37:末尾处的37%的位置,即末尾 3/8 的位置

设计好处

避免偶发性的读操作获取的数据替换热点数据

  • 全表扫描:大量只有本次需要数据替换了热点数据,影响效率。
  • 预读机制:会读到其他未使用的数据页占据lru表的头部

unZip_LRU

InnoDB从1.0.X开始支持页压缩技术。原本16k的页,可以压缩到2k、4k、8k。因此需要unzip_LRU列来管理,但是注意:LRU list中包含了unzip_LRU

如何给unzip_LRU分配内存:(假设需要从缓冲池中申请4KB大小的内存)

  • 检查4KB的unzip_LRU列表,检查是否有可用的空闲页
  • 若有,则直接使用
  • 否则申请8KB的unzip_LRU页
  • 若能申请到页,则拆分成两个4KB页,并存放到unzip_LRU列表
  • 如果申请不到,则从LRU列表申请一个16K的页面,拆分成一个8K,两个4k,分别放到对应的unzip_LRU列表中

2、Free list

结构

双向链表,链表的每个节点就是一个个空闲的缓存页对应的描述数据块。由 Buffer Pool 里的描述数据块组成的,你可以认为是每个描述数据块里都有两个指针,一个是 free_pre 指针,一个是 free_next 指针,分别指向自己的上一个 free 链表的节点,以及下一个 free 链表的节点。

通过 Buffer Pool 中的描述数据块的 free_pre 和 free_next 两个指针,就可以把所有的描述数据块串成一个 free 链表。

//伪代码来描述一下 free 链表中描述数据块节点的数据结构:
DescriptionDataBlock{
    block_id = block1;
    //指向自己的上一个 free 链表的节点指针
    free_pre = null;
    //指向下一个 free 链表的节点指针
    free_next = block2;
}

free 链表有一个基础节点,他会引用链表的头节点和尾节点,里面还存储了链表中有多少个描述数据块的节点,也就是有多少个空闲的缓存页。

下面我们也用

//伪代码来描述一下基础节点的数据结构:
FreeListBaseNode{
    start = block01;
    end = block03;   
    count = 2;
}

使用流程

补充知识

数据页缓存哈希表,它的 key 是表空间+数据页号,而 value 是对应缓存页的地址。

描述如图所示:

InnoDB学习笔记二缓冲池(buffer pool)_第4张图片

  1. 首先,执行SQL(除了插入操作) ,判断数据对应的数据页能否在 数据页缓存哈希表里 找到对应的缓存页。

  2. 如果找到,将直接在 Buffer Pool 中进行增删改查。

  3. 如果找不到,则从 free 链表中找到一个空闲的缓存页,然后从磁盘文件中读取对应的数据页的数据到缓存页中,并且将数据页的信息和缓存页的地址写入到对应的描述数据块中,然后修改相关的描述数据块的 free_pre 指针和 free_next 指针,将使用了的描述数据块从 free 链表中移除。记得,还要在数据页缓存哈希表中写入对应的 key-value 对。最后也是在 Buffer Pool 中进行增删改查。

3. Flush list

脏页(dirty page)

Buffer Pool 中的缓存页因为不断被修改而导致和磁盘文件中的数据不一致了会有很多个脏页,脏页里面很多脏数据。所以,MySQL 会有一条后台线程,定时地将 Buffer Pool 中的脏页刷回到磁盘文件中。

MySQL通过checkPoint技术将脏页刷新到磁盘

  • Flush list中的页,即为脏页
  • 脏页既存在于LRU列表中,也存在于Flush list中(LRU列表用来管理缓冲池中页的可用性,Flush list脏页刷新到磁盘,两者互不影响)

结构

同Free List结构一下 一个双向链表,使用 flush_pre 指针和 flush_next 指针,分别指向自己的上一个 flush链表的节点,以及下一个 flush链表的节点。

DescriptionDataBlock{
    block_id = block1;
    // free 链表的
    free_pre = null;
    free_next = null;

    // flush 链表的
    flush_pre = null;
    flush_next = block2;
}

flush 链表也有对应的基础节点,也是包含链表的头节点和尾节点,还有就是修改过的缓存页的数量。

FlushListBaseNode{
    start = block1;
    end = block2;
    count = 2;
}

到这里,我们都知道,SQL 的增删改都会使得缓存页变为脏页,此时会修改脏页对应的描述数据块的 flush_pre 指针和 flush_next 指针,使得描述数据块加入到 flush 链表中,之后 MySQL 的后台线程就可以将这个脏页刷回到磁盘中。

描述如图所示:

InnoDB学习笔记二缓冲池(buffer pool)_第5张图片

各种 List 关系

数据库刚启动时,LRU list是空的。Free list是最大的。当需要从缓冲池中分页时,看Free list有空闲页:

  • 有则删除Free list的页,加入到LRU list中。维持一个数量平衡

  • 否则,根据LRU算法,淘汰LRU末尾的页,省出内存来,分配给新的页

  • LRU列中数据被修改后,产生脏页。数据库通过checkpoint机制将脏页刷新会磁盘,flush list中的页即为脏页列表。脏页即存在于LRU中,也存在于Flush中

  • lru 链表尾部的缓存页何时刷入磁盘?

    **没有空闲的free list **: 当 free list 为空了,此时需要将数据页加载到缓冲池里,就会 lru list 的 old 数据区域尾部的缓存页刷入磁盘,然后清空,再加载数据页的数据。

    后台定时任务|:定时将 lru list 的 old 数据区域的尾部的一些缓存页刷入磁盘,然后清空,最后把他们对应的描述数据块加入到 free 链表中去。

    当然了,除了 lru list尾部的缓存页会被刷入磁盘,还有的就是 flush list 的缓存页。

    后台线程同时也会在 MySQL 不繁忙的时候,将 flush 链表中的缓存页刷入磁盘中,这些缓存页的描述数据块会从 lru 链表和 flush 链表中移除,并加入到 free 链表中。

    InnoDB学习笔记二缓冲池(buffer pool)_第6张图片

总结

到此,我已经将缓冲池 Buffer Pool介绍完毕了。

下面简单总结一下 Buffer Pool 从初始化到使用的整个流程。

1、MySQL 启动时会根据分配指定大小内存给 Buffer Pool,并且会创建一个个描述数据块和缓存页。

2、SQL 进来时,首先会根据数据的表空间和数据页编号查询 数据页缓存哈希表 中是否有对应的缓存页。

3、如果有对应的缓存页,则直接在 Buffer Pool中执行。

4、如果没有,则检查 free 链表看看有没有空闲的缓存页。

5、如果有空闲的缓存页,则从磁盘中加载对应的数据页,然后将描述数据块从 free 链表中移除,并且加入到 lru 链表的old数据区域的链表头部。后面如果被修改了,还需要加入到 flush 链表中。

6、如果没有空闲的缓存页,则将 lru 链表的old数据区域的链表尾部的缓存页刷回磁盘,然后清空,接着将数据页的数据加载到缓存页中,并且描述数据块会加入到 lru 链表的old数据区域的链表头部。后面如果被修改了,还需要加入到 flush 链表中。

7、5或者6后,就接着在 Buffer Pool 中执行增删改查。

注意:5和6中,缓存页加入到old数据区域的链表头部后,如果在 1s 后被访问,则将入到new数据区域的链表头部。

8、最后,就是描述数据块随着 SQL 语句的执行不断地在 free 链表、flush 链表和 lru 链表中移动了。

你可能感兴趣的:(mysql,innodb缓冲池,LRU,LIST,Free,List,Flush,list)