LRU算法及其优化策略——Mysql篇

4,734 阅读5分钟

LRU算法及其优化策略——Mysql篇.jpg

上一篇文章中,介绍了LRU算法在Redis之中的应用,本篇继续给各位道友介绍在Mysql的InnobDB引擎中,是如何使用LRU算法的。

InnoDB缓冲池

缓存池简介及内存结构

首先来介绍下InnoDB的缓冲池,缓冲池简单来说就是一块内存区域,该区域内缓存着InnoDB访问存储在磁盘的数据和索引信息。缓冲池有两个作用,一是提高了大容量读取操作的效率,二是提高了缓存管理的效率。调配缓存池参数,使得经常访问的参数能够保留在缓存池中是一个很重要的Mysql优化手段。

一个InnoDB缓存池的内存结构图如下图所示:

缓冲池.png

图源自《Mysql技术内幕:InnoDB存储引擎》

缓存池状态

我们可以通过SHOW ENGINE INNODB STATUS命令来查看缓存池在InnoDB引擎中的表现:

----------------------
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 6593445888;                       // 为缓冲池分配的总内存(字节)
Dictionary memory allocated 7687783                      // 为InnoDB数据字典分配的总内存(字节)
Buffer pool size   393208                                // 分配给缓冲池的页面总大小(页)
Free buffers       352642                                // 缓冲池空闲列表的页面总大小(页)
Database pages     40485                                 // 缓冲池LRU列表的页面总大小。(页)
Old database pages 14967                                 // 缓冲池旧LRU子列表的页面总大小(页)
Modified db pages  4                                     // 缓冲池中当前修改的页面数。
Pending reads 0                                          // 等待读入缓冲池的缓冲池页面数。
Pending writes: LRU 0, flush list 0, single page 0       // 从LRU列表的底部开始写入的缓冲池中的旧脏页数。                                                          // 检查点期间要刷新的缓冲池页面数。
                                                         // 缓冲池中暂挂的独立页面写入数。
Pages made young 5, not young 0                          // 缓冲池LRU列表中变年轻的页面总数
                                                         // 缓冲池LRU列表中未设置为年轻的页面总数
...

完整的缓存池状态信息可以在这里找到:缓存池状态信息

缓存池的数量和大小

为了避免多个线程读写缓存池引起的并发冲突,InnoDB可以配置多个缓存池,由参数innodb_buffer_pool_instances指定,内部使用散列表进行分配和管理。

通常来说,当缓存池的大小越大,则Mysql表现的越像一个内存数据库。我们可以在启动时或者运行时通过innodb_buffer_pool_size参数动态地调整缓存池的大小,需要注意的innodb_buffer_pool_size的大小会自动的调整为InnoDB缓存池块innodb-buffer-pool-chunk-size(默认为128M)的整倍数。

为避免潜在的性能问题,缓存池大小/缓存池块大小(innodb_buffer_pool_size/ innodb_buffer_pool_chunk_size)的数量不应超过1000。

缓存池的刷新

说到缓存,必须有缓存刷新机制,即剔除缓存中的脏页(已经被修改,但是并未刷入磁盘中的数据页)。

在5.7以上的版本中,InnoDB会启动默认四个线程并发的来执行缓存池中脏页的清除。脏页的清除有两种模式:

  1. 普通模式,当缓存池中的脏页比例超过innodb_max_dirty_pages_pct_lwm(低水平线默认为25%)时,启动普通模式将脏页刷新到磁盘中。
  2. aggressively flushes(激进模式?),当缓存池中的脏页比例超过innodb_max_dirty_pages_pct(默认为75%)时,启动更快的刷新模式,尽快的将脏页刷新到磁盘当中。

缓存池的预读(Prefetching )

InnoDB的缓存池不仅是被动地缓存,而且会异步地预先从磁盘中读取数据页,有两种方式:

  • 线性:根据缓存池的访问数据的顺序来预读,当读取某一区(Extend)中的页(Page)的数据超过innodb_read_ahead_threshold时,则将该区中剩余的所有页都加载到缓存池中。

  • 随机:根据缓存池中的已有页面进行预读,而不管他们的顺序,当发现缓存池中某一区内页的数量超过了innodb_random_read_ahead,则将改区中剩余的所有页都加载到缓存池中。

缓存池LRU算法

在了解了InnoDB的缓存池概念后,我们来看看背后支持缓存池工作的算法。

当我们使用朴素的LRU算法时,会发现如果有批量的操作时,会打乱缓存数据,大大降低了缓存命中率。而在Mysql当中会有大量的预读及全表扫描的操作,为了使得真真的热数据留在内存中,InnoDB缓存池采用了一种变种的LRU算法,有些像我在这篇文章中写到的LRU-K算法。

新进入缓存池的页并不会直接进入LRU链表的头部,而是插入到距离链表尾3/8的位置(可以由innodb_old_blocks_pct参数进行配置),我们将距离链表尾3/8以上的位置称为新子列表,以下的位置称为旧子列表,数据在链表中自底而上称为变年轻,反之称为变老。下图是一个示意图:

innodb-buffer-pool-list.png

  • 变年轻

    变年轻分为两种情况,第一种是来源于用户的操作而需要读取页面,此时会直接使该页直接移至新子列表链表头部。第二种是来源于数据库内部的预读操作,则在距离插入innodb_old_blocks_time(默认为1000ms)的时间内,即使访问了该页,该页也不会别移到LRU链表的头部。

    也就是说,如果是来源于用户的操作,则最起码需要两次操作才能变年轻。而如果是预读操作,则需要加上一个等待期限。

  • 变老

    随着链表数据的替换和访问,整个列表中的数据会自然的变老。最终最老的页面会从尾部逐出。

总结

本文介绍了Mysql的InnoDB引擎的缓存池的概念,及其对于LRU算法的改造。介绍了另一种解决LRU列表被污染的解决方案。