InnoDB通过在内存中维护缓存池(Buffer Pool)来对数据和索引进行缓存,从而提高数据库性能。了解Mysql内存缓冲池的原理,并针对性地进行调优,能够最大化其带来的性能优势。
通常,在保证服务器上其他应用程序有足够内存的情况下,可以给buffer pool分配尽可能多的内存空间。越大的buffer pool能够使mysql越能够像内存数据库一样提供服务。其大小通过innodb_buffer_pool_size配置。对于64 bit 机器,可以将buffer pool 配置为多个部分,以降低并发操作情况下的资源争用。
InnoDB buffer pool的状态及使用情况可以通过 SHOW ENGINE INNODB STATUS 命令得到,相关信息位于 BUFFER POOL AND BUFFER 区。
1. InnoDB Buffer Pool LRU Algorithm
InnoDB 将buffer pool作为一个list, 采用LRU算法的变体进行维护。当需要向pool中添加新内存分页(page)时,InnoDB会按照LRU原则丢弃一个旧的page,并向list的中间插入新的page。这种方法将list作为两个sublist:
- 在头部,是最近被读取过的新page(new/young)
- 在尾部,是最近未被读取过的旧page(old)
此算法保证,那些最近被频繁读取的page位于new list中,而读取时间较远的page位于old list中,可能会被淘汰。
默认地,LRU 算法运行机制如下:
- 3/8的buffer pool被划分为老年代(the old sublist)
- 中点(midpoint)指新生代(the new sublist)尾部和老年代头部相遇的地方
- 当InnoDB将一个内存分页读入到buffer pool中时,该分页被初始插入到midpoint位置。触发读入一个内存分页的操作可以是用户显式触发的查询操作,或者InnoDB自动执行的数据预读取(read-ahead)
- 当访问老年代中的数据时,该分页会被移动至新生代的头部位置。对于用户触发的查询操作,数据被读入buffer pool会紧接着被访问,从而被移动至新生代头部;而对于read-ahead的情况,对应的数据并不会被立刻访问,甚至可能在其被移出buffer pool前都不会被访问到。
- 随着数据库的运行,被访问到的数据不断地被移动至新生代区的头部,而其余新生代和老年代的数据会不断向后移动,直至被从buffer pool中移除。
2. InnoDB Buffer Pool 主要配置参数
buffer pool主要的配置参数如下:
- innodb_buffer_pool_size 指定buffer pool的大小,在有足够内存空间的情况下,尽可能提高该参数的大小,以提高性能表现。一般地,在只运行mysql的机器上,建议将该参数设置为物理内存空间的50%-75%。
- innodb_buffer_pool_instances 该参数表示将buffer pool划分为若干个独立的缓存池实例。对于高并发的应用场景,将buffer pool划分为多个可以有效减少因为内存资源竞争带来的额外消耗。此参数只有在pool_size 参数设定大于1G的情况下指定才有效,假定将instances指定为N,则每个buffer pool占用的内存带下为size/N。在资源足够的情况下,建议通过组合pool size 与 instances 参数,保证每个独立缓存池的大小大于1G,以获得更佳的性能表现。
- innodb_old_blocks_pct 指定buffer pool 老年代占用总空间的比例大小,取值范围为5-95,默认值为37(即3/8)
- innodb_old_blocks_time 指定读入buffer pool的内存分页,当其被首次访问后,延迟old_blocks_time(ms)时间,移动至新生代。设定为0表示,读入buffer pool的内存分页会在第一次被访问后立即移动至新生代。设定为大于0的数,表示当内存分页被首次访问后,必须等待至少特定的时间才可以被移动至新生代。
将该参数设定为大于0,可以避免仅读取一次的查询过度占用buffer pool的新生代。那些仅被读取一次的内存分页会随着时间的推移,逐渐从老年代中移出。而对于需要进行buffer pool预热的情形,则建议将该参数设定为0,以保证读取的数据能够及时移动至新生代。该参数可以在运行时进行设置,对于那些需要全表扫描或者进行数据备份的场景,可以临时将该参数设定为较大的数值,以避免临时操作对buffer pool使用带来明显的占用和影响。
3. 配置多buffer pool 实例
对于可以将InnoDB 的buffer pool设置为上G的场景,建议将buffer pool配置为多个实例。对应的控制参数已在第一部分进行了介绍。
其原因主要是,在buffer pool容量较大的情况下,许多查询请求都会从buffer pool中获取数据,高并发会带来额外的并发资源维护代价。而将buffer pool 配置为多个实例,InnoDB 保证每个buffer pool其运行完全独立,受独立的信号量控制。被读入buffer pool中的内存分页通过hash随机分配至某个实例中。配置多buffer pool的情况下,建议通过size 及instances 参数的组合,保证每个实例的内存空间大于1G。
4. 降低低频“扫描”(scan)查询导致的低效使用
InnoDB 并没有使用经典的LRU算法管理buffer pool,为了应对可能发生的低频扫描式查询长时间大量占用宝贵的内存资源,InnoDB将buffer pool分为两个区域,新生代和老年代来管理,并且引入old_blocks_pct 和 old_blocks_time 两个参数来允许用户结合自己的使用场景对buffer pool进行配置,以最大化合理利用buffer pool。
如第2节所述,old_blocks_time控制了内存分页被首次读取后必须留在老年代的时间,通过这种控制,有效避免了那些可能出现的“假热”数据一开始就位于buffer pool的头部,此后需要较长时间才能从buffer pool中移除。那些仅读取一次的数据,既然不是热点,一开始限制其位于老年代区,主要带来两个方面的益处:a. 降低其呆在buffer pool中的时间;b. 降低其“权重”,避免可能由于其占用空间过大,而将真正的热点数据挤出buffer pool。
事实上,我认为这里如果想做更加精细的控制,可以映入old_blocks_access_times(将其从老年代移入新生代前必须经过的访问次数),这样,用户可以结合实际的场景取得更加全面的控制。
old_blocks_pct 和 old_blocks_time 两个参数均可以实现动态配置。用户可以结合自己的实际,在不同的使用场景下进行临时配置。总的来说,当不希望大量扫描性的数据侵占buffer pool空间时,可以将pct参数设置较低。而当场景中涉及到的表规模都不大时,可以适当放大pct参数。time参数则需要经过仔细的模拟测试,选择适合应用场景的值。
5. Buffer Pool 数据预读取(read-ahead)
当InnoDB 判断位于同一个内存分区(extent)(一个内存分区由64个内存分页组成)的数据可能将要被读取时,将会对该分区的所有数据进行异步预读取,该预读取将一个分区中不在buffer pool中的内存分页一次性全部读取到pool中。数据预读取技术也是为了提高Mysql的I/O表现。有两种预读取方式:
第一种是线性读取。主要针对顺序读取内存分区中若干分页的情况,触发线性读取的时机由innodb_read_ahead_threshold控制,其值可以设置为0-64中的任一值N,表示当顺序读取位于同一分区的N个分页后,InnoDB会将该分区中的其他数据读取至pool中,该值的默认数值是56。
第二种是随机读取。是指当内存分区中的13个连续的分页被读取至buffer pool后,InnoDB会将该内存分区中的其他分页也读取至pool中。该读取方式由开关参数 innodb_read_ahead_random控制,设置为ON表示启用该预读取方式。
6. 配置Buffer Pool 刷新磁盘(Flushing)
对于在 buffer pool 中发生改变而未同步至数据文件中的数据,InnoDB称之为脏页(dirty-pages)。InnoDB 在 buffer pool 正常运行的同时,以守护的方式将 dirty pages 刷新至磁盘。
刷新的时机由两个参数决定:innodb_max_dirty_pages_pct(默认值为75) 和 innodb_max_dirty_pages_pct_lwm (lwm 表示 low water mark, 低水位)。当dirty pages 达到 innodb_max_dirty_pages_pct_lwm 规定的值时,InnoDB开始刷新磁盘。当dirty pages 达到 innodb_max_dirty_pages_pct 时,InnoDB会以更快的速率执行flushing(位于innodb_io_capacity与innodb_io_capacity_max之间)。
InnoDB 以循环利用的方式管理其日志。在重复使用某一段redo log前,InnoDB 会先将记录在此段redo log但未写入磁盘的记录从buffer pool中刷新至磁盘,从而可能导致I/O性能的短暂突降,此种场景称之为sharp checkpoint。对于写入负载较高的应用场景,sharp checkpoint 可能容易发生,即使当前buffer pool 中的脏页未达到innodb_max_dirty_pages_pct。
InnoDB 基于当前 flushing 的速率以及 redo log 生成的速率估计需要的 flushing 速率,其目的主要是保证当前buffer pool 中有足够可用空间的同时,避免由于 flushing 导致的I/O性能突降。当以较高的速率执行 flushing 时,I/O 可能大部分被其占用,而应用触发的读写得不到及时响应,从而表现出性能突降。
这种自适应的算法能够处理诸如负载突然发生变化的场景。该机制由参数innodb_adaptive_flushing控制,默认为打开状态(ON),可以在运行时或者配置文件中进行动态配置。此算法并不适用于所有场景,对于频繁写入,redo log 经常被写满的情况更加适合。
自适应刷新磁盘还受如下参数的影响:
- innodb_adaptive_flushing_lwm,控制在redo log 达到一定比例后,开始继续自适应磁盘刷新,此时,即使innodb_adaptive_flushing未处于打开状态,也会执行刷新操作。
- innodb_max_dirty_pages_pct_lwm,控制在未达到innodb_max_dirty_pages_pct前就进行磁盘刷新,设为0表示不进行提前刷新。
- innodb_io_capacity_max,控制磁盘刷新速率上限
- innodb_flushing_avg_loops,表示当前算法得出的自适应磁盘刷新速率应用于后续磁盘刷新的次数。对于负载变化剧烈的场景,建议将该参数适当调小,而对于负载相对稳定的场景,建议将该参数适当调高
除此之外,InnoDB 还提供如下两个参数,实现对flushing更加精细的控制:
- innodb_flushing_neighbors,表示是否在flushing时将与dirty pages 位于同一内存分区的其他page也进行刷新。对于HDD(hard disk drive),磁盘写入需要占用时间较长的寻址,将其他同一内存分区中的page同步刷新,可以降低多次写入带来的额外的寻址时间。而对于SSD(solid state drive),寻址时间占用写入操作时间的比例并不高,可以将该参数设置为关闭的状态。
- innodb_lru_scan_length,指定flushing操作扫描的lru list的深度。
7. 保存和恢复Buffer Pool
对于buffer pool较大,在Mysql 启动时需要较长时间进行warm up的情况,InnDB支持在停机时保存Buffer Pool的状态并在下次启动时快速恢复。
【Reference】
1. MySQL 5.6 Reference Manual