InnoDB有多个内存块,可以认为这些内存块组成了一个大的内存池 。。。
1 概述
InnoDB有多个内存块,你可以认为这些内存块组成了一个大的内存池,负责如下工作:
- 维护所有进程/线程需要访问的多个内部数据结构。
- 缓存磁盘上的数据,方便快速地读取,并且在对磁盘文件的数据进行修改之前在这里缓存。
- 重做日志(redo log)缓冲。
- 。。。
后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。此外,将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常情况下InnoDB能恢复到正常运行状态。
2 后台线程
默认情况下,InnoDB存储引擎的后台线程有4类——IO thread,1个master thread,1个锁(lock)监控线程,1个错误监控线程。
IO线程分别是insert buffer thread、log thread、read thread、write thread。在Linux平台下,IO thread的数量不能进行调整,但是在Windows平台下可以通过参数innodb_file_io_threads来增大IO thread。InnoDB Plugin版本开始增加了默认IO thread的数量,默认的read thread和write thread分别增大到了4个,并且不再使用innodb_file_io_threads参数,而是分别使用innodb_read_io_threads和innodb_write_io_threads参数。
show variables like ‘innodb_version’\G
show variables like ‘innodb_%io_threads’\G
3 内存
InnoDB存储引擎内存由以下几个部分组成:缓冲池(buffer pool)、重做日志缓冲池(redo log buffer)以及额外的内存池(additional memory pool),分别由配置文件中的参数innodb_buffer_pool_size和innodb_log_buffer_size的大小决定。
如下所示:(分别以字节显示)。
show variables like ‘innodb_buffer_pool_size’\G
show variables like ‘innodb_log_buffer_size’\G
show variables like ‘innodb_additional_mem_pool_size’\G
3.1 缓冲池
缓冲池是占最大块内存的部分,用来存放各种数据的缓存。因为InnoDB的存储引擎的工作方式总是将数据库文件按页(每页16K)读取到缓冲池,然后按最近最少使用(LRU)的算法来保留在缓冲池中的缓存数据。如果数据库文件需要修改,总是首先修改在缓存池中的页(发生修改后,该页即为脏页),然后再按照一定的频率将缓冲池的脏页刷新(flush)到文件。可以通过命令show engine innodb status\G来查看innodb_buffer_pool的具体使用情况,如下所示。
在BUFFER POOL AND MEMORY里可以看到InnoDB存储引擎缓冲池的使用情况,buffer pool size表明了一共有多少个缓冲帧(buffer frame),每个buffer frame为16K,所以这里一共分配了8192*16/1024/1024G内存的缓冲池。Free buffers表示当前空闲的缓冲帧,Database pages表示已经使用的缓冲帧,Modified db pages表示脏页的数量。就当前状态看来,这台数据库的压力并不大,因为在缓冲池中有大量的空闲页可供数据库进一步使用。
注意:show engine innodb status的命令显示的不是当前的状态,而是过去某个时间范围内InnoDB存储引擎的状态,类似这样的字眼 Per second averages calculated from the last 24 seconds表示的信息是过去24秒内的数据库状态。
具体来看,缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲(insert buffer)、自适应哈希索引(adaptive hash index)、InnoDB存储的锁信息(lock info)、数据字典信息(data dictionary)等。不能简单地认为,缓冲池只是缓存索引页和数据页,它们只是占缓冲池很大的一部分而已。
下图很好地显示了InnoDB存储引擎中内存的结构情况。
3.2 日志缓冲
严格地说,应该是重做(redo)日志缓冲。将重做日志信息先放入这个缓冲区,然后按一定频率将其刷新到重做日志文件。该值一般不需要设置为很大,因为一般情况下每一秒钟就会将重做日志缓冲刷新到日志文件,因此我们只需要保证每秒产生的事务量在这个缓冲大小之内即可。
3.3 额外的内存池
在InnoDB存储引擎中,对内存的管理是通过一种称为内存堆(heap)的方式进行的。在对一些数据结构本身分配内存时,需要从额外的内存池中申请,当该区域的内存不够时,会从缓冲池中申请。InnoDB实例会申请缓冲池(innodb_buffer_pool)的空间,每个缓冲池中的帧缓冲(frame buffer)还有对应的缓冲控制对象(buffer control block),这些对象记录了诸如LRU、锁、等待等方面的信息,而这个对象的内存需要从额外内存池中申请。因此,当你申请了很大的InnoDB缓冲池时,这个值也应该相应增加。
4 Master Thread
innoDB存储引擎的主要工作都是由Master Thread 完成的。master thread的线程优先级别最高。其内部几个循环**(loop)组成:主循环(loop),后台循环(background loop),刷新循环(flush loop),暂停循环(suspend loop)**.master thread会根据数据运行的状态在loop,background loop,flush loop和suspend loop中进行切换.
loop称为主循环,因为大多数的操作都在这个循环中,其中有两大部分操作:每秒钟的操作和每10秒的操作。
每秒一次的操作
- 日志缓冲刷新到磁盘,即使这个事务还没有提交(总是)
- 合并插入缓冲(可能)
- 至多刷新100个innodb的缓冲池中的脏页到磁盘(可能)
- 如果当前没有用户活动,切换到background loop(可能)
即使某个事务还没有提交,innodb存储引擎仍然会每秒将重做日志缓冲中的内容刷新到重做日志文件.这一点是必须知道的,这可以很好地解释为什么再大的事务commit的时间也是很快的
合并插入缓冲(insert buffer)并不是每秒都发生.innodb存储引擎会判断当前一秒内发生的io次数是否小于5次,如果小于5次,innodb认为当前的io压力很小,可以执行合并插入缓冲的操作
同样,刷新100个脏页也不是每秒都在发生.innodb存储引擎通过判断当前缓冲池中的脏页比例(buf_get_modified_ratio_pct)是否超过了配置文件中innodb_max_dirty_pages_pct这个参数(默认为90,代表90%),如果超过了这个阙值,innodb存储引擎认为需要做磁盘同步操作,将100个脏页写入磁盘.
**每10秒的操作 **
- 刷新100个脏页到磁盘(可能)
- 合并至多5个插入缓冲(总是)
- 将日志缓冲刷新到磁盘(总是)
- 删除无用的undo页(总是)
- 刷新100个或者10个脏页到磁盘(总是)
- 产生一个检查点(总是)
在以上过程中,innodb存储引擎会先判断过去10秒之内的磁盘的io操作是否小于200次.如果是,innodb存储引擎认为当前有足够的磁盘io能力,因此将100个脏页刷新到磁盘.接着,innodb存储引擎会合并插入缓冲.不同于1秒操作时可能发生的合并插入缓冲操作,这次的合并插入缓冲操作总会在这个阶段进行.之后,innodb存储引擎会再执行一次将日志缓冲刷新到磁盘的操作,这与每秒发生的操作是一样的.
接着innodb存储引擎会执行一步full purge操作,即删除无用的undo页.对表执行update,delete这类操作时,原生的行被标记为删除,但是因为一致性读的关系,需要保留这些行版本的信息.但是在full purge过程中,innodb存储引擎会判断当前事务系统中已被删除的行是否可以删除,比如有时候可能还有查询操作需要读取之前版本的undo信息,如果可以,innodb会立即将其删除.从源代码中可以发现,innodb存储引擎在操作full purge时,每次最多删除20个undo页.
然后innodb存储引擎会判断缓冲池中脏页的比例,如果有超过70%的脏页,则刷新100个脏页到磁盘;如果脏页的比例小于70%,则只需要刷新10%的脏页到磁盘
最后,innodb存储引擎会产生一个检查点(checkpoint),innodb存储引擎的检查点也被称为模糊检查点(fuzzy checkpoint).innodb存储引擎在checkpoint时并不会把所有缓冲池中的脏页都写到磁盘,因为这样可能会对性能产生影响,而只是将最老日志序列号的页写入磁盘。
接着来看background loop,若当前没有用户活动(数据库空闲时)或者数据库关闭时,就会切换到这个循环.这个循环会执行如下操作:
- 删除无用的undo页(总是).
- 合并20个插入缓冲(总是).
- 跳回到主循环(总是).
- 不断刷新100个页,直到符合条件(可能,跳转到flush loop中完成)
如果flush loop中也没有什么事情可以做了,innodb存储引擎会切换到supend_loop,将master thread挂起,等待事件的发生.若启用了innodb存储引擎,却没有使用任何存储引擎的表,那么master thread总是处于挂起状态.
4.1 master thread潜在问题
(1) 最多只会刷新100个脏页,合并5个插入缓冲;
问题:当密集写时,“忙不过来”,很慢;且发生宕机需要恢复时,由于很多数据还没有刷新到磁盘,可能会导致恢复需要很长的时间。
修正:参照google patch,提供磁盘IO吞吐量参数innodb_io_capacity,默认200,刷新100%脏页、合并5%插入缓冲。
(2) 脏页比例innodb_max_dirty_pages_pct默认为90%
问题:该值“太大了”,如果有很大的内存或者数据库服务器的压力很大时,刷新脏页的速度反而可能会降低;同时,在数据库恢复阶段可能需要更多的时间。
修正:innodb_max_dirty_pages_pct默认90%->默认75%,且通过innodb_adaptive_flushing自适应地刷新影响每一秒刷新脏页的数量(通过一个buf_flush_get_desired_flush_rate函数判断需要刷新脏页最合适的数量,buf_flush_get_desired_flush_rate函数通过判断产生重做日志的速度来判断最合适的刷新脏页的数量)。
5 关键特性
InnoDB的存储引擎主要包括:插入缓存(insert buffer)、两次写(double write)、自适应哈希(Adaptive Hash index)、异步IO(Async IO)、刷新邻接页(Flush Neighbor Page)
5.1 插入缓存
1.1 Insert Buffer
Insert Buffer是InnoDB存储引擎关键特性中最令人激动与兴奋的一个功能。不过这个名字可能会让人认为插入缓冲是缓冲池中的一个组成部分。其实不然,InnoDB缓冲池中有Insert Buffer信息固然不错,但是InsertBuffer和数据页一样,也是物理页的一个组成部分。
一般情况下,主键是行唯一的标识符。通常应用程序中行记录的插入顺序是按照主键递增的顺序进行插的。因此,插入聚集索引一般是顺序的,不需要磁盘的随机读取。因为,对于此类情况下的插入,速度还是非常快的。(如果主键类是UUID这样的类,那么插入和辅助索引一样,也是随机的。)
如果索引是非聚集的且不唯一。在进行插入操作时,数据的存放对于非聚集索引叶子节点的插入不是顺序的,这时需要离散地访问非聚集索引页,由于随机读取的存在而导致了插入操作性能下降。这是因为B+树的特性决定了非聚集索引插入的离散性。
Insert Buffer的设计,对于非聚集索引的插入和更新操作,不是每一次直接插入到索引页中,而是先判断插入非聚集索引页是否在缓冲池中,若存在,则直接插入,不存在,则先放入一个Insert Buffer对象中。数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放在另一个位置。然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge(合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。
需要满足的两个条件:
- 索引是辅助索引;
- 索引不是唯一的。
辅助索引不能是唯一的,因为在插入缓冲时,数据库并不去查找索引页来判断插入的记录的唯一性。如果去查找肯定又会有离散读取的情况发生,从而导致Insert Buffer失去了意义。
1.2 Change Buffer
InnoDB从1.0.x版本开始引入了Change Buffer,可将其视为Insert Buffer的升级。从这个版本开始,InnoDB存储引擎可以对DML操作——INSERT、DELETE、UPDATE都进行缓冲,他们分别是:Insert Buffer、Delete Buffer、Purge buffer。
当然和之前Insert Buffer一样,Change Buffer适用的对象依然是非唯一的辅助索引。
对一条记录进行UPDATE操作可能分为两个过程:
- 将记录标记为已删除(Delete Buffer);
- 真正将记录删除(Purge buffer)。
因此Delete Buffer对应UPDATE操作的第一个过程,即将记录标记为删除。Purge Buffer对应UPDATE操作的第二个过程,即将记录真正的删除。同时,InnoDB存储引擎提供了参数innodb_change_buffering,用来开启各种Buffer的选项。该参数可选的值为:inserts、deletes、purges、changes、all、none。inserts、deletes、purges就是前面讨论过的三种情况。changes表示启用inserts和deletes,all表示启用所有,none表示都不启用。该参数默认值为all。
从InnoDB 1.2.x版本开始,可以通过参数innodb_change_buffer_max_size来控制Change Buffer最大使用内存的数量,innodb_change_buffer_max_size值默认为25,表示最多使用1/4的缓冲池内存空间。而需要注意的是,该参数的最大有效值为50。
在MySQL 5.5版本中通过命令SHOW ENGINE INNODB STATUS,可以观察到类似如下的内容:
1 | SHOW ENGINE INNODB STATUS\G; |
可以看到这里显示了merged operations和discarded operation,并且下面具体显示Change Buffer中每个操作的次数。insert表示Insert Buffer;delete mark表示Delete Buffer;delete表示Purge Buffer;discarded operations表示当Change Buffer发生merge时,表已经被删除,此时就无需再将记录合并(merge)到辅助索引中了。
1.3 Merge Insert Buffer
通过前面的小节读者应该已经知道了Insert/Change Buffer是一棵B+树。若需要实现插入记录的辅助索引页不在缓冲池中,那么需要将辅助索引记录插入到这棵实际B+树中。但是Insert Buffer中的记录何时合(merge)到真正的辅助索引中呢?
概括地说,Merge Insert Buffer的操作可能发生在以下几种情况下:
- 辅助索引页被读取到缓冲池时;
- Insert Buffer Bitmap页追踪到该辅助索引页已无可用空间时;
- Master Thread。
第一种情况为当辅助索引页被读取到缓冲池中时,例如这在执行正常的SELECT查询操作,这时需要检查Insert Buffer Bitmap页,然后确认该辅助索引页是否有记录存放于Insert Buffer B+树中。若有,则将Insert Buffer B+树中该页的记录插入到该辅助索引页中。可以看到对该页多次的记录操作通过一次操作合并到了原有的辅助索引页中,因此性能会有大幅提高。
Insert Buffer Bitmap页用来追踪每个辅助索引页的可用空间,并至少有1/32页的空间。若插入辅助索引记录时检测到插入记录后可用空间会小于1/32页,则会强制进行一个合并操作,即强制读取辅助索引页,将Insert Buffer B+树中该页的记录及待插入的记录插入到辅助索引页中。这就是上述所说的第二种情况。
还有一种情况,之前在分析Master Thread时曾讲到,在Master Thread线程中每秒或每10秒会进行一次Merge Insert Buffer的操作,不同之处在于每次进行merge操作的页的数量不同。
5.2 两次写
1.1 doublewrite应用场景
我们知道,innodb的数据页一般大小是16KB,MySQL存取数据的最小单位也是页,而操作系统并不能保障一个数据页的原子性,也就是说当写入数据时,有可能在一个页中写入一半时(比如8K)数据库宕机,这种情况称为部分写失效(partial page write),从而导致数据丢失。
大家也许会问,难道我不可以根据redo log进行数据恢复吗?答案是肯定的也是否定的,要分为两种情况:1、数据库宕机,物理文件完好无损,是可以通过redo log进行崩溃恢复。2、数据库宕机,正在刷新到磁盘的页发生partial page write,而正好在磁盘上的这个数据页由于宕机发生损坏,这时就无法通过redo log进行数据恢复了,为什么?我们必须要清楚的认识到,redo log里记录的是对页的物理操作!比如一条redo记录”page number xx,偏移量 800 写记录 “this is abc””,那当页损坏时,这条redo记录还有意义吗?于是在这种特殊情况下,doublewrite就派上用场啦!
2.2 doublewrite结构及流程
doublewrite由两部分组成,一部分为内存中的doublewrite buffer,其大小为2MB,另一部分是磁盘上共享表空间(ibdata x)中连续的128个页,即2个区(extent),大小也是2M。doublewrite工作流程如下:
- 当一系列机制(main函数触发、checkpoint等)触发数据缓冲池中的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次、每次1MB顺序写入共享表空间的物理磁盘上。
- 马上调用fsync函数,同步脏页进磁盘。由于在这个过程中,doublewrite页的存储时连续的,因此写入磁盘为顺序写,性能很高;完成doublewrite后,再将脏页写入实际的各个表空间文件,这时写入就是离散的了。各模块协作情况如下图(第一步应为脏页产生的redo记录logbuffer,然后logbuffer写入redo log file,为简化次要步骤直接连线表示):
查看doublewrite工作情况,可以执行命令:
1 | show global status like 'innodb_dblwr%'\G |
以上数据显示,doublewrite一共写了 61932183个页,一共写了15237891次,从这组数据我们可以分析,之前讲过在开启doublewrite后,每次脏页刷新必须要先写doublewrite,而doublewrite存在于磁盘上的是两个连续的区,每个区由连续的页组成,一般情况下一个区最多有64个页,所以一次IO写入应该可以最多写64个页。而根据以上我这个系统Innodb_dblwr_pages_written与Innodb_dblwr_writes的比例来看,大概在4左右,远远还没到64,所以从这个角度也可以看出,系统写入压力并不高。
参数skip_innodb_doublewrite可以禁止使用两次写功能,这时可能会发生前面提及的写失效问题。不过,如果你有多台从服务器(slave server),需要提供较快的性能(如slave上做的是RAID0),也许启用这个参数是一个办法。不过,在需要提供数据高可靠性的主服务器(master server)上,任何时候我们都应确保开启两次写功能。
注意:有些文件系统本身就提供了部分写失效的防范机制,如ZFS文件系统。在这种情况下,我们就不要启用doublewrite了。
2.3 崩溃恢复
如果操作系统在将页写入磁盘的过程中发生崩溃,如上图,在恢复过程中,innodb存储引擎可以从共享表空间的doublewrite中找到该页的一个最近的副本,将其复制到表空间文件,再应用redo log,就完成了恢复过程。因为有副本所以也不担心表空间中数据页是否损坏。
2.4 自适应哈希索引
哈希是一种非常快的查找方法,在一般情况时间复杂度为O(1)。而B+树的查找次数,取决于B+树的高度,在生成环境中,B+树的高度一般为3-4层,只需要查询3-4次。
InnoDB存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以提升速度,就建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index, AHI)。AHI是通过缓冲池的B+树页构造而来的。因此建立的速度非常快,且不要对整张表构建哈希索引。InnoDB存储引擎会自动根据访问的频率和模式来自动的为某些页建立哈希索引。
AHI有一个要求,对这个页的连续访问模式(查询条件)必须一样的。例如联合索引(a,b)其访问模式可以有以下情况:
- WHERE a=XXX;
- WHERE a=xxx AND b=xxx。
若交替进行上述两张查询,InnoDB存储引擎不会对该页构造AHI。此外AHI还有如下要求:
- 以该模式访问了100次;
- b.页通过该模式访问了N次,其中N=页中记录/16。
根据官方文档显示,启用AHI后,读取和写入的速度可以提高2倍,负责索引的链接操作性能可以提高5倍。其设计思想是数据库自由化的,无需DBA对数据库进行人为调整。
2.5 异步Io
为了提高磁盘操作性能,当前的数据库系统都采用异步IO的方式来处理磁盘操作。InnoDB也是如此。
与AIO对应的是Sync IO,即每进行一次IO操作,需要等待此次操作结束才能继续接下来的操作。但是如果用户发出的是一条索引扫描的查询,那么这条SQL语句可能需要扫描多个索引页,也就是需要进行多次IO操作。在每扫描一个页并等待其完成再进行下一次扫描,这是没有必要的。用户可以在发出一个IO请求后立即再发出另外一个IO请求,当全部IO请求发送完毕后,等待所有IO操作完成,这就是AIO。
AIO的另外一个优势是进行IO Merge操作,也就是将多个IO合并为一个IO操作,这样可以提高IOPS的性能。
在InnoDB 1.1.x之前,AIO的实现是通过InnoDB存储引擎中的代码来模拟的。但是从这之后,提供了内核级别的AIO的支持,称为Native AIO。Native AIO需要操作系统提供支持。Windows和Linux都支持,而Mac则未提供。在选择MySQL数据库服务器的操作系统时,需要考虑这方面的因素。
MySQL可以通过参数innodb_use_native_aio来决定是否启用Native AIO。在InnoDB存储引擎中,read ahead方式的读取都是通过AIO完成,脏页的刷新,也是通过AIO完成。
2.6 刷新邻接页
InnoDB存储引擎在刷新一个脏页时,会检测该页所在区(extent)的所有页,如果是脏页,那么一起刷新。这样做的好处是通过AIO可以将多个IO写操作合并为一个IO操作。该工作机制在传统机械磁盘下有显著优势。但是需要考虑下吧两个问题:
- 是不是将不怎么脏的页进行写入,而该页之后又会很快变成脏页?
- 固态硬盘有很高IOPS,是否还需要这个特性?
为此InnoDB存储引擎1.2.x版本开始提供参数innodb_flush_neighbors来决定是否启用。对于传统机械硬盘建议使用,而对于固态硬盘可以关闭。
6 启动 关闭与恢复
InnoDB存储引擎是MySQL的存储引擎之一,因此InnoDB存储引擎的启动和关闭更准确地是指在MySQL实例的启动过程中对InnoDB表存储引擎的处理过程。
6.1 innodb_fast_shutdown
在关闭时,参数innodb_fast_shutdown影响着表的存储引擎为InnoDB的行为。该参数可取值为0、1、2。
- 0代表当MySQL关闭时,InnoDB需要完成所有的full purge和merge insert buffer操作,这会需要一些时间,有时甚至需要几个小时来完成。如果在做InnoDB plugin升级,通常需要将这个参数调为0,然后再关闭数据库。
- 1是该参数的默认值,表示不需要完成上述的full purge和merge insert buffer操作,但是在缓冲池的一些数据脏页还是会刷新到磁盘。
- 2表示不完成full purge和merge insert buffer操作,也不将缓冲池中的数据脏页写回磁盘,而是将日志都写入日志文件。这样不会有任何事务会丢失,但是MySQL数据库下次启动时,会执行恢复操作(recovery)。
当正常关闭MySQL数据库时,下一次启动应该会很正常。但是,如果没有正常地关闭数据库,如用kill命令关闭数据库,在MySQL数据库运行过程中重启了服务器,或者在关闭数据库时将参数innodb_fast_shutdown设为了2,MySQL数据库下次启动时都会对InnoDB存储引擎的表执行恢复操作。
6.2 innodb_force_recovery
参数innodb_force_recovery影响了整个InnoDB存储引擎的恢复状况。该值默认为0,表示当需要恢复时执行所有的恢复操作。当不能进行有效恢复时,如数据页发生了corruption,MySQL数据库可能会宕机,并把错误写入错误日志中。
但是,在某些情况下,我们可能并不需要执行完整的恢复操作,我们自己知道如何进行恢复。比如正在对一个表执行alter table操作,这时意外发生了,数据库重启时会对InnoDB表执行回滚操作。对于一个大表,这需要很长时间,甚至可能是几个小时。这时我们可以自行进行恢复,例如可以把表删除,从备份中重新将数据导入表中,这些操作的速度可能要远远快于回滚操作。
innodb_force_recovery还可以设置为6个非零值:1~6。大的数字包含了前面所有小数字的影响,具体情况如下。
- (SRV_FORCE_IGNORE_CORRUPT):忽略检查到的corrupt页。
- (SRV_FORCE_NO_BACKGROUND):阻止主线程的运行,如主线程需要执行full purge操作,会导致crash。
- (SRV_FORCE_NO_TRX_UNDO):不执行事务回滚操作。
- (SRV_FORCE_NO_IBUF_MERGE):不执行插入缓冲的合并操作
- (SRV_FORCE_NO_UNDO_LOG_SCAN):不查看撤销日志(Undo Log),InnoDB存储引擎会将未提交的事务视为已提交。
- (SRV_FORCE_NO_LOG_REDO):不执行前滚的操作。
需要注意的是,当设置参数innodb_force_recovery大于0后,可以对表进行select、create、drop操作,但insert、update或者delete这类操作是不允许的。