MySQL如何加速读写速度?来看看Buffer Pool

210 阅读10分钟

什么是 Buffer Pool

什么是 Buffer Pool? 从字面意思看是缓冲池。Buffer Pool 是干什么用的?既然是缓冲池,那它就是用来缓存数据的。看一看 Google 公司给出的各层级硬件执行速度。

从上图中可以看出,如果想让 MySQL 运行的快,就要尽量避免 CPU 和磁盘直接打交道,而是多让 CPU 和内存打交道,处在内存结构中的 InnoDB Buffer Pool 就起着这样的作用。

Buffer Pool 是 InnoDB 存储引擎中的一个重要组件,它用于管理数据库中的缓冲区。缓冲区是用于存储数据库中的数据页的内存区域,可以提高数据库的性能。

当数据库需要读取或写入数据时,首先会在 Buffer Pool 中查找相应的数据页,如果找到则直接读取或写入内存中的数据页,如果没有找到需要的数据页,则会从磁盘中读取相应的数据页到 Buffer Pool 中,再进行相应的操作。

看一下 MySQL 官档中提供的 InnoDB 体系架构图:(MySQL 5.7)

从上图中可以看到,Buffer Pool 是内存结构中最为重要且核心的组件。

Buffer Pool 的结构

在 MySQL 启动的时候,会向操作系统申请一片连续的内存空间作为 Buffer Pool,默认配置下 Buffer Pool 只有 128MB 。可以使用以下命令查看 Buffer Pool 的大小。

SELECT @@innodb_buffer_pool_size/1024/1024/1024; #字节转为G

生产环境可以根据实际的内存大小进行调整.

set global innodb_buffer_pool_size = 4227858432; ##单位字节

在 Buffer Pool 中数据的存储方式并不是表中的每行数据,而是将空间划分为多个数据页,每个页中存储表中的数据, 一个页的默认大小为 16KB,在读取硬盘中的数据时,也是以页为单位读取。

对于每一个数据页,Buffer Pool 还会有额外的描述块信息,用于记录缓存页的表空间、页号、缓存页地址、链表节点等信息,这部分描述块约占缓存页大小的 5%左右,大概 800 个字节左右的大小。

怎么管理 Buffer Pool

为了方便的管理 Buffer Pool 中的数据信息,MySQL 使用了三个列表来管理页的信息。

  • Free List
  • Flush List
  • LRU List

Free List

MySQL 开始运行时, Buffer Pool 中的数据是空的,运行一段时间后,Buffer Pool 中既有被使用的缓存页也有没有被使用的。为了方便的管理这些未被使用的缓存页,在 MySQL 使用时能够快速的找到空闲缓存页,设计出了 Free List。将空闲缓存页的「描述块」作为链表的节点,以链表的形式将所有的空闲缓存页组织起来。

Free List 上除了有控制块,还有一个头节点,该头节点包含链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。

Flush List

来说 Flush List 之前,先要了解一下脏页。 SQL 的增删改查首先都会在 Buffer Pool 中执行,那么内存数据页和磁盘数据页内容中的内容就会产生不一致,这样的数据页被称为脏页

Mysql 中有一条线程,会定时同步内存 Buffer Pool 中的脏页刷回到磁盘文件中。为了方便的查询那些是脏页。使用了一个 Flush List 的链表记录脏页的信息。它的结构与 Free List 类似,也是空闲缓存页的「描述块」作为链表的节点,包含了一个额外的头结点。

以下四种情况,InnoDB 会进行脏页刷新:

  • MySQL 认为系统空闲;
  • MySQL 正常关闭过程;
  • Free List 不足;
  • redo log 写满。

LRU List

既然 Buffer Pool 的目的是提高读写的速度,那么就要确保缓存页的命中率,Buffer Pool 的大小是有限的,对于一些频繁访问的数据我们希望可以一直留在 Buffer Pool 中,而一些很少访问的数据希望可以在某些时机可以淘汰掉。

要实现这个最容易想到的就是 LUR(Least recently used)算法:链表头部的节点是最近使用的,尾部的是未被使用的,当节点被访问时,将节点移到头部, 当空间不足时,首先淘汰尾部的数据。

所以在 MySQL 中,有一个 LRU List 来管理 Buffer Pool 中已经有数据的缓存页(包含脏页)。Mysql 同时又对 LUR 做了进一步的优化,因为普通的 LUR 在使用过程中会存在两个问题:

  • 预读失效
  • Buffer Pool 污染

预读失效

根据程序的局部性原理,当前被访问的数据,其相邻的数据在未来有很大的几率会被访问。所以,MySQL 在加载数据页时,会提前把它相邻的数据页一并加载进来,目的是为了减少磁盘 IO。

如果使用普通的 LUR,当前的数据和相邻的数据都会被放到链表的头部,但是这些相邻的数据可能一直不会被访问到,这就造成了预读失效。它不光会占用空间,有可能还会把频繁访问的缓存页排挤到链表的尾部,进而使这些缓存页被淘汰掉。

为了解决这个问题,MySQL 将 LUR List 分为了两部分:old 区域 和 young 区域。其中 区域占比 70 %,old 区域占比 30 %。

划分这两个区域后,与当前数据相邻预读的页就只需要加入到 old 区域的头部。

当页被真正访问的时候,才将页插入 young 区域的头部。

可以通过调整 innodb_old_blocks_pct 参数,可以设置 young 区域和 old 区域比例。

Buffer Pool 污染

当一个需要访问大量数据的 sql 被执行时,可能导致把 Buffer Pool 的所有页都替换出去,导致大量热数据被换出,MySQL 性能急剧下降,这种情况叫缓冲池污染。

比如这条 sql

select * from user where name like "%name%";

由于这条语句会发生索引失效,所以会进行全表扫描。

  1. 从磁盘读到的页加入到 LRU 链表的 old 区域头部
  2. 当从页里读取行记录时,也就是页被访问的时候,就要将该页放到 young 区域头部
  3. 接下来拿行记录的进行模糊匹配,如果符合条件,就加入到结果集里;
  4. 如此往复,直到扫描完表中的所有记录。

经过这一番折腾,原本 young 区域的热点数据都会被替换掉。

为了解决这个问题,Mysql 设置了一个 old 区的停留时间判断innodb_old_blocks_time,默认是 1000 ms。。大体思路是这样的:

当第一次访问 old 区的某个缓存页时,记录访问的时间。当再次访问这个缓存页,如果在时间间隔内则不会被放到 young 区,如果不在时间间隔内则放到 young 区。

另外,Mysql 针对 young 区域其实做了一个优化,前面 1/4 被访问不会移动到链表头部,只有后面的 3/4 被访问了才会。因为 young 区的数据本身就是会被频繁访问的,如果每次访问都去移动链表,势必造成性能的下降(影响再小极端情况下也可能会不可控)。

生产环境中的 Buffer Pool

多 Buffer Pool

在多线程,高并发的场景下,如果只有一个 Buffer Pool,必定会存在数据的一致性得问题,解决方式就是加锁,这样就造成了 MySQL 性能的降低。

MsSQL 允许设置多个 Buffer Pool 来提高性能。MySQL 的默认规则:如果给 Buffer Pool 分配的内存小于 1GB,那么最多就只会给一个 Buffer Pool。如果很多可以在配置文件里设置。

[server]
innodb_buffer_pool_size = 8589934592
innodb_buffer_pool_instances = 4

参数innodb_buffer_pool_size 设置 Buffer Pool 的总大小为 8G,参数 innodb_buffer_pool_instances 设置一共有 4 个 Buffer Pool 实例。上面配置中,MySQL 一共有 4 个 Buffer Pool ,每个的大小为 2G。

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

如何动态调整 Buffer Pool 的大小?

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

为了方便的进行动态调整,Buffer Pool 引入了 chunk 机制。

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

假设现在 Buffer Poool 总大小是 8GB,计划要动态加到 16GB,那么此时只要申请一系列的 128MB 大小的 chunk 就可以了,只要每个 chunk 是连续的 128MB 内存就行了。然后把这些申请到的 chunk 内存分配给 buffer pool 就行了。

生产环境中,应该给 Buffer Pool 设置多大的内存?

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

Buffer Pool 实例数可以参考下面这个公式:

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

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

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

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

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

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

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

show engine innodb status;

----------------------
 Buffer Pool  AND MEMORY
----------------------
--  Buffer Pool 的最终大小
Total memory allocated
--  Buffer Pool 一共有多少个缓存页
 Buffer Pool  size
-- free 链表中一共有多少个缓存也是可以使用的
Free buffers
-- lru链表中一共有多少个缓存页
Database pages
-- lru链表链表中的冷数据区一共有多少个缓存页
Old database pages
-- flush链表中的缓存页的数量
Modified db pages
-- 等待从磁盘上加载进来的缓存页的数量
Pending reads
-- 即将从lru链表中刷入磁盘的数量,flush链表中即将刷入磁盘的缓存页的数量
Pending writes: LRU 0, flush list 0, single page 0
-- lru链表的冷数据区的缓存页被访问之后转移到热数据区的缓存页的数量,以及冷数据区里1s之内被访问但是没有进入到热数据区的缓存页的数量
Pages made young 260368814, not young 0
-- 每秒从冷数据转移到热数据区的缓存页的数量,以及每秒在冷数据区被访问但是没有进入热数据区的缓存页的数量
332.69 youngs/s, 0.00 non-youngs/s
-- 已经读取创建和写入的缓存页的数量,以及每秒读取、创建和写入的缓存页的数量
Pages read 249280313, created 1075315, written 32924991 359.96 reads/s, 0.02 creates/s, 0.23 writes/s
-- 表示1000次访问中,有多少次是命中了BufferPool缓存中的缓存页,以及每1000次访问有多少数据从冷数据区转移到热数据区,以及没有转移的缓存页的数量
 Buffer Pool  hit rate 867 / 1000, young-making rate 123 / 1000 not 0 / 1000
-- lru链表中缓存页的数量
LRU len: 8190
-- 最近50s读取磁盘页的总数,cur[0]表示现在正在读取的磁盘页的总数
I/O sum[5198]:cur[0],

www.51cto.com/article/704…

zhuanlan.zhihu.com/p/399592958

cloud.tencent.com/developer/a…

xiaolincoding.com/mysql/buffe…

github.com/asdbex1078/…