MySql--InnoDB Buffer Pool

摘要

InnoDB Buffer Pool

  • InnoDB为了缓存磁盘中的页,在MySQL服务器启动的时候就向操作系统申请了一片连续的内存,这片内存叫做Buffer Pool(中文名是缓冲池),默认情况下Buffer Pool只有128M大小。

  • innodb_buffer_pool_size规定了系统将多少内存用作InnoDB的索引缓存

1
2
3
4
5
6
7
# 查看buffer pool设置大小,推荐设置为内存的60%,最大不要超过75%,比如这里设置了1G,单位是字节
mysql> show variables like 'innodb_buffer_pool_size';
+-------------------------+------------+
| Variable_name | Value |
+-------------------------+------------+
| innodb_buffer_pool_size | 1073741824 |
+-------------------------+------------+
  • Buffer Pool中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是16KB。

  • 为了更好的管理这些在Buffer Pool中的缓存页,InnoDB为每一个缓存页都创建了一些所谓的控制信息,这些控制信息包括该页所属的表空间编号、页号、缓存页在Buffer Pool中的地址、链表节点信息、一些锁信息以及LSN信息,当然还有一些别的控制信息。

  • 每个缓存页对应的控制信息占用的内存大小是相同的,我们称为控制块。控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,Buffer Pool的初始化过程,就是把申请到的Buffer Pool的内存空间划分成若干对控制块和缓存页的过程。其中控制块被存放到 Buffer Pool 的前边,缓存页被存放到 Buffer Pool 后边

  • free链表:与空闲的缓存页一一对应的控制块会组成一个free链表,用于表示哪些缓冲页可用,每当需要从磁盘中加载一个页到Buffer Pool中时,就从free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上(就是该页所在的表空间、页号之类的信息),然后把该缓存页对应的free链表节点从链表中移除,表示该缓存页已经被使用了。

  • 缓存页的哈希处理

    • 当我们需要访问某个页中的数据时,就会把该页从磁盘加载到Buffer Pool中,如果该页已经在Buffer Pool中的话直接使用就可以了。
    • 那么问题也就来了,我们怎么知道该页在不在Buffer Pool中呢?难不成需要依次遍历Buffer Pool中各个缓存页么?
    • 可以用表空间号 + 页号作为key,缓存页作为value创建一个哈希表,在需要访问某个页的数据时,先从哈希表中根据表空间号 + 页号看看有没有对应的缓存页,如果有,直接使用该缓存页就好,如果没有,那就从free链表中选一个空闲的缓存页,然后把 磁盘中对应的页加载到该缓存页的位置。
  • flush链表:因为频繁的往磁盘中写数据会严重的影响程序的性能,所以并不是修改了某个页面的数据就立刻刷新到磁盘上,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要在未来某个时间被刷新到磁盘上的,所以也叫flush链表。链表的构造和free链表差不多。

  • LRU链表:Buffer Pool没有多于的空间时,需要基于LRU算法将旧的缓存页从Buffer Pool中移除,然后再把新的页放进来。当我们需要访问某个页时,如果该页不在Buffer Pool中,在把该页从磁盘加载到Buffer Pool中的缓存页时,就把该缓存页对应的控制块作为节点塞到LRU链表的头部。如果该页已经缓存在Buffer Pool中,则直接把该页对应的控制块移动到LRU链表的头部。所以当Buffer Pool中的空闲缓存页使用完时,到LRU链表的尾部找些缓存页淘汰就行了。

    • 实际上LRU链表的规则并不是这么简单,因为mysql支持预读,即读取一个数据页时会同时读取其附近的多个页面(这些页可能用不到),或者执行了大表的扫描全表的查询语句(使用频率偏低),这样就有可能淘汰掉Buffer Pool中那些高频访问的页。

    • 解决方法是把这个LRU链表按照一定比例分成两截

      • 一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做热数据,或者称young区域。
      • 另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做冷数据,或者称old区域。
      • 可以通过查看系统变量innodb_old_blocks_pct的值来确定old区域在LRU链表中所占的比例
      1
      2
      3
      4
      5
      6
      mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_pct';
      +-----------------------+-------+
      | Variable_name | Value |
      +-----------------------+-------+
      | innodb_old_blocks_pct | 37 |
      +-----------------------+-------+
    • InnoDB规定,当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓存页时,该缓存页对应的控制块会被放到old区域的头部,该缓存页再次被访问到时才会把该页放到young区域的头部,前提是这两次访问的时间间隔大于系统变量innodb_old_blocks_time设置的时间间隔,默认1000毫秒

    1
    2
    3
    4
    5
    6
    mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_time';
    +------------------------+-------+
    | Variable_name | Value |
    +------------------------+-------+
    | innodb_old_blocks_time | 1000 |
    +------------------------+-------+
    • 对于young区域的缓存页来说,我们每次访问一个缓存页就要把它移动到LRU链表的头部,这样频繁的对LRU链表进行节点移动操作也会拖慢速度?为了解决这个问题,MySQL规定只有被访问的缓存页位于young区域的1/4的后边,才会被移动到LRU链表头部,这样就 可以降低调整LRU链表的频率,从而提升性能。

刷新脏页到磁盘
mysql后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。主要有两种刷新路径:

  • 1、从LRU链表的冷数据中刷新一部分页面到磁盘。
  • 2、从flush链表中刷新一部分页面到磁盘。
  • 每个控制块大约占用缓存页大小的5%,而我们设置的innodb_buffer_pool_size并不包含这部分控制块占用的内存空间大小,也就是说InnoDB在为Buffer Pool向操作系统申请连续的内存空间时,这片连续的内存空间一般会比innodb_buffer_pool_size的值大5%左右,这也是为什么不建议设置innodb_buffer_pool_size超过内存的75%。

  • InnoDB增删改查都是直接操作这个buffer pool,并顺序写入undo logredo log,如果直接操作硬盘就是随机写,效率会非常低

  • InnoDB引擎数据更新执行顺序

1.加载要查询或修改的硬盘数据所在的页到buffer pool,加载时可能会同时加载相邻的页
2.将要修改的数据旧值写入undo log文件,如果事务提交失败,可以用undo日志里的数据进行回滚
3.更新buffer pool内存中的数据
4.写入redo log buffer(内存)
5.提交事务时写入redo log文件,用于在异常情况(如事务提交成功,但buffer pool里的数据尚未写入磁盘,此时宕机)下恢复buffer pool内存中的数据
6.写入bin log文件,主要用于恢复数据库磁盘里的数据
7.bin log文件记录成功后写入commit标记到redo log文件,保证redo与binlog数据一致性
8.buffer pool内存中的数据随机写入磁盘,独立线程每隔一段时间就会刷盘,如果buffer pool写满了也会采用LRU等算法将内存数据写入磁盘

多个Buffer Pool实例

  • Buffer Pool本质是InnoDB向操作系统申请的一块连续的内存空间,在多线程环境下,访问Buffer Pool中的各种链表都需要加锁处理,在Buffer Pool特别大而且多线程并发访问特别高的情况下,单一的Buffer Pool可能会影响请求的处理速度。
    所以在Buffer Pool特别大的时候,我们可以把它们拆分成若干个小的Buffer Pool,每个Buffer Pool都称为一个实例,它们都是独立的,独立的去申请内存空间,独立的管理各种链表,所以在多线程并发访问时并不会相互影响,从而提高并发处理能力。

  • 我们可以在服务器启动的时候通过设置innodb_buffer_pool_instances的值来修改Buffer Pool实例的个数,默认8,最大值是64。

1
2
3
4
5
6
mysql> show variables like 'innodb_buffer_pool_instances';
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| innodb_buffer_pool_instances | 8 |
+------------------------------+-------+
  • 每个Buffer Pool实例实际占内存空间: innodb_buffer_pool_size/innodb_buffer_pool_instances

  • innodb_buffer_pool_size(默认128M)的值小于1G的时候设置多个实例是无效的,InnoDB会默认把innodb_buffer_pool_instances的值修改为1。

  • 最佳的innodb_buffer_pool_instances的数量是,innodb_buffer_pool_size除以innodb_buffer_pool_instances,可以让每个Buffer Pool实例达到1个G

  • mysql服务启动时(故障后重启),会先将redo log中的内容加载到Buffer Pool中,然后在读取undo log进行事务回滚,以此达到重启前的状态

查看Buffer Pool的状态信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mysql> SHOW ENGINE INNODB STATUS\G
………………
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 0
Dictionary memory allocated 1009326
Buffer pool size 65530
Free buffers 63832
Database pages 1689
Old database pages 0
Modified db pages 0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 1542, created 161, written 352
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: 1689, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]

返回值说明
Total memory allocated:代表Buffer Pool向操作系统申请的连续内存空间大小,包括全部控制块、缓存页、以及碎片的大小。
Dictionary memory allocated:为数据字典信息分配的内存空间大小,注意这个内存空间和Buffer Pool没啥关系,不包括在Total memory allocated中。
Buffer pool size:代表该Buffer Pool可以容纳多少缓存页,注意,单位是页!
Free buffers:代表当前Buffer Pool还有多少空闲缓存页,也就是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区域头部的节点数量。
Page made not young:在将innodb_old_blocks_time设置的值大于0时,首次访问或者后续访问某个处在old区域的节点时由于不符合时间间隔的限制而不能将其移动到young区域头部时,Page made not young的值会加1。
youngs/s:代表每秒从old区域被移动到young区域头部的节点数量。
non-youngs/s:代表每秒由于不满足时间限制而不能从old区域移动到young区域头部的节点数量。
Pages read、created、written:代表读取,创建,写入了多少页。后边跟着读取、创建、写入的速率。
Buffer pool hit rate:表示在过去某段时间,平均访问1000次页面,有多少次该页面已经被缓存到Buffer Pool了。
young-making rate:表示在过去某段时间,平均访问1000次页面,有多少次访问使页面移动到young区域的头部了。
not (young-making rate):表示在过去某段时间,平均访问1000次页面,有多少次访问没有使页面移动到young区域的头部。
LRU len:代表LRU链表中节点的数量。
unzip_LRU:代表unzip_LRU链表中节点的数量。
I/O sum:最近50s读取磁盘页的总数。
I/O cur:现在正在读取的磁盘页数量。
I/O unzip sum:最近50s解压的页面数量。
I/O unzip cur:正在解压的页面数量。