Mysql - Buffer Pool

说明

  • 对于使用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中的地址
    • 链表节点信息
  • 在内存空间中存放示意图如下
    Mysql - Buffer Pool_第1张图片

    • 控制块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

你可能感兴趣的:(#,DB,mysql,buffer,pool)