说明
- 对于使用InnoDB存储引擎的表来说, 无论是用于存储用户数据的索引,还是系统数据,都是以页的形式存放在表空间(tablespace)中。
- 所谓的表空间(tablespace),实际是InnoDB对一个或几个实际文件的抽象,最终还是存在磁盘上。
- 当InnoDB处理客户端请求时,如果需要访问某个页的数据, 即使只需要该页中的一条数据,也会把完整的页加载到内存中。
- 在读写完成后,不会释放内存空间,而且缓存起来备用
相关配置
-
配置文件配置方式
[server] # buffer pool size 总大小 -> 8GB innodb_buffer_pool_size = 8589934592 # buffer pool instance 个数 -> 2 innodb_buffer_pool_instances = 2 # buffer pool chunk 大小 -> 128M innodb_buffer_pool_chunk_size = 134217728 # old区占 buffer pool 区域的比例 innodb_old_blocks_pct = 40 # LRU old区刷脏时扫描的表个数 innodb_lru_scan_depth
-
临时配置方式
# 修改old区占buffer pool的比例 set global innodb_old_blocks_pct = 40 ;
-
注意
- 当innodb_buffer_pool_size小于5M时,innodb_buffer_pool_size会被强制生效为5MB
- 当innodb_buffer_pool_size小于1G时,innodb_buffer_pool_instances会被强制生效为1
组成
-
BufferPool对应的一片连续的内存区域会被划分为若干页,该页的大小与InnoDB表空间使用的页大小一致,默认为16K,称为缓存页/缓冲页
-
每个缓存页对应一个控制块,控制块内容包括
- 表空间编号
- 页号
- 缓存页在BufferPool中的地址
- 链表节点信息
- …
-
在内存空间中存放示意图如下
- 控制块1和缓存页1对应,控制块2和缓存页2对应,以此类推
- 碎片是当内存无法进行分配是留下的空闲部分
-
5.7.22 debug 模式下,控制块的大小是808字节,我们设置的innodb_buffer_pool_size并不包含这部分大小
-
控制块大约占innodb_buffer_pool_size的5%,所以实际申请的内存大小约为innodb_buffer_pool_size*105%
-
缓存页的hash处理
-
大体是用表空间号+页号做为key,缓存页控制块的地址作为value,创建hash表
-
当访问某个页的数据时, 现充哈希表中根据key查看是否有对应的value, 如果有就使用,如果没有则从free链表中选择一个空闲的缓存页,把磁盘中的数据加载进去
-
疑问:第一步的数据和页是如何关联的
-
名词解释
1. 脏页
- 当执行数据更新时,会先将更新的数据记录在BufferPool中的缓存页中,此时缓存页中的数据和磁盘页中的数据不同,我们将这种状态的缓存页称为脏页(dirty page)
2. 预读
2.1 线性预读
- 由Innodb_read_ahead_threshold参数控制,默认值为56
- 当顺序访问某个区(extent)的页面数量超过这个系统变量的值时,会触发一次异步读取,将下个区中的全部页面加载到缓存页,该步骤不会影响当前工作线程的正常执行
2.2 随机预读
- 由 innodb_random_read_ahead 参数控制,默认关闭
- 如果某个区中的13个连续页(这里必须是young区的前1/4)都被加载到了BufferPooll中, 则会异步将该区所有的页都加载进去
3. 链表
3.1 Free 链表
作用
- 用于记录哪些缓存页是空闲的
结构
- 基节点:记录控制块的起始位置(start),终止位置(end)和节点个数(n),节点个数=空闲的缓存页个数
- 控制块:包含pre和next指针(这里的控制块就是缓存页的控制块)
内存信息
- free链表大小约为40字节,且不占用BufferPool中的内存
如何工作
- 需要加载缓存页时,会从free链表中取一个空闲缓存页,并把该缓存页的控制块信息填上,然后把链表节点(控制块)从链表中移除,表示缓存页已经被使用
- 注意:从链表中取出和移除的是控制块,从控制块中获取缓存页信息,再获取缓存页的数据
3.2 flush链表
功能
- 用于管理脏页
结构
- 结构大体和free链表相同
- 脏页的控制块中包含flush链表的pre和next指针
注意
- 如果缓存页是脏页,那肯定不是空闲的,它一定在flush表中。
- 如果缓存页是干净页,那它一定在free表中。
- 所以控制块中的链表指针只可能在free或flush中二选一
3.3 lru链表
命名规则
- 来自于 Least Recently Used 的英文首字母缩写,意为"最近很少使用"。
作用
- 用于淘汰buffer_pool中使用最少的页。
如何工作
- 访问不在BufferPool中的页时,先把该页从磁盘加载到buffer_pool的缓存页,再把该缓存页的控制块加入到LRU链表的头部
- 如果已经在BufferPool中, 则移动对应缓存页的控制块到LRU链表头部
- 这样LRU链表靠后的就是不常被使用的缓存页,当没有空闲缓存页可用时,再LRU链表尾部找缓存页进行淘汰
3.4 lru链表优化版(划分区域)
为什么要优化
-
最早的LRU链表存在两个场景比较尴尬
-
场景一
- 早期LRU链表设计的场景是"用到某页才会加载",但InnoDB有一个功能叫预读
- 预读机制会使不一定被用到的页被放到LRU头部,其他页顺序后移。如果缓冲区不够大,则会淘汰掉一些有用的页,降低BufferPool命中率
-
场景二
- 一些低频使用的数据在进行全表扫描或大数据加载时,会将高频使用的缓存页挤出buffer_pool
优化方式
-
为了解决以上两个场景的问题,将原有的LRU划分为两个区域
- 一部分储存高频使用的缓存页,这部分链表称为热数据,也叫做young区
- 一部分存储低频缓存页,称为冷数据,也叫做old区
-
针对场景一
- 首次加载缓存页时, 会先放到old区的头部(优化前会直接放到LRU链表头部)
- 避免预读的页将高频使用的页挤到链表尾部导致淘汰
-
针对场景二
- 对某个old区的缓存页进行第一次访问时,在对应的控制块中记录访问时间,如果后续的访问时间与第一次的访问时间在规定时间间隔内,则不会从old区移到young区,否则会移动。
- 这个间隔时间由 innodb_old_blocks_time 控制
- 查看方式:Show variables like ‘innodb_old_blocks_time’;
- 默认值:1000 ms(即1s)
-
为了减少young区因为控制块移动导致的开销,只有young区后3/4的缓存也在被访问时,才会移动到LRU头部
区域比例
- 默认情况下old区占LRU链表的3/8(即37%)
查看
# 查看old区占比
show variables like 'innodb_old_blocks_pct';
# 查看young区时间间隔阈值
show variables like 'innodb_old_blocks_time';
4. 刷脏
- 后台有专门的线程负责每隔一段时间就把脏页刷到磁盘, 这样不会影响到用户的正常请求
- 方式一:BUF_FLUSH_LRU
- 从LRU链表的old区刷新一部分页到磁盘
- 缓存页的控制块中记录了该缓存页是否被修改
- 刷新的页数可以通过innodb_lru_scan_depth来指定
- 方式二:BUF_LFUSH_LIST
- 从FLUSH链表中刷新一部分页到磁盘,刷新的速率取决于系统是否繁忙
- 方式三:BUF_FLUSH_SINGLE_PAGE
- 当申请新缓存页时发现没有可用的空闲缓存页,则会查看LRU尾部,尝试直接释放掉未修改的缓冲页。
- 如果都是脏页,则将尾部的脏页同步刷到磁盘
- 加快刷脏效率的配置
- innodb_flush_neighbors
- innodb_io_capacity_max
- innodb_adaptive_flushing
- Innodb_max_dirty_pages_pct
5. buffer_pool_instance
- BufferPool 实例个数
- 默认为8,取值范围(1~64)
6. buffer_pool_chunk
- 5.7.5后,每个BufferPool实例由多个Chunk组成,每个chunk是一片连续的内存空间
- chunk默认是128MB,这个值只能在服务启动时修改
- BufferPool size 必须是 chunk * instance 的整数倍,即 innodb_buffer_pool_size = innodb_buffer_pool_instances * innodb_buffer_pool_chunk_size * N
- 若chunk=128M,instance=16,则bufferPool size必须为2G的整数倍,如配置为8G,会正常生效
- 若chunk=128M,instance=16,则bufferPool size必须为2G的整数倍,如配置为9G,则bufferSize会被调整为10G
- 如果bufferPoolSize < innodb_buffer_pool_instances * innodb_buffer_pool_chunk_size, 则系统会将chunk调整为 bufferPoolSize/instance
- 若chunk=256,instance=16,bufferPool size=2G,因为chunk*instance=4G,大于bufferpoolsize,所以chunk会被调整为 2G/instance = 128M
查看命令解析
# show engine innodb status
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 549715968 # bufferpool的连续内存空间大小,包括控制块,缓存页,碎片
Dictionary memory allocated 140608 # 不包含在bufferpool中
Buffer pool size 32768 # bufferpool中可容纳的页
Free buffers 32376 # 空闲页,也是free链表中的节点数
Database pages 392 # lru的页数量,包括young和old
Old database pages 0 # old
Modified db pages 0 # 脏页数量,即flush节点数量
Pending reads 0 # 等待从磁盘加载到bufferpool的页数(分配缓冲页及对应控制块,控制块添加到LRUold区头部)
Pending writes: LRU 0, flush list 0, single page 0 # 三种刷脏方式对应刷脏的页数
Pages made young 0, not young 0 # 移动到young区的页数,notyang查询间隔小于参数而导致不移动的页数
0.00 youngs/s, 0.00 non-youngs/s # 按秒计算上边的指标
Pages read 259, created 148, written 643 # 读,创建,写的页数
0.00 reads/s, 0.00 creates/s, 0.00 writes/s # 读,创建,写的页数速率
No buffer pool page gets since the last printout #
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 392, unzip_LRU len: 0 # LRU节点的数量、后边忽略
I/O sum[0]:cur[0], unzip sum[0]:cur[0] # 最近50s读取磁盘页的总数、正在读取的磁盘页的总数,后边忽略
参考链接
- http://www.45fan.com/article.php?aid=1D8xEYCJKG4YnANt
- innodb_io_capacity参数性能测试
- https://zhuanlan.zhihu.com/p/415004185