当前位置:   article > 正文

MySQL Buffer Pool

MySQL Buffer Pool

总结自:小林codingbojiangzhou

虽然说 MySQL 的数据是存储在磁盘里的,但是也不能每次都从磁盘里面读取数据,这样性能是极差的。

要想提升查询性能,加个缓存就行了嘛。所以,当数据从磁盘中取出后,缓存内存中,下次查询同样的数据的时候,直接从内存中读取。为此,Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。

Buffer Pool 是在 MySQL 启动的时候,向操作系统申请的一片连续的内存空间,默认配置下 Buffer Pool 只有 128MB 。然后按照默认的16KB的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页。

可以通过调整 innodb_buffer_pool_size 参数来设置 Buffer Pool 的大小,一般建议设置成可用物理内存的 60%~80%。

为了更好的管理这些在 Buffer Pool 中的缓存页,InnoDB 为每一个缓存页都创建了一个控制块,控制块信息包括「缓存页的表空间、页号、缓存页地址、链表节点」等等。控制块也是占有内存空间的,它是放在 Buffer Pool 的最前面,接着才是缓存页。

管理Buffer Pool

缓存页哈希表

有些数据页被加载到 Buffer Pool 的缓存页中了,那怎么知道一个数据页有没有被缓存呢?

所以InnoDB还会有一个哈希表数据结构,它用 表空间号+数据页号 作key,value 就是缓存页的地址。

当使用一个数据页的时候,会先通过表空间号+数据页号作为key去这个哈希表里查一下,如果没有就从磁盘读取数据页,如果已经有了,就直接使用该缓存页。

管理空闲页(Free List)

那当我们从磁盘读取数据的时候,总不能通过遍历这一片连续的内存空间来找到空闲的缓存页吧,这样效率太低了。所以,为了能够快速找到空闲的缓存页,可以使用链表结构,将空闲缓存页的「控制块」作为链表的节点,这个链表称为 Free 链表(空闲链表)。

有了 Free 链表后,每当需要从磁盘中加载一个页到 Buffer Pool 中时,就从 Free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上,然后把该缓存页对应的控制块从 Free 链表中移除。

管理脏页(Flush List)

  • Free Page(空闲页),表示此页未被使用,位于 Free 链表;

  • Clean Page(干净页),表示此页已被使用,但是页面未发生修改,位于LRU 链表。

  • Dirty Page(脏页),表示此页「已被使用」且「已经被修改」,其数据和磁盘上的数据已经不一致。当脏页上的数据写入磁盘后,内存数据和磁盘数据一致,那么该页就变成了干净页。脏页同时存在于 LRU 链表和 Flush 链表。

设计 Buffer Pool 除了能提高读性能,还能提高写性能,也就是更新数据的时候,不需要每次都要写入磁盘,而是将 Buffer Pool 对应的缓存页标记为脏页,然后再由后台线程将脏页写入到磁盘。

那为了能快速知道哪些缓存页是脏的,于是就设计出 Flush 链表,它跟 Free 链表类似的,链表的节点也是控制块,区别在于 Flush 链表的元素都是脏页。有了 Flush 链表后,后台线程就可以遍历 Flush 链表,将脏页写入到磁盘。

提升缓存命中率(冷热分离的LRU List)

简单的 LRU 算法的实现思路是这样的:

  • 当访问的页在 Buffer Pool 里,就直接把该页对应的 LRU 链表节点移动到链表的头部。

  • 当访问的页不在 Buffer Pool 里,除了要把页放入到 LRU 链表的头部,还要淘汰 LRU 链表末尾的节点。

但是简单的 LRU 算法并没有被 MySQL 使用,因为简单的 LRU 算法无法避免下面这两个问题:

  • 预读失效;

  • Buffer Pool 污染;

预读失效

根据程序的空间局部性原理,靠近当前被访问数据的数据,在未来很大概率会被访问到。所以,MySQL 在加载数据页时,会提前把它相邻的数据页一并加载进来,目的是为了减少磁盘 IO。

但是可能这些被提前加载进来的数据页,并没有被访问,相当于这个预读是白做了,这个就是预读失效。如果使用简单的 LRU 算法,就会把预读页放到 LRU 链表头部,而当 Buffer Pool空间不够的时候,还需要把末尾的页淘汰掉。

如果这些预读页如果一直不会被访问到,就会出现一个很奇怪的问题,不会被访问的预读页却占用了 LRU 链表前排的位置,而末尾淘汰的页,可能是频繁访问的页,这样就大大降低了缓存命中率。

Buffer Pool 污染

当某一个 SQL 语句扫描了大量的数据时,在 Buffer Pool 空间比较有限的情况下,可能会将 Buffer Pool 里的所有页都替换出去,导致大量热数据被淘汰了,等这些热数据又被再次访问的时候,由于缓存未命中,就会产生大量的磁盘 IO,MySQL 性能就会急剧下降,这个过程被称为 Buffer Pool 污染。

如果我们写了一个全表扫描的查询语句(页中每条数据会被访问最多一次),一下就将整个表的页加载到了 LRU 的头部。

冷热数据分离的LRU List

为了解决简单 LRU 链表的问题,InnoDB在设计 LRU 链表的时候,实际上是采取冷热数据分离的思想,LRU链表会被拆成两部分,一部分是热数据(又称new/young列表),一部分是冷数据(又称old列表)。如下图所示。冷数据默认占37%,通过innodb_old_blocks_pct 参数来设置。

基于冷热分离的LRU链表,这时新加载一个缓存页时,就不是直接放到LRU的头部了,而是放到冷数据区域的头部。那什么时候将冷数据区域的页移到热数据区域呢?

  • 如果是预读机制加载了一些不会被访问的页,慢慢的被淘汰掉就行了。如果预读的页被访问了,就将其放入热数据头部?这样是不行的全表扫描加载进来的页,必然是会被读取至少一次的,而且一页包含很多条记录,可能会被访问多次。

  • 所以 InnoDB 设置了一个规则,在第一次访问冷数据区域的缓存页的时候,就在它对应的描述信息块中记录第一次访问的时间,默认要间隔1秒后再访问这个页,才会被移到热数据区域的头部。也就是从第一次加载到冷数据区域后,1秒内多次访问都不会移动到热数据区域,基本上全表扫描查询某缓存页的操作1秒内就结束了。(间隔时间是由参数 innodb_old_blocks_time 控制的,默认是 1000毫秒

热数据区域中的页是每访问一次就移到头部吗?也不是的,热数据区域是最频繁访问的数据,如果频繁的对LRU链表进行节点移动操作也是不合理的。所以 InnoDB 就规定只有在访问了热数据区域的 后3/4 的缓存页才会被移动到链表头部,访问 前1/4 中的缓存页是不会移动的。

总结

  • LRU链表分为冷、热数据区域,前 63% 为热数据区域,后 37% 为冷数据区域,加载缓存页先放到冷数据区域头部。

  • 冷数据区域的缓存页第一次访问超过1秒后,再次访问时才会被移动到热数据区域头部。

  • 热数据区域中,只有后 3/4 的缓存页被访问才会移到头部,前 1/4 被访问到不会移动。

  • 淘汰数据优先淘汰冷数据区域尾部的缓存页。

LRU List 和 Flush List

Flush链表中的缓存页一定是在 LRU 链表中的,而 LRU 链表中不在 Flush链表 中的缓存页就是未修改过的页。可以通过下图来理解 LRU 链表和 Flsuh链表。

可以看到,脏页既存在于 LRU链表 中,也存在于 Flush链表 中。LRU链表 用来管理 Buffer Pool 中页的可用性,Flush链表 用来管理将页刷新回磁盘,二者互不影响。

即 Free + LRU(包含Flush)约等于 所有页数量,因为缓冲池中的页还可能分配给自适应哈希索引、Lock信息、Insert Buffer等

脏页刷脏

引入了 Buffer Pool 后,当修改数据时,首先是修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页,但是磁盘中还是原数据。因此脏页需要被刷入磁盘,保证缓存和磁盘数据一致,但是若每次修改数据都刷入磁盘,则性能会很差,因此一般都会在一定时机进行批量刷盘。

  • 当 redo log 日志满了的情况下,会主动触发脏页刷新到磁盘;

  • 后台有专门的线程会定时从LRU链表尾部扫描一些缓存页,扫描的数量可以通过参数 innodb_lru_scan_depth 来设置。如果有脏页,就会把它们刷回磁盘,然后释放掉,不是脏页就直接释放掉,再把它们加回Free链表中。这种刷新页面的方式被称之为 BUF_FLUSH_LRU

  • Buffer Pool 没有空闲页时,需要将LRU List的尾部淘汰一个数据页淘,如果淘汰的是脏页,需要先将脏页同步到磁盘;这种刷新单个页面到磁盘中的刷新方式被称之为 BUF_FLUSH_SINGLE_PAGE

  • MySQL 认为空闲时,后台线程会定期将 Flush 链表适量的脏页刷入到磁盘;这种刷新页面的方式被称之为 BUF_FLUSH_LIST

  • MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;

查看Buffer Pool状态

我们可以通过 SHOW ENGINE INNODB STATUS; 来查看 InnoDB 的状态信息。但是要注意,状态并不是当前的状态,而是过去某个时间范围内 InnoDB 存储引擎的状态。

缓冲池和内存信息

从输出的内容中,可以找到 BUFFER POOL AND MEMORY 这段关于缓冲池和内存的状态信息。

  1. ----------------------
  2. BUFFER POOL AND MEMORY
  3. ----------------------
  4. Total large memory allocated 1099431936
  5. Dictionary memory allocated 8281957
  6. Buffer pool size 65535
  7. Free buffers 1029
  8. Database pages 63508
  9. Old database pages 23423
  10. Modified db pages 80
  11. Pending reads 0
  12. Pending writes: LRU 0, flush list 0, single page 0
  13. Pages made young 15278983, not young 2027514654
  14. 0.00 youngs/s, 0.00 non-youngs/s
  15. Pages read 83326150, created 1809368, written 21840503
  16. 0.00 reads/s, 0.00 creates/s, 0.00 writes/s
  17. Buffer pool hit rate 1000 / 1000, young-making rate 1 / 1000 not 0 / 1000
  18. Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
  19. LRU len: 63508, unzip_LRU len: 0
  20. I/O sum[833]:cur[0], unzip sum[0]:cur[0]
  • Total large memory allocated:Buffer Pool向操作系统申请的内存空间大小,包括全部控制块、缓存页、以及碎片的大小。

  • Dictionary memory allocated:为数据字典信息分配的内存空间大小,这个内存空间和Buffer Pool没啥关系,不包括在Total large memory allocated中。

  • Buffer pool size:缓存池页的数量,所以缓冲池大小为 65535 * 16KB = 1G

  • Free buffers:Free链表中的空闲缓存页数量。

  • Database pages:LRU链表中的缓存页数量。需要注意的是,Database pages + Free buffers 可能不等于 Buffer pool size,因为缓冲池中的页还可能分配给自适应哈希索引、Lock信息、Insert Buffer等,而这部分不需要LRU来管理。

  • Old database pages:LRU冷数据区域(old列表)的缓存页数量,23423/63508=36.88%,约等于 37%

  • Modified db pages:修改过的页,这就是 Flush链表中的脏页数量。

  • Pending reads:正在等待从磁盘上加载到Buffer Pool中的页面数量。当准备从磁盘中加载某个页面时,会先为这个页面在Buffer Pool中分配一个缓存页以及它对应的控制块,然后把这个控制块添加到LRU的冷数据区域的头部,但是这个时候真正的磁盘页并没有被加载进来,所以 Pending reads 的值会加1。

  • Pending writes:从LRU链表中刷新到磁盘中的页面数量,其实就对应着前面说的三种刷盘的时机:BUF_FLUSH_LRU、BUF_FLUSH_LIST、BUF_FLUSH_SINGLE_PAGE

  • Pages made young:显示了页从LRU的冷数据区域移到热数据区域头部的次数。注意如果是热数据区域后3/4被访问移动到头部是不会增加这个值的。

  • Pages made not young:这个是由于 innodb_old_blocks_time 的设置导致页没有从冷数据区域移到热数据区域的页数,可以看到这个值减少了很多不常用的页被移到热数据区域。

  • xx youngs/s, xx non-youngs/s:表示 made young 和 not young 这两类每秒的操作次数。

  • xx reads/s, xx creates/s, xx writes/s:代表读取,创建,写入的速率。

  • Buffer pool hit rate xx/1000:表示在过去某段时间,平均访问1000次页面,有多少次该页面已经被缓存到Buffer Pool了,表示缓存命中率。这里显示的就是 100%,说明缓冲池运行良好。这是一个重要的观察变量,通常该值不应该小于 95%,否则我们应该看下是否有全表扫描引起LRU链表被污染的问题。

  • young-making rate xx/1000 not xx/1000:表示在过去某段时间,平均访问1000次页面,有多少次访问使页面移动到热数据区域的头部了,以及没移动的缓存页数量。

  • LRU len:LRU 链表中节点的数量。

  • I/O sum[xx]:cur[xx]:最近50s读取磁盘页的总数,现在正在读取的磁盘页数量。

LRU List 信息

我们还可以查询 information_schema 下的 INNODB_BUFFER_PAGE_LRU 来观察LRU链表中每个页的具体信息。

SELECT * FROM information_schema.INNODB_BUFFER_PAGE_LRU WHERE TABLE_NAME = '`hzero_platform`.`iam_role`';

其中的一些信息如下:

  • POOL_ID:缓冲池ID,我们是可以设置多个缓冲池的。

  • SPACE:页所属表空间ID,表空间ID也可以从 information_schema.INNODB_SYS_TABLES 去查看。

  • PAGE_NUMBER:页号。

  • PAGE_TYPE:页类型,INDEX就是数据页。

  • NEWEST_MODIFICATION、OLDEST_MODIFICATION:LRU热数据区域和冷数据区域被修改的记录,如果想查询脏页的数量,可以加上条件 (NEWEST_MODIFICATION > 0 or OLDEST_MODIFICATION > 0)

  • NUMBER_RECORDS:这一页中的记录数。

  • COMPRESSED:是否压缩了

设置Buffer Pool大小

多线程访问 Buffer Pool 的时候,会涉及到对同一个 Free、LRU、Flush 等链表的操作,例如节点的移动、缓存页的刷新等,那必然是会涉及到加锁的。就算只有一个 Buffer Pool,多线程访问要加锁、释放锁,由于基本都是内存操作,所以性能也是很高的。但在一些高并发的生产环境中,配置多个 Buffer Pool,还是能极大地提高数据库并发性能的。

可以通过参数 innodb_buffer_pool_instances 来配置 Buffer Pool 实例数,通过参数 innodb_buffer_pool_size 设置所有 Buffer Pool 的总大小(单位字节)。每个 Buffer Pool 的大小就是 innodb_buffer_pool_size / innodb_buffer_pool_instances。InnoDB 规定,当 innodb_buffer_pool_size 小于1GB的时候,设置多个实例是无效的,会默认把innodb_buffer_pool_instances 的值修改为1

动态调整Buffer Pool大小

可以在运行时动态调整 innodb_buffer_pool_size 这个参数,但 InnoDB 并不是一次性申请 pool_size 大小的内存空间,而是以 chunk 为单位申请。一个 chunk 默认就是 128M,代表一片连续的空间,申请到这片内存空间后,就会被分为若干缓存页与其对应的描述信息块。

也就是说一个Buffer Pool实例其实是由若干个chunk组成的,每个chunk里划分了描述信息块和缓存页,然后共用一套 Free链表、LRU链表、Flush链表。每个chunk 的大小由参数 innodb_buffer_pool_chunk_size 控制,这个参数只能在服务器启动时指定,不能在运行时动态修改。

合理设置

在生产环境中安装MySQL数据库,首先我们一般要选择大内存的机器,那我们如何合理的设置 Buffer Pool 的大小呢?

比如有一台 32GB 的机器,不可能说直接给个30G,要考虑几个方面。首先前面说过,innodb_buffer_pool_size 并不包含描述块的大小,实际 Buffer Pool 的大小会超出 innodb_buffer_pool_size 5% 左右。另外机器本身运行、MySQL运行也会占用一定的内存,所以一般 Buffer Pool 可以设置为机器的 50%~60% 左右就可以了,比如32GB的机器,就设置 innodb_buffer_pool_size 为 20GB。

另外,innodb_buffer_pool_size 必须是 innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances 的倍数,主要是保证每一个Buffer Pool实例中包含的chunk数量相同。

比如默认 chunk_size=128MB,pool_size 设置 20GB,pool_instances 设置 16 个,那么 20GB / (128MB * 16) = 10 倍,这样每个 Buffer Pool 的大小就是 128MB * 10 = 1280MB。如果将 pool_instances 设置为 32 个,那么 20GB / (128MB * 32) = 5 倍,这样每个 Buffer Pool 的代销就是 128MB * 5 = 640MB

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/木道寻08/article/detail/817183
推荐阅读
相关标签
  

闽ICP备14008679号