Buffer Pool是什么
Buffer Pool就是MySQL服务器启动时向操作系统申请的一片连续的内存。默认情况下,Buffer Pool的容量为128MB。
SHOW VARIABLES LIKE 'innodb_buffer_pool_%';
可以通过启动项innodb_buffer_pool_size设置(单位为字节),不能设置小于5MB。
[server]
innodb_buffer_pool_size = 268435456
Buffer Pool有什么用
MySQL对存储引擎的操作单位是页,为了避免每次读取修改一个页后,就立刻写入到磁盘,下次再从磁盘中取出。这样频繁的I/O相当耗费性能。
只需要在页被读取到内存后,不需要那么着急就写入到磁盘并释放掉内存,而是在Buffer Pool中再“待一会儿”。下次再有请求访问这个页,就可以节省磁盘开销了。
Buffer Pool的数据结构
Buffer Pool对应的内存区域被划分为若干个页面,叫做缓冲页,页面大小与InnoDB表空间中的页大小一致,默认都是16KB。为了更好地管理缓冲页,每一个缓冲页都设立了一些控制信息。这些控制信息包括该页所属的表空间编号、页号、缓冲页在Buffer Pool中的地址、链表节点信息等。
每个缓冲页对应的控制信息叫做控制快,占用的内存大小是相同的。控制快和缓冲页是一一对应的,控制快在Buffer Pool的前面,缓冲页在Buffer Pool的后面,整体结构如图所示。
Buffer Pool中的碎片为分配控制快和缓冲页后剩余的,不够完整分配一对控制快和缓冲页的无用的空间。innodb_buffer_pool_size设置的值并不包含控制快占用的内存空间,只是缓冲页的大小,实际占用内存会稍微大一点,大5%左右。
链表
Buffer Pool初始化过程:1️⃣先向操作系统申请Buffer Pool内存空间;2️⃣把这个内存空间划分为若干对控制块和缓冲页。刚初始化时,这些缓冲页都是空闲的,随着程序的运行,会将磁盘InnoDB表空间中的页读取到缓冲页中。
free链表
所有空闲的缓冲页对应的控制块作为一个节点连起来的双向链表叫做free链表。free链表如图所示:
为了更好地管理free链表,定义了一个基节点,包含链表的头节点地址、尾节点地址以及当前链表中节点的数量等信息。基节点占用的空间不属于Buufer Pool内存,是单独申请的内存空间,大小为40字节。
有了free链表后,每当需要从磁盘中加载一个页到Buffer Pool中时,就从free链表中取一个空闲的缓冲页(节点实际上是缓冲页对应的控制块),并且把该缓冲页对应的控制块填充(表空间、页号之类的信息)。 然后把该缓冲页对应的控制块从free链表中移除, 表示该缓冲页已经被使用了。
flush链表
在已经被读取到Buffer Pool中的页,是根据表空间号+页号来定位一个页的。表空间号+页号是一个key,缓冲页的控制块的地址就是对应的value创建一个哈希表。在需要访问某个页的数据时, 先从哈希表中根据表空间号+页号看看是否有对应的缓冲页。如果有,直接使用该缓冲页就好;如果没有,就从free链表中选一个空闲的缓冲页,然后把磁盘中对应的页加载到该缓冲页的位置。
被加载到Buffer Pool中的页中的数据被修改后,数据就与磁盘中的不一致了,此时Buffer Pool中的这个页称为脏页。脏页不会立刻刷新到磁盘,频繁进行I/O操作十分消耗性能。而是被加入到一个双向链表中,叫做flush链表,这个链表中所有的节点都是脏页,都需要被刷新到磁盘。
LRU链表
Buffer Pool中不再有空闲的缓冲页时,就需要淘汰掉最近很少使用的部分缓冲页,以提高访问命中率。
简单队列式链表的缺点:
- InnoDB预读机制:所谓预读是指InnoDB认为执行当前的请求时,可能会在后面读取某些页面,于是就预先把这些页面加载到Buffer Pool中。根据触发方式不同,分为两种预读:
- 线性预读:系统变量innodb_read_ahead_threshold表示一个数值,如果顺序访问的某个区的页面超过该值,就会出发一次异步读取下一个区中全部的页面到Buffer Pool中。
- 随机预读:系统变量innodb_random_read_ahdead表示一个状态属性ON/OFF,默认为OFF。当设置为ON时,如果某个区的13个连续的页面都被加载到了Buffer Pool中,无论是否为顺序读取,都会触发一次异步读取本区中所有的页面到Buffer Pool中。
- 全表扫描:大量的全表扫描,导致Buffer Pool根本不够用,只能把内存中所有的页面都清除掉,用来加载新的页,对于表中记录非常多的情况,这样可能还不够。
为了增加Buffer Pool命中率,设计出了LRU链表,按照一定比例分成两截:
- 一部分存储使用频率非常高的缓冲页,这一部分也称为热数据,或者young区域;
- 另一部分存储使用频率不是很高的缓冲页。这一部分也称为冷数据,或者old区域。
LRU链表按照按照一定比例分成两半,不是某些节点的固定属性。这个比例由系统变量innodb_old_blocks_pct来决定,表示old区域在LRU链表中所占的比例为百分之多少。
SHOW VARIABLES LIKE 'innodb_old_blocks_pct';
LRU链表中的节点移动规则注意事项:
-
当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓冲页时,该缓冲页对应的控制块会被放到old区域的头部。
-
在对某个处于old区域的缓冲页进行第一次访问的时间在某个时间间隔内,那么该页面就不会从old区域移动到young区域的头部,否则它将移动到young区域的头部。这个时间间隔由系统变量innodb_old_blocks_time控制,单位ms。
SHOW VARIABLES LIKE 'innodb_old_blocks_time';
- 只有被访问的缓冲页位于young区域1/4位置以后,才会被移动到young区域头部。这样做是为了减少LRU移动频率。
刷盘机制
后台有专门的线程负责每隔一段时间把脏页刷新到磁盘,异步执行,不影响用户操作。刷新方式主要有以下两种:
- 从LRU刷新到磁盘
缓冲页对应的控制块中存储了该页是否被修改的信息,可以判定该页是否为脏页。后台线程定时扫描LRU链表尾部,如果发现脏页,就把它们刷新到磁盘。这种刷新方式叫BUF_FLUSH_LRU。系统变量innodb_lru_scan_depth决定扫描的页面数量。
- 从flush链表刷新到磁盘
后台线程定时从flush链表中刷新一部分页面到磁盘,刷新的速率取决于当前系统的忙闲程度。这种刷新方式叫BUF_FLUSH_LIST。
如果后台线程刷新脏页的速度较慢,导致Buffer Pool中无可用空闲页可用,这时会去LRU链表尾部尝试将一个脏页刷新到磁盘。这种将单个页面刷新到磁盘中的方式叫BUF_FLUSH_SINGLE_PAGE。
多个Buffer Pool实例
Buffer Pool本质上是一块内存,在多线程环境下,访问Buffer Pool中的各种链表都需要加锁处理。单一的Buffer Pool可能会影响处理速度。MySQL支持建立多个Buffer Pool实例,每一个实例都是独立的内存空间,独立地管理各种链表,在多线程并发中互不受影响。服务启动时通过innodb_buffer_pool_instances修改创建Buffer Pool实例的个数。
查看Buffer Pool实例数量:
SHOW VARIABLES LIKE 'innodb_buffer_pool_instances';
启动项设置Buffer Pool实例数量:
[server]
innodb_buffer_pool_instances = 2
每一个Buffer Pool实例所占用的内存大小是相同的,为Buffer Pool总大小除以实例数量,每一个实例的大小为总大小平均分配。
注意:Buffer Pool实例数量不是越多越好,分别管理各个实例也需要性能开销。innodb_buffer_pool_size小于1GB时,设置多个实例是无效的。
MySQL5.7.5及更高版本支持服务运行过程中调整Buffer Pool的大小,每次调整大小时,都需要重新向操作系统申请内存,然后将旧的Buffer Pool中的内容复制到新的内存空间中,非常耗费性能。为了解决这个问题,MySQL其实是通过一个chunk为一个单位向操作系统申请内存。一个Buffer Pool实例其实是由若干个chunk组成,一个chunk代表一片连续的内存空间,里面包含了若干个缓冲页与其对应的控制块。
有了chunk作为申请内存的最小单位,服务器运行期间调整Buffer Pool的大小时,就可以以chunk为单位增加或删除内存。每一个chunk的大小通过innodb_buffer_pool_chunk_size设置,默认值是134217728,也就是128MB。innodb_buffer_pool_chunk_size只能通过启动项设置,不可以在服务运行过程中修改。innodb_buffer_pool_chunk_size的值不包含缓冲页对应的控制块的内存空间大小。
SHOW VARIABLES LIKE 'innodb_buffer_pool_chunk_size';
Buffer Pool配置注意事项
- innodb_buffer_pool_size必须大于或等于innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances。否则,MySQL自动将innodb_buffer_pool_chunk_size 的值修改为innodb_buffer_pool_size ÷ innodb_buffer_pool_instances的值。
- innodb_buffer_pool_size必须是innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances的倍数,目的是为了保证每一个Buffer Pool实例中chunk数量相同。否则MySQL会自动将innodb_buffer_pool_size的值修改为整倍数。
Buffer Pool状态信息
SHOW ENGINE INNODB STATUS;
=====================================
2023-11-08 04:36:19 0x7f3e084f6700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 33 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 13 srv_active, 0 srv_shutdown, 13191 srv_idle
srv_master_thread log flush and writes: 13202
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 23
OS WAIT ARRAY INFO: signal count 20
RW-shared spins 0, rounds 52, OS waits 21
RW-excl spins 0, rounds 66, OS waits 2
RW-sx spins 0, rounds 0, OS waits 0
Spin rounds per wait: 52.00 RW-shared, 66.00 RW-excl, 0.00 RW-sx
------------
TRANSACTIONS
------------
Trx id counter 1852
Purge done for trx's n:o < 1848 undo n:o < 0 state: running but idle
History list length 10
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421379820698360, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421379820697440, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
--------
FILE I/O
--------
I/O thread 0 state: waiting for completed aio requests (insert buffer thread)
I/O thread 1 state: waiting for completed aio requests (log thread)
I/O thread 2 state: waiting for completed aio requests (read thread)
I/O thread 3 state: waiting for completed aio requests (read thread)
I/O thread 4 state: waiting for completed aio requests (read thread)
I/O thread 5 state: waiting for completed aio requests (read thread)
I/O thread 6 state: waiting for completed aio requests (write thread)
I/O thread 7 state: waiting for completed aio requests (write thread)
I/O thread 8 state: waiting for completed aio requests (write thread)
I/O thread 9 state: waiting for completed aio requests (write thread)
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,
ibuf aio reads:, log i/o's:, sync i/o's:
Pending flushes (fsync) log: 0; buffer pool: 0
431 OS file reads, 713 OS file writes, 294 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.27 writes/s, 0.00 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
insert 0, delete mark 0, delete 0
discarded operations:
insert 0, delete mark 0, delete 0
Hash table size 34673, node heap has 1 buffer(s)
Hash table size 34673, node heap has 1 buffer(s)
Hash table size 34673, node heap has 1 buffer(s)
Hash table size 34673, node heap has 1 buffer(s)
Hash table size 34673, node heap has 1 buffer(s)
Hash table size 34673, node heap has 1 buffer(s)
Hash table size 34673, node heap has 1 buffer(s)
Hash table size 34673, node heap has 1 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s
---
LOG
---
Log sequence number 16409178
Log flushed up to 16409178
Pages flushed up to 16409178
Last checkpoint at 16409169
0 pending log flushes, 0 pending chkp writes
69 log i/o's done, 0.00 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
# 分配给InnoDB的总内存大小,比innodb_buffer_pool_size略大,包含控制块、缓冲页以及碎片页的大小等等。
Total large memory allocated 137428992
# 为数据字典分配的内存空间大小。
Dictionary memory allocated 210138
# Buffer Pool可容纳缓冲页的数量,单位是页,计算大小需要×16KB。
Buffer pool size 8191
# 当前Buffer Pool还有多少空闲缓冲页。
Free buffers 7415
# LRU链表中页的数量。
Database pages 768
# LRU链表中old区域的页数量。
Old database pages 263
# flush链表中页的数量。
Modified db pages 0
# 等待从磁盘加载到Buffer Pool中的页面数量。当准备从磁盘中加载页时,会先在Buffer Pool中为这个页分配一个控制块,然后把控制块添加到LRU的old区域头部。但此时页还没有真正加载到缓冲页,Pending reads的值表示处于当前状态的页数量,等所有的页都加入到了缓冲页,该值就为0了。
Pending reads 0
# 即将从LRU链表刷新到磁盘中的页面数量/即将从flush链表刷新到磁盘中的页面数量/即将以单个页面的形式刷新到磁盘中的页面数量。
Pending writes: LRU 0, flush list 0, single page 0
# Pages made young表示LRU链表中曾经从old区域移动到young区域头部的节点数量。一个节点每次只有从old区域移动到young区域头部时才会将Pages made young加1。从young区域1/4往后的位置移动到young头部时,Pages made young不会加1。
# Pagse made not young表示处于old区域的节点,不符合innodb_old_blocks_time设置的间隔时间而不能移动到young区域头部,Pages made not young加1。
Pages made young 0, not young 0
# 每秒从old区域移动到young区域的节点数量/每秒由于不满足时间间隔而无法从old区域移动到young区域头部的节点数量。
0.00 youngs/s, 0.00 non-youngs/s
# 读取的页面数量/创建的页面数量/写入的页面数量。
Pages read 406, created 362, written 444
# 每秒读取页面的数量/每秒创建页面的数量/每秒写入页面的数量
0.00 reads/s, 0.00 creates/s, 0.27 writes/s
# Buffer Pool hit rate表示过去的一段时间内,平均访问1000次页面,该页面有多少次被缓存到Buffer Pool中。
# young-making rate表示在过去的一段时间内,平均访问1000次页面,有多少次访问使得页面移动到young区域头部。注意的是,这里统计的不只是从old区域移动到young区域头部的节点,还包括从young区域1/4以后位置移动到young区域头部的节点。
# not(young-making rate)表示过去的一段时间内,平均访问1000次页面,有多少次访问没有使页面移动到young区域头部。注意的是,这里统计的不只是由于时间间隔导致没能从old区域移动到young区域头部的节点,还包括处于young区域1/4之前位置而不需要移动到young区域头部的节点。
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表示LRU链表中节点的数量
# unzip_LRU len表示unzip_LRU链表中节点的数量
LRU len: 768, unzip_LRU len: 0
# I/O sum表示最近50s读取磁盘页的数量
# I/O cur表示现在正在读取的磁盘页面数量
# I/O unzip sum表示最近50s解压的页面数量
# I/O unzip cur表示现在正在解压的页面数量
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
0 read views open inside InnoDB
Process ID=1, Main thread ID=139904543950592, state: sleeping
Number of rows inserted 20140, updated 0, deleted 0, read 30137
0.73 inserts/s, 0.00 updates/s, 0.00 deletes/s, 909.79 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================
阅读学习《MySQL是怎样运行的》小孩子4919