1.前言
innodb 存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理,因此可将其视为基于磁盘的数据库系统(Disk-base database).在数据库系统中,由于cpu速度和磁盘速度之前的鸿沟,基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能
2.缓冲池
缓冲池简单地来说就是一块内存区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。在数据库中进行读取页的操作,首先将磁盘督导的页存放在缓冲池中,这个过程叫做”FIx“在缓冲池中。下一次再读相同的页的同时,首先判断该页是否在缓冲池中,若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。
对于数据库中页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上,这里需要注意的是,页从缓冲池刷回到磁盘的操作并不是每次页发生更新时触发,而是通过一种称为checkpoint的机制刷回磁盘。同样这也是为了提高数据库的整体性能。
注意缓冲池的大小直接影响着数据库的整体性能。这里可以通过配置参数innodb_buffer_pool_size来设置
root@localhost 17:33: [liulin]> show variables like ‘%innodb_buffer_pool_size%‘; +-------------------------+-----------+ | Variable_name | Value | +-------------------------+-----------+ | innodb_buffer_pool_size | 134217728 | +-------------------------+-----------+ 1 row in set (0.00 sec)
具体来看,缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲、自适应哈希、innodb存储的锁信息、数据字典信息等。不能简单地认为,缓冲池只是缓存索引页和数据页,它们只是占缓冲池很大的一部分而已。
图为 innodb内存数据对象
从innodb 1.0.x版本开始,允许有多个缓冲池实例,每个页根据哈希值平均分配到不同缓冲池实例中,这样做的好处是减少数据库内部的资源的竞争,增加数据库的并发处理能力,可以通过参数:innodb_buffer_pool_instances来进行配置,默认是1
root@localhost 19:51: [liulin]> show variables like ‘%innodb_buffer_pool_instances%‘; +------------------------------+-------+ | Variable_name | Value | +------------------------------+-------+ | innodb_buffer_pool_instances | 1 | +------------------------------+-------+ 1 row in set (0.00 sec)
而且如果想要看每个缓冲池实例对象运行的状态,可以通过information_schema架构下的表innodb_buffer_pool_stats来观察缓冲池的状态。
3 LRU List、Free List和Flush List
可以看出缓冲池是一个很大的内存区域,其中存放各种类型的页,那么innodb存储引擎是怎么对这么大的内存区域进行管理呢?
通过来说,数据库的缓冲池是通过LRU(Latest Recent Used,最近最少使用)算法来进行管理的,即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池不能存放新读取到的页时,将首先释放LRU列表中尾端的页。
在innodb存储引擎中,缓冲池页的大小默认是16KB,同样使用LRU算法对缓冲池进行管理,稍有不同的是innodb存储引擎对传统的LRU算法做了一些优化,在Innodb的存储引擎中,LRU列表中还加入了midpoint位置,新读到的页,虽然是最新访问的页,但并不是直接放入到LRU列表的首部,而是放入到LRU列表的midpoint位置,这个算法在innodb中被称为midpoint insertion strategy.默认配置下,该位置在LRU列表长度的5/8处。midpoint位置可由参数innodb_old_blocks_pct控制,
root@localhost 20:07: [information_schema]> show variables like ‘%innodb_old_blocks_pct%‘; +-----------------------+-------+ | Variable_name | Value | +-----------------------+-------+ | innodb_old_blocks_pct | 37 | +-----------------------+-------+ 1 row in set (0.00 sec)
从上面可以看出,参数innodb_old_blocks_pct默认值是37,表示新读取的页插入到LRU列表尾端的37%的位置(差不多3/8的位置)。在innodb存储引擎中,把midpoint之后的列表称为old列表,之前的列表称为new列表。可以简单理解为new列表中的页都是最为活跃的热点数据。
innodb存储引擎还有一个参数来进一步管理LRU列表,这个参数是innodb_old_block_time,用于表示页读取到mid位置后需要等待多久才会被加入到LRU列表的热端,因此当需要执行全表扫描这种很消耗性能且是否占用内存的这种语句时,可以通过下面的方法尽可能使LRU列表中热点数据不被刷出。
root@localhost 20:17: [information_schema]> show variables like ‘%innodb_old_blocks_time%‘; +------------------------+-------+ | Variable_name | Value | +------------------------+-------+ | innodb_old_blocks_time | 1000 | +------------------------+-------+ 1 row in set (0.00 sec)
这里默认是1000s,
mysql> set global innodb_old_blocks_time=1000;
# data or index scan operation
mysql> set global innodb_old_blocks_time=0;
这里如果用户预估自己活跃的热点数据不止63%,那么在执行sql语句前,还可以通过下面的语句来减少热点页可能被刷出的概率
mysql> set global innodb_old_blocks_pct=20;
LRU列表用来管理已经读取的页,但当数据库刚启动时,LRU列表是空的,即没有任何页,这时页都存放在free 列表中,当需要从缓冲池中分页时,首先从Free列表中查找是否有可用的空闲页,若有则将该页从Free列表中删除,放入到LRU列表中。否则,根据LRU算法,淘汰LRU列表末尾的页,将该内存空间分配给新的页。当页从LRU列表的old部分加入到new部分时,称此时发生的操作为page made young,而因为innodb_old_blocks_time的设置而导致页没有从old部分移动到new部分的操作称为page not made young.可以通过show engine innodb status来观察LRU列表及Free列表的使用情况和运行状态。
---------------------- BUFFER POOL AND MEMORY ---------------------- Total large memory allocated 137428992 Dictionary memory allocated 145200 Buffer pool size 8191 Free buffers 7810 Database pages 381 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 347, created 34, written 46 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: 381, unzip_LRU len: 0
通过命令show engine innodb status可以看到:当前buffer pool size 共有8191个页,即8191x16K,总共128M(默认值)。Free buffers表示当前Free列表中页的数量,database pages表示LRU列表中页的数量。可能的情况是Free buffer与database pages的数量之和不等于buffer pool size。但是上面是等于了,如果要是不等于的话,可能因为缓冲池中的页还可能会被分配给自适应哈希索引、lock信息、insert buffer等页,而这部分页是不需要LRU算法进行维护,因此不存在LRU列表。page made young 显示了LRU列表中页移动到前端的次数。
此外可以通过表innodb_buffer_pool_stats观察缓冲池的运行状态,且可以通过innodb_buffer_page_LRU来观察每个LRU列表中每个页的具体信息。
innodb存储引起还支持页的压缩技术,这里就不一一叙述了。
在LRU列表中的页被修改后,称该页为脏页(dirty page),即缓冲池中的页和磁盘上的页的数据产生了不一致。这时数据库会通过checkpoint机制将脏页刷新到磁盘,而Flush列表中的页即脏页列表。需要注意的是,脏页既存在于LRU列表中,也存在于Flush列表中,LRU列表用来管理缓冲池中页的可用性,Flush列表用来管理将页刷回磁盘,二者互不影响。
4.重做日志缓冲
innodb存储引擎的内存区域除了有缓冲池外,还有重做日志缓冲(redo log buffer )。innodb存储引擎首先将重做日志信息先放入到这个缓冲区,然后按照一定频率将其刷新到重做日志文件。重做日志缓冲一般不需要设置很大,因为一般情况下每一秒会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。该值可以配置参数innodb_log_buffer_size控制,默认为16M;
在通常情况下,16M的重做日志缓冲足以满足绝大部分的应用,因为重做日志在下列三种情况下会将重做日志换冲的内容刷新到外部磁盘的重做日志文件中。
- Master Thread 每一秒将重做日志缓冲刷新到重做日志文件
- 每个事务提交时会将重做日志缓冲刷新到重做日志文件
- 当重做日志缓冲池剩余空间小1/2时,重做日志缓冲刷新到重做日志文件。
5 额外的内存池
额外的内存池通常被DBA忽略,它们认为该值并不是十分重要,事实上恰恰相反,该值同样也十分重要,在innodb存储引擎中,对内存的管理是通过一种称为内存堆(heap)的方式进行的。在对于一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池进行申请,例如,分配了缓冲池(innodb_buffer_pool),但是每个缓冲池中的帧缓冲(frame buffer)还有对应的缓冲控制对象(buffer contronl block),这些对象记录了一些诸如LRU、锁、等待等信息,而这个对象的内存需要从额外内存池中进行申请。因此,在申请了很大的innodb缓冲池时,也应该考虑相应地增加这个值。