文章目录
InnoDB Buffer Pool是什么?
我们知道,如果我们使用的是InnoDB存储引擎,那么不管我们的索引还是表的数据都是以页的形式存放在表空间里的,而表空间是InnoDB对文件系统中一个或几个文件的抽象,也就是说数据实际都是存在磁盘上面的。
可是磁盘的速度都是比较慢的,所以InnoDB存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把整页的数据全部加载到内存中,进行读写,并且在读写完成后也不会立即把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省去磁盘IO的开销了。
而在内存中存放数据页的空间就叫做InnoDB Buffer Pool
,默认情况下InnoDB Buffer Pool
只有128M,当然如果机器内存充足,可以设置的大一些。
我们的数据是如何放在InnoDB Buffer Pool中的?
我们都知道我们数据库里面有很多的表,一个表中有很多字段,一个表里还有很多行数据,每行数据都有自己的字段值。但是MySQL并不是直接把一行一行的数据放到Buffer Pool
中的,MySQL对数据抽象出来了一个数据页的概念,它把很多行数据放在了一个数据页中,也就是我们的磁盘文件中会有很多的数据页,每一页数据里放了很多数据行。
默认情况下,磁盘中存放的数据页的大小是16KB,也就是说,一页数据包含了16KB的内容,而Buffer Pool
中存放的缓存页大小是和磁盘上数据页的大小是一一对应的,都是16KB。
但是对于每个缓存页,都会有一个描述信息,这个描述信息是用来描述缓存页的,包含了比如: 这个数据页所属的表空间、数据页的编号、这个缓存页在Buffer Pool中的地址等等,每个缓存页都会对应一个描述信息,这个描述信息本身也是一块数据,在Buffer Pool
中,每个缓存页的描述数据放在最前面,然后各个缓存页放在后面。
需要注意的是每个缓存页16KB,而每个描述数据大概是800多字节,相当于缓存页大小的5%左右,我们设置的innodb_buffer_pool_size
并不包含这部分描述数据占用的内存大小,所以我们设置的buffer pool假如是1G,实际上Buffer Pool的真正大小会超出一些,大概是1G * 1.05 = 1.05G左右。
大概就是下面的样子
InnoDB怎么知道数据页是否在Buffer Pool中?
我们已经知道了,当需要访问某个页中的数据时,就会把该页从磁盘加载到Buffer Pool
中,如果该页已经在Buffer Pool
中的话直接使用就可以了。那么我们怎么能知道该页在不在Buffer Pool
中呢,依次遍历Buffer Pool
中的各个缓存页肯定不合适的,所以InnoDB Buffer Pool
还维护了一个哈希表,用表空间号 + 页号
作为key,缓存页作为value。
当需要访问某个页的数据时,先从哈希表中根据表空间 + 页号
看看有没有对应的缓存页,如果有,直接使用该缓存页就行,如果没有,那就从磁盘上读取一个该数据页,然后放到free链表
中的一个空缓存页上。
InnoDB Buffer Pool的组成
InnoDB Buffer Pool除了有缓存页、描述数据块之外,还有几个链表来管理Buffer Pool
,比如free链表
、flush链表
、LRU链表
等等,下面我们一起来了解一下这三个链表。
free链表
当我们启动MySQL的时候,需要对Buffer Pool进行初始化,先向操作操作系统申请了Buffer Pool
的内存空间,然后把它划分为很多对描述数据和缓存页。但是此时并没有真正的缓存页被缓存到Buffer Pool
中,之后随着对数据库的增删改查,会不断的从磁盘中读取一个一个的数据页放到缓存页中,那么此时怎么能知道哪些缓存页是空闲的呢?最好在某个地方记录一下哪些缓存页是空闲的,所以开发数据库的大佬们设计了一个free链表
,它是一个双向链表的数据结构,把所有空闲缓存页对应的描述数据作为一个个节点放到一个链表中。刚完成初始化时Buffer Pool
中所有的缓存页都是空闲的,所以每一个缓存页对应的描述数据块都会被加入到free链表
中。
从图中可以看到,为了更好的管理free链表
,开发InnoDB的大佬们还定义了一个基础节点,里面包含了链表的头结点地址,尾结点地址,以及当前链表中节点的数量等信息,需要注意的是链表的基础节点占用的内存空间并不包含在为Buffer Pool
申请的一大片连续内存空间之内,而是单独申请了一块内存空间,不过基础节点占用的内存空间并不大,大概40字节左右。
有了free链表
之后就方便多了,每当需要从磁盘加载一个页到Buffer Pool
中时,就从free链表
中取一个空闲的缓存页,并且把该缓存页对应的描述数据的信息填上(就是该页所在的表空间、页号之类的信息),然后把该缓存页对应的节点用free链表
中移除,表示该缓存页已经被使用了。
flush链表
当我们在执行增删改查的时候,如果发现数据页没缓存,就会基于free链表
找到一个空闲的缓存页,然后读取到缓存页里去,如果已经缓存了,那么下一次就可以直接使用缓存页。
然后我们可能会去更新Buffer Pool
中缓存页的数据,一旦我们更新了缓存页中的数据,那么缓存页里的数据和磁盘上的数据就不一致了,这时候呢,我们就说这个缓存页就是脏数据,脏页(dirty page)。
我们知道,为了确保数据一致性,以及数据最终肯定会被刷回磁盘的,但是如果遍历所有的缓存页来刷盘,这个效率也太低了,所以开发InnoDB的大佬们又设计了一个flush链表
,这个flush链表
本质也是一个双向链表,里面存了被修改过的缓存页的描述数据
,凡是被修改过的缓存页,都会把它的描述数据块加入到flush链表
中去,后续要被flush
刷新到磁盘上去的。
LRU链表
free链表已经被用完了怎么办?
我们现在已经知道了,我们对数据执行CRUD操作的时候,都会把磁盘上的数据页加载到空闲的缓存页中,这时候我们想一下free链表
中的数据肯定会越来越少,如果free链表
中没有空闲缓存页了怎么办呢。
那肯定是要淘汰一部分缓存页,可是怎么能知道淘汰哪些缓存页合适呢?
我们回忆一下为什么要有Buffer Pool
,目的就是为了减少和磁盘的IO交互,提升缓存的命中率,所以开发InnoDB的大佬们,又设计了一个LRU链表
(Least Recently Used, 最近最少使用),这样使用可以将最早使用过的缓存页给淘汰掉。
简单的LRU链表会有什么问题吗?
如果使用简单的LRU链表,当我们需要访问某个页的时候,可能会发生两种情况:
- 如果该页不在
Buffer Pool
中,就把该页从磁盘加载到Buffer Pool
中的缓存页时,就把该缓存页对应的描述数据块作为一个节点添加到链表的头部。 - 如果该页已经缓存在
Buffer Pool
中了,就直接把该页对应的描述数据块移动到LRU链表
的头部。
简单来说就是当我们使用到一个缓存页的时候,就将它移动到LRU链表
的头部,这样LRU链表尾部
就是最近最少使用的缓存页了,当free链表
中的空闲缓存页不够时,直接淘汰LUR链表
尾部的缓存页就可以了。
但是这种简单的LRU链表
实际使用中会面临两个尴尬的问题
问题一:
InnoDB有预读的功能,分别是线性预读和随机预读
- 线性预读
InnoDB
中有一个变量innodb_read_ahead_threshold
默认是56,如果顺序访问了某个区(extent,一个区内有64个数据页)的数据页超过了这个参数的值,就会异步的将下一个区中全部的页面加载到Buffer Pool
中
- 随机预读
- 有一个变量
innodb_random_read_ahead
,默认是关闭的。假如是开启的,当Buffer Pool
中缓存了某个区的13个连续的页面,不管这些页面是不是顺序读取加载的,都会异步的将本区的所有其它页面加载到Buffer Pool
中。
- 有一个变量
预期确实可能会减少去磁盘中读取数据的次数,提高性能,可是如果我们预读的缓存页并没有被用到,然后还放在了LRU链表
的头部,这明显不合适,反而可能会淘汰一些我们会真正使用的缓存页。
问题二:
如果我们执行了一些全表扫描的语句,一个表可能会包含很多区,很多页。当我们需要访问这些页的时候,也都会把他们都加载到Buffer Pool
中,相当于把Buffer Pool
换了一次血,但是这种全表扫描语句执行频率可能不会很高,让全表扫描读到的缓存页导致把经常使用的缓存页淘汰,明显也不太好。
总结上面的两个问题
- 通过预读功能加载到
Buffer Pool
中的页不一定会被用到 - 如果一些使用频率非常低的页被加载到
Buffer Pool
中,可能会把那些使用频率比较高的页从Buffer Pool
中淘汰掉
实际的LRU链表是怎么设计的?
开发InnoDB
的大佬们当然不允许上面的两种情况影响Buffer Pool
的性能,所以大佬们把LRU链表
按照一定的比例分成了两截
- 一截是使用频率比较高的缓存页,这部分链表叫做
热数据
,也叫young区域
- 还有一截是使用频率不是很高的缓存页,这一部分链表叫做
冷数据
,也叫old区域
并且LRU链表
的young区域
和old区域
的比例实际上是可以调节的,innodb_old_blocks_pct
这个参数默认是37
,意思就是old区域
占LRU链表
的比例,37%
大概就是3/8
,所以young区域
默认大概占5/8
。
实际上的LRU链表
大概长这样
数据什么时候会进入old区域,什么时候会进入young区域呢?
当磁盘中的数据页第一次被加载到Buffer Pool
中时,该缓存页的描述数据块会被放到old区域
的头部,这样不管是通过预读
机制读取进来的数据,还是全表扫描进来的数据,都会先放在old区域
,不会影响young区域
中那些比较热的数据。
那么缓存页什么时候会从old区域
进入到young区域
呢,还有一个变量innodb_old_blocks_time
,默认是1000
,单位是毫秒。
意思是当数据页被加载到old区域
的头部后,1000毫秒后再次访问到这个缓存页的时候,才会将它从old区域
移动到young区域
的头部。
每访问一次数据就移动一次LRU链表吗?
对于young区域
的缓存页来说,每访问一个缓存页就将它移动到LRU链表
的头部,这样开销会比较大,毕竟young区域
的缓存页都比较热,可能会经常被访问到,所以实际上只有被访问的缓存页在young区域
的后3/4的位置,才会被移动到LRU链表
中young区域
的头部,换句话说,如果访问的一个缓存页在young区域
的前1/4,不会把它移动到young区域
的头部,这样会尽可能的减少缓存页的移动。
InnoDB Buffer Pool的大小可以在运行期间调整大小吗?
在MySQL 5.7.5 之前,Buffer Pool
的大小只能在服务器启动时配置,在服务器运行过程中是不能调整大小的,不过开发Innodb的大佬们为了能让Buffer Pool
的大小可以在运行期间调整,让Buffer Pool
以一个chunk
为单位向操作系统申请空间。
我们可以想一下,如果说没有chunk
,每次扩容都需要申请一大块内存空间,然后将旧的Buffer Pool
中的数据拷贝到新的Buffer Pool
中,这会多耗时,并且拷贝过程中需要两块Buffer Pool
同时存在。所以有了chunk
,一大块的Buffer Pool
由很多的chunk
块组成,扩缩容的时候只要增加或减少chunk
块就好了,直接方便了很多。
每块chunk
的大小是通过innodb_buffer_pool_chunk_size
参数配置的,默认是128M,不过需要注意的是chunk
的大小在运行期间是不可以改变的。
还有一点需要注意的是,innodb_buffer_pool_size
必须是innodb_buffer_pool_chunk_size
× innodb_buffer_pool_instances
的倍数,主要是想保证每个Buffer Pool
实例中包含的chunk
数量相同。不过就算我们设置的不是倍数,mysql也会自动帮我们调整为倍数。
通过下面命令可以在线调整
Buffer Pool
大小
mysql> SET GLOBAL innodb_buffer_pool_size=402653184;
查询缓冲池大小调整进度
mysql> SHOW STATUS WHERE Variable_name='InnoDB_buffer_pool_resize_status';
Buffer Pool中的脏页怎么刷新到磁盘?
后台有专门的线程负责每隔一段时间将脏页刷回磁盘,主要有两种方式
- 从
LRU链表
的冷数据中刷新一部分缓存页到磁盘- 后台线程会定时从
LRU链表
的尾部扫描一些页面,扫描页面的数量可以通过innodb_lru_scan_depth
参数指定(默认1024),如果从里面发现了脏页,就会把他们刷新到磁盘中。这种刷新页面的方式称为BUF_FLUSH_LRU
。
- 后台线程会定时从
- 从
flush链表
中刷新一部分缓存页到磁盘- 后台线程也会定时从
flush链表
中刷新一部分页面到磁盘,刷新的速度取决于担心系统是不是很繁忙。这种刷新的方式称为BUF_FLUSH_LIST
。
- 后台线程也会定时从
InnoDB Buffer Pool与Qurey Cache有什么区别?
Query Cache是什么?
看下图,如果将MySQL分为Server层和存储引擎层两部分,那么Query Cache位于Server层。
在MySQL在执行一条SELECT语句时,MySQL会先从Query Cache中查询是否曾经执行过这个SQL,如果没有执行过,会先执行这条SQL,并将查询结果以key-value的方式缓存起来,key是SQL语句,value是查询结果。
have_query_cache
: 来查看服务器中是否存在查询缓存。
query_cache_size
: 用来设置查询缓存的大小。
query_cache_type
有三个可选值 (如果query_cache_type
是在服务器启动是设置的,那么仅允许使用数字)
- 0或者OFF用来禁用缓存
- 1或者ON启用缓存,但是那些以
SELECT SQL_NO_CACHE
的语句除外 - 2或者DEMAND,仅缓存那些以
SELECT SQL_CACHE
的语句
query_cache_limit
:设置单个查询结果可以缓存的最大大小,默认为1MB
Query Cache的缺点
一但有SQL更新了这张表,那么该表的查询缓存就会全部失效,并且SQL语句的不同也会导致无法使用缓存,所以这个缓存的命中率可能会比较低,再加上维护缓存也要不小的成本。
所以一般推荐使用redis之类的缓存,管理起来更加的方便,禁用MySQL的查询缓存,并且MySQL8.0以后已经移除了查询缓存。
总结InnoDB Buffer Pool和Query Cache的区别
可以看到InnoDB Buffer Pool
和Query Cache
就不是一个层面的东西,Query Cache
是MySQL Server层面的东西,而InnoDB Buffer Pool
是存储引擎层面的。
Query Cache
是类似于redis一样的一个key-value缓存,key是SQL语句,value是查询结果。
而InnoDB Buffer Pool
是用来缓存数据页的,对数据页进行CRUD之前,都需要将数据页从磁盘加载到Buffer Pool
中,如果对缓存页进行了修改,产生了脏页,就会慢慢的刷回磁盘中。
生产环境中的InnoDB Buffer Pool应该怎么配置?
通过前面的知识,我们已经了解了,Buffer Pool
实际上就是一块连续空间的内存,在多线程访问的时候,Buffer Pool
相关的各种链表需要进行变化,肯定会涉及到加锁,所以Buffer Pool
特别大而且多线程并发访问的时候,一个Buffer Pool
性能可能不会太高。所以在Buffer Pool
特别大的时候,我们可以把设置多个小的Buffer Pool
,每个Buffer Pool
都是独立的,独立去申请内存空间,独立的管理各种链表,相当于减少了并发的冲突,减少了锁的竞争,有点ConcurrentHashMap
中数据分段的味道。
设置多个Buffer Pool
是根据innodb_buffer_pool_instances
这个参数来设置,默认为1。
注意的是innodb_buffer_pool_size
设置的是所有Buffer Pool
的总内存,也就是说每个Buffer Pool
的大小为innodb_buffer_pool_size / innodb_buffer_pool_instances
。
不过也不是Buffer Pool
数量越多越好,innodb_buffer_pool_size
小于1G的时候设置多个innodb_buffer_pool_instances
是无效的,当innodb_buffer_pool_size
大于1G的时候,可以设置多个Buffer Pool
,换句话说,每个Buffer Pool
的实例内存必须要大于1G。
innodb_buffer_pool_instances
的值范围为1-64。
生产环境中,如果服务器是数据库专用的,推荐将innodb_buffer_pool_size
设置为机器内存的50%-75%
。
MySQL官方文档推荐,数据库中的表比较大时,将innodb_old_blocks_pct
设置为较小的是可以防止只读取一次的数据消耗缓冲池的很大一部分,例如可以设置为5%
。数据库内的表都比较小时,在缓冲池内移动页面的开销比较小,可以适当调大innodb_old_blocks_pct
的值,例如50%
。
查看Buffer Pool的状态信息
我们可以通过SHOW ENGINE INNODB STATUS
命令查看InnoDB
的一些状态信息。
mysql> show engine innodb status\G
....省略了很多......
....InnoDB Buffer Pool的相关信息只看BUFFER POOL AND MEMORY这一栏就行....
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137428992
Dictionary memory allocated 295138
Buffer pool size 8191
Free buffers 1024
Database pages 7165
Old database pages 2624
Modified db pages 0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 8477, not young 36385157
0.00 youngs/s, 0.00 non-youngs/s
Pages read 95235, created 59407, written 137654
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
Buffer pool hit rate 999 / 1000, young-making rate 0 / 1000 not 5 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 7165, unzip_LRU len: 0
I/O sum[33]:cur[0], unzip sum[0]:cur[0]
我们一起来看一下里面的每个值都代表啥意思
- Total large memory allocated: 代表
Buffer Pool
向操作系统申请的连续内存空间大小,包括全部的描述数据、缓存页、以及碎片大小。(我们可以看到这个数据是稍微大于innodb_buffer_pool_size
的大小的,之前说过这个参数没有包含描述数据块的大小) - Dictionary memory allocated: 为数据字典信息分配的内存空间大小,需要注意的是,这个内存空间和
Buffer Pool
没啥关系,不包含在Total large memory allocated
中。 - Buffer pool size: 代表该
Buffer Pool
可以容纳多少缓存页,注意的是,单位是页! - Free buffers: 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区域
内移动,这个值是不会增加的。 - Pages made not young:在
innodb_old_blocks_time
值大于0时,首次访问或者后续访问某个处于在old区域
的节点由于不符合时间间隔的限制而不能将其移动到young区域
头部时,Pages made not young
的值会加1。 - youngs/s: 代表每秒从
old区域
被移动到young区域
头部的节点数量。 - non-youngs/s:代表每秒由于不满足时间限制而不能从
old区域
移动到young区域
头部的节点数量。 - Pages read、created、written: 代表读取、创建、写入了多少页。
- reads/s、creates/s、writes/s: 代表了每秒读取、创建、写入的速率
- Buffer pool hit rate: 表示在过去某段时间,平均访问1000次页面,有多少次该页面已经被缓存到
Buffer Pool
中了。 - young-making rate: 过去某段时间内,平均访问1000次页面,有多少次访问使页面移动到
young区域
的头部了,这里和**Pages made young**
不一样,这里不仅仅不含从old区域
移动到young区域
的次数,还包含从young区域
移动到young区域
头部的次数。 - young-making not: 过去某段时间内,平均访问1000次页面,有多少次访问没有使页面移动到
young区域
的头部,与young-making rate
类似,不仅包含没有从old区域
移动到young区域
的次数,还包含在young区域
没有移动的次数。 - LRU len: 代表
**LRU链表**
的数量 - unzip_LRU len: 代表
unzip_LRU链表
的长度 - I/O sum: 最近50s读取磁盘页的总数
- I/O cur: 现在正在读取磁盘页的数量
- I/O unzip sum: 最近50s解压的页面数量
- I/O unzip cur: 正在解压的页面数量