Mysql的Innodb存储引擎缓冲池个人理解

Mysql的Innodb存储引擎缓冲池个人理解

综合借鉴了网上其他同行的博文,增加了一些自己的理解。

在数据库数据处理中, 缓冲池在改善性能方面扮演着很重要的角色, 为了保证性能, innodb 维护了自己的缓冲池。 文章大体介绍一下innodb缓冲区实现和管理策略。
在innodb中,需要用到数据页(需要保存到磁盘的数据)均是从这个缓冲池里分配出来的, 因此,可以说,缓冲池在对innodb的性能很大的影响。
几个基本的概念
AWE(Address Windowing Extensions):地址窗口化扩展,允许在 32 位版本的 Windows 操作系统上使用 4 GB 以上的物理内存。最多可支持 64 GB 的物理内存。更多信息请看 http://baike.baidu.com/view/1390438.htm; innodb是支持AWE内存管理的
Frame;帧,16K的虚拟地址空间, 在缓冲池的管理上,整个缓冲区是是以大小为16k的frame(可以理解为数据块)为单位来进行的,frame是innodb中页的大小。
Page: 页,16K的物理内存, page上存的是需要保存到磁盘上的数据, 这些数据可能是数据记录信息, 也可以是索引信息或其他的元数据等;
Control Block:控制块,对于每个frame, 对应一个block, block上的信息是专门用于进行frame控制的管理信息, 但是这些信息不需要记录到磁盘,而是根据读入数据块在内存中的状态动态生成的, 主要包括: 1. 页面管理的普通信息,互斥锁, 页面的状态, awe(windows平台上awe机制的管理信息)等 2. 脏回写(flush)管理信息3. lru控制信息 4. 快速查找的管理信息, 为了便于快速的超找某一个block或frame, 缓冲区里面的block被组织到一些hash表中; 缓冲区中的block的数量是一定得, innodb缓冲区对所管理的block用lru(last recently used)策略进行替换。
互斥访问
缓冲池的整个缓冲区一个数据结构buf_pool进行管理和控制, 一个专门的mutex保护着, 这个mutex是用来保护buf_pool这个控制结构中的数据域的, 并不保护缓冲区中的数据frame以及用于管理的block, 缓冲区里block或者frame中的访问是由专门的读写锁来保护的, 每个block/frame一个。在5.1以前, 每个block是没专门的mutex保护的,如果需要进行互斥保护,直接使用缓冲区的mutex, 结果导致很高的争用; 5.1以后,每个block一个mutex对其进行保护, 从而在很大程度上解缓了对buf_pool的mutex的争用。

内存缓冲池:
  首先将从磁盘读到的页放在缓冲池中,这个过程称为将页“FIX”在缓冲池。下次访问时,若在缓冲池中,则该页被命中;若不在缓冲池中,读取磁盘上的页。修改操作,首先修改缓冲池中的页,然后再以一定的频率刷新到磁盘。通过show variables like ‘innodb_buffer_pool_size’\G;查看缓冲池大小。
  具体的说缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲(insert buffer、自适应哈希索引(adaptive hash index、InnoDB存储的锁信息(lock info、数据字典信息(data dictionary等。注意区别与MyISAM存储引擎。

缓冲池管理用到的几个重要的列表
在缓冲区的管理中, 几个重要的block列表(双向链表):

关键:
IO加载一个page页到缓冲池中生成一个frame, 同时生成了控制信息block,他们都是唯一的。
LRU列表引用此block作为一个节点,当frame修改了数据,相应block也发生了改变,flush列表引用此block作为一个脏页节点。
Mysql插入数据行是实时的,没经过缓冲(内存中只含有部分数据页,若插入内存中的数据页时,可能与磁盘中的记录产生主键或唯一键冲突,
当修改记录行的主键字段或者唯一键字段时,会实时同步主键或唯一键索引到磁盘中,检验是否发生冲突)。
当主键自增时,后一条数据总是插入前一条数据之下,此时磁头就不需要经过寻道和旋转,效率很高
插入缓冲,并不是缓存的一部分,而是物理页对于非聚集索引的插入或更新操作,不是每一次直接插入索引页.而是先判断插入的非聚集索引页是否在缓冲池中.如果在,则直接插入,如果不再,则先放入一个插入缓冲区中.然后再以一定的频率执行插入缓冲和非聚集索引页子节点的合并操作.使用条件:非聚集索引,非唯一

LRU列表: 用来进行lru管理的列表, 列表里的每个block所控制的数据都是当前效的数据;列表中的block基本是照访问的顺序排列的;最近被访问的放在最前面, 最先被访问的放在最后;lru列表中维护中维护了一个LRU_old, 大概指向整个列表的倒数3/8左右, 当增加一个新的block(主语与最近被访问的block的区别)进来的时候, 把新的块刚好放到这个点附近, 具体是前还是后取决于这个LRU_old目前的位置, 这么做的目的是使新增加进来的block放到一个合适的位置, 不至于放到最先(最近被使用过)或最后(最老)

MySQL的InnoDB引擎设置索引及数据缓存池,其中用到的LRU算法来维持缓存的命中率
这里用到了双向链表来作为缓冲池,每个数据节点称为block
该算法采用“中点插入法”:当插入一个新block时,移除表尾最近最少使用的block,在中点插入新block。
这个中点将链表分为两部分:倒数3/8
1.靠近表头的一部分,为young区,这里的block是最近使用的节点
2.靠近表尾的一部分,为old区,这里的block是最近少使用的
该算法通过链表中的block的使用热度来维持各block的位置,其中old区的block为链表满的时候移除的候选区
具体算法如下:(类似LinkedHashMap的lru算法)
1.链表的3/8被设置为old区
2.中点不是链表的中间点,而是old区的表头节点,即old区与young区的相邻的那个节点
3.当读取的数据不在缓冲池里的时候,读取到的block需要插入到链表中,插入点为中点,但是插入的新节点为old区的节点,如果此时old区满了得话,移除表尾的block(LRU节点
4.当读取old区的block时,该节点将变成“young”节点:此节点移动到young区的表头(young区的头部那里
5.在数据库操作中,被访问的节点将移动到young的表头,这样一来,在young区中的未被访问的节点将逐渐往表尾移动,当移动过中点,将变为old区的节点。而old区的节点若被访问到将变为young节点移动到表头,而old区中的为被访问的节点依旧往表尾移动,当表满时,表尾那个block将会被淘汰掉

LRU List——Latest Recent Used(最近最少使用)
  默认大小页的大小16KB,通过show engine innodb status;可以查看当前缓冲池的页数。InnoDB对传统的LRU算法进行了优化。在InnoDB中加入了midpoint。传统的LRU算法当访问到的页不在缓冲区是直接将磁盘页数据调到缓冲区队列列头;而InnoDB并不是直接插入到缓冲区队列的队头,而是插入LRU列表的midpoint位置。这个算法称之为midpoint insertion stategy。默认配置插入到列表长度的5/8处。midpoint由参数innodb_old_blocks_pct控制。
  midpoint之前的列表称之为new列表,之后的列表称之为old列表。可以简单的将new列表中的页理解为最为活跃的热点数据。
  好处:不使用朴素的LRU算法。出于效率考虑,因为可能存在类似于“扫表”等偶然操作,这样做可以避免将热点数据替换掉,可能添加到缓冲区的页是偶然操作用到的页。
  然而mid位置的页不是永久的。为了解决这个问题,InnoDB存储引擎引入了innodb_old_blocks_time来表示页读取到mid位置之后需要等待多久才会被加入到LRU列表的热端。可以通过设置该参数保证热点数据不轻易被刷出。

flush列表: 列表block是那些所管理的数据被修改但是还没更新到磁盘的脏frame,(即缓冲池中的页和磁盘上的页数据产生了不一致),根据修改的先后顺序排列的block列表, 最老的放最后。innodb起来后, 主线程会定期去检查缓冲区中存在的脏页block所占block总数的比例, 一旦占比大于srv_max_buf_pool_modified_pct(70%), 就会试图把一些脏页flush到磁盘(通过checkpoint机制);除了主线程会定期做这个事情外,工作线程在进行数据操作时 ,如果发现没可用的free block, 也会通过flush一些脏页来腾出空间。

Checkpoint:
1、innodb会批量的把buffer pool中的脏页以及redo log 刷新到磁盘,称之为检查点.
2、并不是在一次刷新中刷新所有的内容,因为这样会降低mysql的性能,甚至无法提供服务
3、在恢复的过程中,innodb会向前扫描实务日志,把这些脏数据刷新到磁盘中
4、innodb循环使用它的事务日志,所以旧的日志必然在未来某一时刻被覆盖,innodb必须保证,在旧日志被覆盖之前,与这些旧日志条目相关的脏数据都被刷新到了磁盘
5、如果这一点不能保证,万一服务器crash,buffer pool中的脏页就永远也无法恢复了.
6、所以在切换日志的时候,innodb必然会做检查点,把所的脏页都刷新到磁盘
7、从这个意义上,innodb的事物日志越大,节省的磁盘IO越多,对系统性能越好.但是crash后恢复的时间肯定会变长
8、innodb的检查点每隔几秒钟就会做一次
9、只是经过日志切换后,在日志被冲用前,该日志的内容必须被全部刷新到磁盘,否则系统就会hung住
10、尝试用大一点的事务日志,可以减少检查点过程中写磁盘的次数(之所以节省,是因为IO的合并)

Checkpoint触发条件:
1、每1秒,若buffer pool中的脏页比率超过了srv_max_buf_pool_modified_pct = 70%,则进行checkpoint,刷脏页, flush PCT_IO(100)的dirty pages = 200(参数:innodb_io_capacity 能够对其定义);若采用adaptive flushing,则计算flush rate,进行必要的flush。

2、每10秒,若buffer pool中的脏页比率超过了70%,flush PCT_IO(100)的dirty pages,若buffer pool中的脏页比率未超过70%,flush PCT_IO(10%)的dirty pages = 20;每10s,必定调用一次log_checkpoint,做一次checkpoint

脏页比率 = 需要被flush的页面数/(使用中的页面数+空闲页面数+1)
innodb_adaptive_flushing_lwm —设置redo log flush低水位线,当需要flush的redo log超过这个低水位时,立即强制启用adaptive flushing,即便没设置使用adaptive flush 机制
innodb_io_capacity = N —-设置Innodb后台进程最大的IO性能指标,列如:从buffer pool中刷新数据页,从insert buffer中合并数据等.默认值200,在繁忙的OLTP模式下,需要适当提高.
innodb_io_capacity_max = N —设置Innodb_io_capacity_在紧急情况下的上限值
innodb_flushing_avg_loops = N —-设置Innodb统计前N个page flush 速率,避免太快flush

free list : 所空闲block的列表, 当需要分配一个block时, 从中取出一个block。

Free List
  数据库刚启动的时候,LRU 列表为空,此时需要用到的时候直接将Free列表中的页删除,在LRU列表中增加相应的页,维持页数守恒。

awe_LRU_free_mapped: 用于方便awe的block列表, 这些block所管理的page已经映射到了frame(物理内存对应的虚拟内存空间),其中的元素必定也处于free列表或lru列表中。 这个列表会在当分配一个awe页面时用到。
adaptive hash search: 缓冲区中的页面是通过双向列表的方式组织起来的, 如果需要查找根据页号查找某个页面block的话, 速度不会快,尤其是数据块多的时候; 为了加速查找过程,在用双向列表组织block的时候,也采用了adaptive hash的数据组织, hash的键值(key)是页号(数据页在所在表空间的编号), 这个在一定程度上能加速block的查找, 但是当以awe方式管理内存的话,这种
hash查找方式不会启用
页面读入机制
当读入页面的时候, 首先需要找到一个可用的block, 这个block或者来自于free list或者lru-list; 在读入数据块的时候, block会加上排他锁以防止其他线程再次使用这个block,
会在block中标记出这个块正处于io状态, 然后把io请求加入到io调用请求对列; 完成读入操作时, 再释放block上的排它锁更新io状态。
数据页面的读取写入
缓冲池中的数据基本是采用同步aio的方式,这里的同步aio的意思是: 工作线程发出读或写请求后,请求会被放到一个读写请求队列中,由专门的io线程负责写盘和读盘,而工作线程则等待io的完成,
注意, 这里是等待完成,而不会放弃执行,因而称作为同步aio.
提前读(预读)机制
为了优化数据读入性能, 缓冲区读入采取了提前读的机制(当然,这个机制可以配置成不**,当然, 提前读的机制是基于数据局部性的原理来的,
这就是为什么采用取值过于随机的字段作为主键会导致性能降低的原因之一:过于随机的主键会导致提前读不起作用, 而且会导致更多的换页行为; 这个预读取对于上层的功能如索引管理是透明的,
对于上层的功能来说, 需要提供的信息是需要读取的页是否有后继页和前置页。 两种预读机制: 线性预读和随机预读
线性预读: 当第一读缓冲区中某一个已经存在(注意,这里必须是已经存在于缓冲区额数据块)的数据块时, 会检查这个数据块是不是处于所谓的线性预读区域(比如, 区域大小是64, 当前读入的也是100,那么所在的预读区域是65 ~ 128),如果是, 则统计一下这个区域中目前没有被访问过的页面,如果数量多于预读机制设定的预读数量,则放弃本次预读, 这个其实是检查目前的区域是否还大量的页面没被访问过, 如果是的话, 自然没必要去做预读了; 否则, 取得读取页面的后继页和前置页(照数据页的自然顺序?,然后检查后继页或者前置页是否是一个新区域的边界,如果是, 则发出读取
该区域里面的数据页面的异步请求。
随机读: 当读取一个页面时, 根据所读取页的位置计算出该页面所在的随机预读区域, 区域的计算也基于设定的区域页面数量来计算的(如前面关于线性预读的例子), 然后根据lru对列的信息计算该区域中多少块最近被访问到了, 如果被访问的数量达到一定的额度(这个额度是根据预读区域的大小计算出来的, 5 + 预读区域大小 / 8),则预读取该区域中目前还没读取到缓冲区的块.
show innodb status 中关于buffer pool的输出
Buffer pool size 262144 整个缓冲池中的页的数量, 包括flush列表中的和lru列表中的,以及被分配出去的页的数量
Free buffers 0 free列表中的页的数量
Database pages 258053 分配出去, 正在被使用页的数量
Modified db pages 37491 flush列表中的数量
Pending reads 0 发出了请求但没完成的io读个数
Pending writes: LRU 0, flush list 0, single page 0 发出了请求但没完成的io读个数在各个列表上的体现
Pages read 57973114, created 251137, written 10761167, 从磁盘上读取出来的页数, 在内存中建立了页面但是没从磁盘上读取出来的页面数以及写入了的页面数
9.79 reads/s, 0.31 creates/s, 6.00 在刚过去的时间间隔里, 平均每秒的读取数和新建数
Buffer pool hit rate 999 / 1000 命中率, 缓冲区中读到的页 / 总共发出的读页数
与缓冲池相关的状态变量及含义
| Innodb_buffer_pool_pages_data 分配出去, 正在被使用页的数量
| Innodb_buffer_pool_pages_dirty 脏页但没被flush除去的页面数
| Innodb_buffer_pool_pages_flushed 已经flush的页面数
| Innodb_buffer_pool_pages_free 当前空闲页面数
| Innodb_buffer_pool_pages_latched 当前被锁住的页面数
| Innodb_buffer_pool_pages_misc 用于管理功能的页面数, 如adaptive hash等
| Innodb_buffer_pool_pages_total 缓冲区总共的页面数
| Innodb_buffer_pool_read_ahead_rnd 随机预读的次数
| Innodb_buffer_pool_read_ahead_seq 线性预读的次数
| Innodb_buffer_pool_read_requests 总共从缓冲池中缓存的页面中读取出的页数
| Innodb_buffer_pool_reads 从磁盘上一页一页的读取的页数,从缓冲池中读取页面, 但缓冲池里面没, 就会从磁盘读取
| Innodb_buffer_pool_wait_free 缓冲池等待空闲页的次数, 当需要空闲块而系统中没有时, 就会等待空闲页面
| Innodb_buffer_pool_write_requests 缓冲池总共发出的写请求次数
| Innodb_data_fsyncs 总共完成的fsync次数
| Innodb_data_pending_fsyncs innodb当前等待的fsync次数
| Innodb_data_pending_reads innodb当前等待的读的次数
| Innodb_data_pending_writes innodb当前等待的写的次数
| Innodb_data_read | 总共读入的字节数
| Innodb_data_reads innodb完成的读的次数
| Innodb_data_writes innodb完成的写的次数
| Innodb_data_written 总共写出的字节数
关于缓冲池的关键的配置变量
innodb_buffer_pool_size: 缓冲池的大小