博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

MySQL InnoDB存储引擎详解

Posted on 2015-09-11 09:25  DBA日志小记  阅读(607)  评论(0编辑  收藏  举报
2.2   InnoDB体系架构
通过第1章我们了解了MySQL的体系结构,现在可能你想更深入地了解InnoDB的架构模型。图2-1简单显示了InnoDB的存储引擎的体系架构。InnoDB有多个内存块,你可以认为这些内存块组成了一个大的内存池,负责如下工作:
q 维护所有进程/线程需要访问的多个内部数据结构。
q 缓存磁盘上的数据,方便快速地读取,并且在对磁盘文件的数据进行修改之前在这里缓存。
q 重做日志(redo log)缓冲。
MySQL InnoDB存储引擎详解 - 北晴空 - DBA日志小记
 
图2-1   InnoDB体系结构
后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。此外,将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常情况下InnoDB能恢复到正常运行状态。
2.2.1   后台线程
由于Oracle是多进程的架构(Windows下除外),因此可以通过一些很简单的命令来得知Oracle当前运行的后台进程,如ipcs命令。一般来说,Oracle的核心后台进程有CKPT、DBWn、LGWR、ARCn、PMON、SMON等。
很多DBA问我,InnoDB存储引擎是否也是这样的架构,只不过是多线程版本的实现后,我决定去看InnoDB的源代码,发现InnoDB并不是这样对数据库进程进行操作的。InnoDB存储引擎是在一个被称做master thread的线程上几乎实现了所有的功能。
默认情况下,InnoDB存储引擎的后台线程有7个—4个IO thread,1个master thread,1个锁(lock)监控线程,1个错误监控线程。IO thread的数量由配置文件中的innodb_file_ io_threads参数控制,默认为4,如下所示。
mysql> show engine innodb status\G;
*************************** 1. row ***************************
  Type: InnoDB
  Name: 
Status: 
=====================================
100719 21:34:03 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 50 seconds
……
--------
FILE I/O
--------
I/O thread 0 state: waiting for i/o request (insert buffer thread)
I/O thread 1 state: waiting for i/o request (log thread)
I/O thread 2 state: waiting for i/o request (read thread)
I/O thread 3 state: waiting for i/o request (write thread)
Pending normal aio reads: 0, aio writes: 0,
 ibuf aio reads: 0, log i/o's: 0, sync i/o's: 0
Pending flushes (fsync) log: 0; buffer pool: 0
45 OS file reads, 562 OS file writes, 412 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s
……
----------------------------
END OF INNODB MONITOR OUTPUT
============================
1 row in set (0.00 sec)
可以看到,4个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参数,如下所示。
mysql> show variables like 'innodb_version'\G;
*************************** 1. row ***************************
Variable_name: innodb_version
        Value: 1.0.6
1 row in set (0.00 sec)
mysql> show variables like 'innodb_%io_threads'\G;
*************************** 1. row ***************************
Variable_name: innodb_read_io_threads
        Value: 4
*************************** 2. row ***************************
Variable_name: innodb_write_io_threads
        Value: 4
2 rows in set (0.00 sec)
mysql> show engine innodb status\G;
*************************** 1. row ***************************
  Type: InnoDB
  Name: 
Status: 
=====================================
100719 21:55:26 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 36 seconds
......
--------
FILE I/O
--------
I/O thread 0 state: waiting for i/o request (insert buffer thread)
I/O thread 1 state: waiting for i/o request (log thread)
I/O thread 2 state: waiting for i/o request (read thread)
I/O thread 3 state: waiting for i/o request (read thread)
I/O thread 4 state: waiting for i/o request (read thread)
I/O thread 5 state: waiting for i/o request (read thread)
I/O thread 6 state: waiting for i/o request (write thread)
I/O thread 7 state: waiting for i/o request (write thread)
I/O thread 8 state: waiting for i/o request (write thread)
I/O thread 9 state: waiting for i/o request (write thread)
Pending normal aio reads: 0, aio writes: 0,
 ibuf aio reads: 0, log i/o's: 0, sync i/o's: 0
Pending flushes (fsync) log: 0; buffer pool: 0
3229856 OS file reads, 7830947 OS file writes, 1601902 OS fsyncs
reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s
......
----------------------------
END OF INNODB MONITOR OUTPUT
============================
1 row in set (0.01 sec)
在Windows下,我们还可以通过Visual Studio来调试MySQL,并设置断点来观察所有的线程信息,如图2-2所示。
图2-2   Windows下InnoDB存储引擎的线程
2.2.2   内存
InnoDB存储引擎内存由以下几个部分组成:缓冲池(buffer pool)、重做日志缓冲池(redo log buffer)以及额外的内存池(additional memory pool),分别由配置文件中的参数innodb_buffer_pool_size和innodb_log_buffer_size的大小决定。以下显示了一台MySQL数据库服务器,它将InnoDB存储引擎的缓冲池、重做日志缓冲池以及额外的内存池分别设置为2.5G、8M和8M(分别以字节显示)。
mysql> show variables like 'innodb_buffer_pool_size'\G;
*************************** 1. row ***************************
Variable_name: innodb_buffer_pool_size
        Value: 2621440000
1 row in set (0.00 sec)
mysql> show variables like 'innodb_log_buffer_size'\G;
*************************** 1. row ***************************
Variable_name: innodb_log_buffer_size
        Value: 8388608
1 row in set (0.00 sec)
mysql> show variables like 'innodb_additional_mem_pool_size'\G;
*************************** 1. row ***************************
Variable_name: innodb_additional_mem_pool_size
        Value: 8388608
1 row in set (0.00 sec)
缓冲池是占最大块内存的部分,用来存放各种数据的缓存。因为InnoDB的存储引擎的工作方式总是将数据库文件按页(每页16K)读取到缓冲池,然后按最近最少使用(LRU)的算法来保留在缓冲池中的缓存数据。如果数据库文件需要修改,总是首先修改在缓存池中的页(发生修改后,该页即为脏页),然后再按照一定的频率将缓冲池的脏页刷新(flush)到文件。可以通过命令SHOW ENGINE INNODB STATUS来查看innodb_buffer_ pool的具体使用情况,如下所示。
mysql> show engine innodb status\G;
*************************** 1. row ***************************
Status: 
=====================================
090921 10:55:03 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 24 seconds
......
----------------------
BUFFER POOL AND MEMORY
----------------------
......
Buffer pool size   65536
Free buffers       51225
Database pages     12986
Modified db pages  8
......
在BUFFER POOL AND MEMORY里可以看到InnoDB存储引擎缓冲池的使用情况,buffer pool size表明了一共有多少个缓冲帧(buffer frame),每个buffer frame为16K,所以这里一共分配了65536*16/1024=1G内存的缓冲池。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)等。不能简单地认为,缓冲池只是缓存索引页和数据页,它们只是占缓冲池很大的一部分而已。图2-3很好地显示了InnoDB存储引擎中内存的结构情况。
参数innodb_buffer_pool_size指定了缓冲池的大小,在32位Windows系统下,参数innodb_buffer_pool_awe_mem_mb还可以启用地址窗口扩展(AWE)功能,突破32位下对于内存使用的限制。但是,在使用这个参数的时候需要注意,一旦启用AWE功能,InnoDB存储引擎将自动禁用自适应哈希索引(adaptive hash index)的功能。
图2-3   InnoDB存储引擎内存结构
日志缓冲将重做日志信息先放入这个缓冲区,然后按一定频率将其刷新到重做日志文件。该值一般不需要设置为很大,因为一般情况下每一秒钟就会将重做日志缓冲刷新到日志文件,因此我们只需要保证每秒产生的事务量在这个缓冲大小之内即可。
额外的内存池通常被DBA忽略,认为该值并不是十分重要,但恰恰相反的是,该值其实同样十分重要。在InnoDB存储引擎中,对内存的管理是通过一种称为内存堆(heap)的方式进行的。在对一些数据结构本身分配内存时,需要从额外的内存池中申请,当该区域的内存不够时,会从缓冲池中申请。InnoDB实例会申请缓冲池(innodb_buffer_pool)的空间,但是每个缓冲池中的帧缓冲(frame buffer)还有对应的缓冲控制对象(buffer control block),而且这些对象记录了诸如LRU、锁、等待等方面的信息,而这个对象的内存需要从额外内存池中申请。因此,当你申请了很大的InnoDB缓冲池时,这个值也应该相应增加。

2.3   master thread
通过对前一小节的学习我们已经知道,InnoDB存储引擎的主要工作都是在一个单独的后台线程master thread中完成的。这一节我们将具体解释该线程的具体实现以及该线程可能存在的问题。
2.3.1   master thread源码分析
master thread的线程优先级别最高。其内部由几个循环(loop)组成:主循环(loop)、后台循环(background loop)、刷新循环(flush loop)、暂停循环(suspend loop)。master thread会根据数据库运行的状态在loop、background loop、 flush loop和suspend loop中进行切换。
loop称为主循环,因为大多数的操作都在这个循环中,其中有两大部分操作:每秒钟的操作和每10秒的操作。伪代码如下:
void master_thread(){
loop:
for(int i = 0; i < 10; i++){
    do thing once per second
    sleep 1 second if necessary
}
do things once per ten seconds
goto loop;
}
可以看到,loop循环通过thread sleep来实现,这意味着所谓的每秒一次或每10秒一次的操作是不精确的。在负载很大的情况下可能会有延迟(delay),只能说大概在这个频率下。当然,InnoDB源代码中还采用了其他的方法来尽量保证这个频率。
每秒一次的操作包括:
q 日志缓冲刷新到磁盘,即使这个事务还没有提交(总是)。
q 合并插入缓冲(可能)。
q 至多刷新100个InnoDB的缓冲池中的脏页到磁盘(可能)。
q 如果当前没有用户活动,切换到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个脏页写入磁盘。
总结上述3个操作,伪代码可以进一步具体化,如下所示:
void master_thread(){
    goto loop;
loop:
for(int i = 0; i<10; i++){
    thread_sleep(1) // sleep 1 second
    do log buffer flush to disk
    if (last_one_second_ios < 5 )
        do merge at most 5 insert buffer
    if ( buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct )
        do buffer pool flush 100 dirty page
    if ( no user activity )
        goto backgroud loop
}
do things once per ten seconds
background loop:
    do something
    goto loop:
}
接着来看每10秒的操作,包括如下内容:
q 刷新100个脏页到磁盘(可能)。
q 合并至多5个插入缓冲(总是)。
q 将日志缓冲刷新到磁盘(总是)。
q 删除无用的Undo页(总是)。
q 刷新100个或者10个脏页到磁盘(总是)。
q 产生一个检查点(总是)。
在以上的过程中,InnoDB存储引擎会先判断过去10秒之内磁盘的IO操作是否小于200次。如果是,InnoDB存储引擎认为当前有足够的磁盘IO操作能力,因此将100个脏页刷新到磁盘。接着,InnoDB存储引擎会合并插入缓冲。不同于每1秒操作时可能发生的合并插入缓冲操作,这次的合并插入缓冲操作总会在这个阶段进行。之后,InnoDB存储引擎会再执行一次将日志缓冲刷新到磁盘的操作,这与每秒发生的操作是一样的。
接着InnoDB存储引擎会执行一步full purge操作,即删除无用的Undo页。对表执行update、delete这类操作时,原先的行被标记为删除,但是因为一致性读(consistent read)的关系,需要保留这些行版本的信息。但是在full purge过程中,InnoDB存储引擎会判断当前事务系统中已被删除的行是否可以删除,比如有时候可能还有查询操作需要读取之前版本的Undo信息,如果可以,InnoDB会立即将其删除。从源代码中可以发现,InnoDB存储引擎在操作full purge时,每次最多删除20个Undo页。
然后,InnoDB存储引擎会判断缓冲池中脏页的比例(buf_get_modified_ratio_pct),如果有超过70%的脏页,则刷新100个脏页到磁盘;如果脏页的比例小于70%,则只需刷新10%的脏页到磁盘。
最后,InnoDB存储引擎会产生一个检查点(checkpoint),InnoDB存储引擎的检查点也称为模糊检查点(fuzzy checkpoint)。InnoDB存储引擎在checkpoint时并不会把所有缓冲池中的脏页都写入磁盘,因为这样可能会对性能产生影响,而只是将最老日志序列号(oldest LSN)的页写入磁盘。
现在,我们可以完整地把主循环(main loop)的伪代码写出来了,内容如下:
void master_thread(){
    goto loop;
loop:
for(int i = 0; i<10; i++){
    thread_sleep(1) // sleep 1 second
    do log buffer flush to disk
    if (last_one_second_ios < 5 )
        do merge at most 5 insert buffer
    if ( buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct )
        do buffer pool flush 100 dirty page
    if ( no user activity )
        goto backgroud loop
}
if ( last_ten_second_ios < 200 )
    do buffer pool flush 100 dirty page
do merge at most 5 insert buffer
do log buffer flush to disk
do full purge
if ( buf_get_modified_ratio_pct > 70% )
    do buffer pool flush 100 dirty page
else
    buffer pool flush 10 dirty page
do fuzzy checkpoint
goto loop
background loop:
    do something
goto loop:
}
接着来看background loop,若当前没有用户活动(数据库空闲时)或者数据库关闭时,就会切换到这个循环。这个循环会执行以下操作:
q 删除无用的Undo页(总是)。
q 合并20个插入缓冲(总是)。
q 跳回到主循环(总是)。
q 不断刷新100个页,直到符合条件(可能,跳转到flush loop中完成)。
如果flush loop中也没有什么事情可以做了,InnoDB存储引擎会切换到suspend_loop,将master thread挂起,等待事件的发生。若启用了InnoDB存储引擎,却没有使用任何InnoDB存储引擎的表,那么master thread总是处于挂起状态。
最后,master thread完整的伪代码如下:
void master_thread(){
    goto loop;
loop:
for(int i = 0; i<10; i++){
    thread_sleep(1) // sleep 1 second
    do log buffer flush to disk
    if ( last_one_second_ios < 5 )
        do merge at most 5 insert buffer
    if ( buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct )
        do buffer pool flush 100 dirty page
    if ( no user activity )
        goto backgroud loop
}
if ( last_ten_second_ios < 200 )
    do buffer pool flush 100 dirty page
do merge at most 5 insert buffer
do log buffer flush to disk
do full purge
if ( buf_get_modified_ratio_pct > 70% )
    do buffer pool flush 100 dirty page
else
    buffer pool flush 10 dirty page
do fuzzy checkpoint
goto loop
background loop:
do full purge
do merge 20 insert buffer
if not idle:
goto loop:
else:
    goto flush loop
flush loop:
do buffer pool flush 100 dirty page
if ( buf_get_modified_ratio_pct> innodb_max_dirty_pages_pct )
    goto flush loop
goto suspend loop
suspend loop:
suspend_thread()
waiting event
goto loop;
}
从InnoDB Plugin开始,用命令SHOW ENGINE INNODB STATUS可以查看当前master thread的状态信息,如下所示:
mysql> show engine innodb status\G;
*************************** 1. row ***************************
  Type: InnoDB
  Name: 
Status: 
=====================================
090921 14:24:56 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 6 seconds
----------
BACKGROUND THREAD
----------
srv_master_thread loops: 45 1_second, 45 sleeps, 4 10_second, 6 background, 6 flush
srv_master_thread log flush and writes: 45  log writes only: 69
......
这里可以看到主循环执行了45次,每秒sleep的操作执行了45次(说明负载不是很大),10秒一次的活动执行了4次,符合1∶10。background loop执行了6次,flush loop执行了6次。因为当前这台服务器的压力很小,所以能在理论值上运行。但是,如果是在一台压力很大的MySQL服务器上,我们看到的可能会是下面的情景:
mysql> show engine innodb status\G;
*************************** 1. row ***************************
  Type: InnoDB
  Name: 
Status: 
=====================================
091009 10:14:34 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 42 seconds
----------
BACKGROUND THREAD
----------
srv_master_thread loops: 2188 1_second, 1537 sleeps, 218 10_second, 2 background, 2 flush
srv_master_thread log flush and writes: 1777  log writes only: 5816
......
可以看到当前主循环运行了2188次,但是循环中的每一秒钟SLEEP的操作只运行了
1537次。这是因为InnoDB对其内部进行了一些优化,当压力大时并不总是等待1秒。所以说,我们并不能认为1_second和sleeps的值总是相等的。在某些情况下,可以通过两者之间差值的比较来反映当前数据库的负载压力。

2.3.2   master thread的潜在问题
在了解了master thread的具体实现过程后,我们会发现InnoDB存储引擎对于IO其实是有限制的,在缓冲池向磁盘刷新时其实都做了一定的硬性规定(hard coding)。在磁盘技术飞速发展的今天,当固态磁盘出现时,这种规定在很大程度上限制了InnoDB存储引擎对磁盘IO的性能,尤其是写入性能。
从前面的伪代码来看,无论何时,InnoDB存储引擎最多都只会刷新100个脏页到磁盘,合并20个插入缓冲。如果是在密集写的应用程序中,每秒中可能会产生大于100个的脏页,或是产生大于20个插入缓冲,此时master thread似乎会“忙不过来”,或者说它总是做得很慢。即使磁盘能在1秒内处理多于100个页的写入和20个插入缓冲的合并,由于hard coding,master thread也只会选择刷新100个脏页和合并20个插入缓冲。同时,当发生宕机需要恢复时,由于很多数据还没有刷新回磁盘,所以可能会导致恢复需要很快的时间,尤其是对于insert buffer。
这个问题最初是由Google的工程师Mark Callaghan提出的,之后InnoDB对其进行了修正并发布了补丁。InnoDB存储引擎的开发团队参考了Google的patch,提供了类似的方法来修正该问题。因此InnoDB Plugin开始提供了一个参数,用来表示磁盘IO的吞吐量,参数为innodb_io_capacity,默认值为200。对于刷新到磁盘的数量,会按照innodb_io_capacity的百分比来刷新相对数量的页。规则如下:
q 在合并插入缓冲时,合并插入缓冲的数量为innodb_io_capacity数值的5%。
q 在从缓冲区刷新脏页时,刷新脏页的数量为innodb_io_capacity。
如果你使用了SSD类的磁盘,或者将几块磁盘做了RAID,当你的存储拥有更高的IO速度时,完全可以将innodb_io_capacity的值调得再高点,直到符合你的磁盘IO的吞吐量为止。
另一个问题是参数innodb_max_dirty_pages_pct的默认值,在MySQL 5.1版本之前(包括5.1),该值的默认值为90,意味着脏页占缓冲池的90%。但是该值“太大”了,因为你会发现,InnoDB存储引擎在每1秒刷新缓冲池和flush loop时,会判断这个值,如果大于innodb_max_dirty_pages_pct,才刷新100个脏页。因此,如果你有很大的内存或你的数据库服务器的压力很大,这时刷新脏页的速度反而可能会降低。同样,在数据库的恢复阶段可能需要更多的时间。
在很多论坛上都有对这个问题的讨论,有人甚至将这个值调到了20或10,然后测试发现性能会有所提高,但是将innodb_max_dirty_pages_pct调到20或10会增加磁盘的压力,对系统的负担还是会有所增加。Google对这个问题进行了测试,可以证明20并不是一个最优值。而从InnoDB Plugin开始,innodb_max_dirty_pages_pct默认值变为了75,和Google测试的80比较接近。这样既可以加快刷新脏页的频率,也能保证磁盘IO的负载。
InnoDB Plugin带来的另一个参数是innodb_adaptive_flushing(自适应地刷新),该值影响每1秒刷新脏页的数量。原来的刷新规则是:如果脏页在缓冲池所占的比例小于innodb_max_dirty_pages_pct时,不刷新脏页。大于innodb_max_dirty_pages_pct时,刷新100个脏页,而innodb_adaptive_flushing参数的引入,InnoDB存储引擎会通过一个名为buf_flush_get_desired_flush_rate的函数来判断需要刷新脏页最合适的数量。粗略地翻阅源代码后你会发现,buf_flush_get_desired_flush_rate是通过判断产生重做日志的速度来判断最合适的刷新脏页的数量。因此,当脏页的比例小于innodb_max_dirty_pages_pct时,也会刷新一定量的脏页。
通过上述的讨论和解释,从InnoDB Plugin开始,master thread的伪代码最终变成了:
void master_thread(){
    goto loop;
loop:
for(int i = 0; i<10; i++){
    thread_sleep(1) // sleep 1 second
    do log buffer flush to disk
    if ( last_one_second_ios < 5% innodb_io_capacity )
        do merge 5% innodb_io_capacity insert buffer
    if ( buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct )
        do buffer pool flush 100% innodb_io_capacity dirty page
    else if enable adaptive flush
        do buffer pool flush desired amount dirty page
    if ( no user activity )
        goto backgroud loop
}
if ( last_ten_second_ios < innodb_io_capacity)
    do buffer pool flush 100% innodb_io_capacity dirty page
do merge at most 5% innodb_io_capacity insert buffer
do log buffer flush to disk
do full purge
if ( buf_get_modified_ratio_pct > 70% )
    do buffer pool flush 100% innodb_io_capacity dirty page
else
    do buffer pool flush 10% innodb_io_capacity dirty page
do fuzzy checkpoint
goto loop
background loop:
do full purge
do merge 100% innodb_io_capacity insert buffer
if not idle:
goto loop:
else:
    goto flush loop
flush loop:
do buffer pool flush 100% innodb_io_capacity dirty page
if ( buf_get_modified_ratio_pct> innodb_max_dirty_pages_pct )
    go to flush loop
    goto suspend loop
suspend loop:
suspend_thread()
waiting event
goto loop;
}
很多测试都显示,InnoDB Plugin较之以前的InnoDB存储引擎在性能方面有了极大的提高,其实这与以上master thread的改动是密不可分的,因为InnoDB存储引擎的核心操作大部分都是在master thread中。

2.4   关键特性
InnoDB存储引擎的关键特性包括插入缓冲、两次写(double write)、自适应哈希索引(adaptive hash index)。这些特性为InnoDB存储引擎带来了更好的性能和更高的可靠性。
2.4.1   插入缓冲
插入缓冲是InnoDB存储引擎关键特性中最令人激动的。不过,这个名字可能会让人认为插入缓冲是缓冲池中的一个部分。其实不然,InnoDB缓冲池中有Insert Buffer信息固然不错,但是Insert Buffer和数据页一样,也是物理页的一个组成部分。
我们知道,主键是行唯一的标识符,在应用程序中行记录的插入顺序是按照主键递增的顺序进行插入的。因此,插入聚集索引一般是顺序的,不需要磁盘的随机读取。比如说我们按下列SQL定义的表。
mysql> create table t ( id int auto_increment, name varchar(30),primary key (id));
Query OK, 0 rows affected (0.14 sec)
id列是自增长的,这意味着当执行插入操作时,id列会自动增长,页中的行记录按id执行顺序存放。一般情况下,不需要随机读取另一页执行记录的存放。因此,在这样的情况下,插入操作一般很快就能完成。但是,不可能每张表上只有一个聚集索引,在更多的情况下,一张表上有多个非聚集的辅助索引(secondary index)。比如,我们还需要按照name这个字段进行查找,并且name这个字段不是唯一的。即,表是按如下的SQL语句定义的:
mysql> create table t ( id int auto_increment, name varchar(30),primary key (id),key(name));
Query OK, 0 rows affected (0.21 sec)
这样的情况下产生了一个非聚集的并且不是唯一的索引。在进行插入操作时,数据页的存放还是按主键id的执行顺序存放,但是对于非聚集索引,叶子节点的插入不再是顺序的了。这时就需要离散地访问非聚集索引页,插入性能在这里变低了。然而这并不是这个name字段上索引的错误,因为B+树的特性决定了非聚集索引插入的离散性。
InnoDB存储引擎开创性地设计了插入缓冲,对于非聚集索引的插入或更新操作,不是每一次直接插入索引页中。而是先判断插入的非聚集索引页是否在缓冲池中。如果在,则直接插入;如果不在,则先放入一个插入缓冲区中,好似欺骗数据库这个非聚集的索引已经插到叶子节点了,然后再以一定的频率执行插入缓冲和非聚集索引页子节点的合并操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对非聚集索引执行插入和修改操作的性能。
插入缓冲的使用需要满足以下两个条件:
q 索引是辅助索引。
q 索引不是唯一的。
当满足以上两个条件时,InnoDB存储引擎会使用插入缓冲,这样就能提高性能了。不过考虑一种情况,应用程序执行大量的插入和更新操作,这些操作都涉及了不唯一的非聚集索引,如果在这个过程中数据库发生了宕机,这时候会有大量的插入缓冲并没有合并到实际的非聚集索引中。如果是这样,恢复可能需要很长的时间,极端情况下甚至需要几个小时来执行合并恢复操作。
辅助索引不能是唯一的,因为在把它插入到插入缓冲时,我们并不去查找索引页的情况。如果去查找肯定又会出现离散读的情况,插入缓冲就失去了意义。
可以通过命令SHOW ENGINE INNODB STATUS来查看插入缓冲的信息:
mysql> show engine innodb status\G;
*************************** 1. row ***************************
  Type: InnoDB
  Name: 
Status: 
=====================================
100727 22:21:48 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 44 seconds
......
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 7545, free list len 3790, seg size 11336,
8075308 inserts, 7540969 merged recs, 2246304 merges
......
----------------------------
END OF INNODB MONITOR OUTPUT
============================
1 row in set (0.00 sec)
seg size显示了当前插入缓冲的大小为11 336*16KB,大约为177MB,free list len代表了空闲列表的长度,size代表了已经合并记录页的数量。下面一行可能是我们真正关心的,因为它显示了提高性能了。Inserts代表插入的记录数,merged recs代表合并的页的数量,merges代表合并的次数。merges∶merged recs大约为3∶1,代表插入缓冲将对于非聚集索引页的IO请求大约降低了3倍。
目前插入缓冲存在一个问题是,在写密集的情况下,插入缓冲会占用过多的缓冲池内存,默认情况下最大可以占用1/2的缓冲池内存。以下是InnoDB存储引擎源代码中对insert buffer的初始化操作:
/** Buffer pool size per the maximum insert buffer size */
#define IBUF_POOL_SIZE_PER_MAX_SIZE 2
ibuf->max_size = buf_pool_get_curr_size() / UNIV_PAGE_SIZE
    / IBUF_POOL_SIZE_PER_MAX_SIZE;
这对其他的操作可能会带来一定的影响。Percona已发布一些patch来修正插入缓冲占用太多缓冲池内存的问题,具体的可以到http://www.percona.com/percona-lab.html查找。简单来说,修改IBUF_POOL_SIZE_PER_MAX_SIZE就可以对插入缓冲的大小进行控制,例如,将IBUF_POOL_SIZE_PER_MAX_SIZE改为3,则最大只能使用1/3的缓冲池内存。
2.4.2   两次写
如果说插入缓冲带给InnoDB存储引擎的是性能,那么两次写带给InnoDB存储引擎的是数据的可靠性。当数据库宕机时,可能发生数据库正在写一个页面,而这个页只写了一部分(比如16K的页,只写前4K的页)的情况,我们称之为部分写失效(partial page write)。在InnoDB存储引擎未使用double write技术前,曾出现过因为部分写失效而导致数据丢失的情况。
有人也许会想,如果发生写失效,可以通过重做日志进行恢复。这是一个办法。但是必须清楚的是,重做日志中记录的是对页的物理操作,如偏移量800,写'aaaa'记录。如果这个页本身已经损坏,再对其进行重做是没有意义的。这就是说,在应用(apply)重做日志前,我们需要一个页的副本,当写入失效发生时,先通过页的副本来还原该页,再进行重做,这就是doublewrite。InnoDB存储引擎doublewrite的体系架构如图2-4所示:
图2-4   InnoDB存储引擎doublewrite架构
doublewrite由两部分组成:一部分是内存中的doublewrite buffer,大小为2MB;另一部分是物理磁盘上共享表空间中连续的128个页,即两个区(extent),大小同样为2MB。当缓冲池的脏页刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先拷贝到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次写入1MB到共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题。在这个过程中,因为doublewrite页是连续的,因此这个过程是顺序写的,开销并不是很大。在完成doublewrite页的写入后,再将doublewrite buffer中的页写入各个表空间文件中,此时的写入则是离散的。可以通过以下命令观察到doublewrite运行的情况:
mysql> show global status like 'innodb_dblwr%'\G;
*************************** 1. row ***************************
Variable_name: Innodb_dblwr_pages_written
        Value: 6325194
*************************** 2. row ***************************
Variable_name: Innodb_dblwr_writes
        Value: 100399
2 rows in set (0.00 sec)
可以看到,doublewrite一共写了6 325 194个页,但实际的写入次数为100 399,基本上符合64∶1。如果发现你的系统在高峰时Innodb_dblwr_pages_written: Innodb_dblwr_writes远小于64∶1,那么说明你的系统写入压力并不是很高。
如果操作系统在将页写入磁盘的过程中崩溃了,在恢复过程中,InnoDB存储引擎可以从共享表空间中的doublewrite中找到改页的一个副本,将其拷贝到表空间文件,再应用重做日志。下面显示了由doublewrite进行恢复的一种情况:
090924 11:36:32  mysqld restarted
090924 11:36:33  InnoDB: Database was not shut down normally!
InnoDB: Starting crash recovery.
InnoDB: Reading tablespace information from the .ibd files...
InnoDB: Error: space id in fsp header 0, but in the page header 4294967295
InnoDB: Error: tablespace id 4294967295 in file ./test/t.ibd is not sensible
InnoDB: Error: tablespace id 0 in file ./test/t2.ibd is not sensible
090924 11:36:33  InnoDB: Operating system error number 40 in a file operation.
InnoDB: Error number 40 means 'Too many levels of symbolic links'.
InnoDB: Some operating system error numbers are described at
InnoDB: http://dev.mysql.com/doc/refman/5.0/en/operating-system-error-codes.html
InnoDB: File name ./now/member
InnoDB: File operation call: 'stat'.
InnoDB: Error: os_file_readdir_next_file() returned -1 in
InnoDB: directory ./now
InnoDB: Crash recovery may have failed for some .ibd files!
InnoDB: Restoring possible half-written data pages from the doublewrite
InnoDB: buffer...
参数skip_innodb_doublewrite可以禁止使用两次写功能,这时可能会发生前面提及的写失效问题。不过,如果你有多台从服务器(slave server),需要提供较快的性能(如slave上做的是RAID0),也许启用这个参数是一个办法。不过,在需要提供数据高可靠性的主服务器(master server)上,任何时候我们都应确保开启两次写功能。
注意:有些文件系统本身就提供了部分写失效的防范机制,如ZFS文件系统。在这种情况下,我们就不要启用doublewrite了。
2.4.3   自适应哈希索引
哈希(hash)是一种非常快的查找方法,一般情况下查找的时间复杂度为O(1)。常用于连接(join)操作,如SQL Server和Oracle中的哈希连接(hash join)。但是SQL Server和Oracle等常见的数据库并不支持哈希索引(hash index)。MySQL的Heap存储引擎默认的索引类型为哈希,而InnoDB存储引擎提出了另一种实现方法,自适应哈希索引(adaptive hash index)。
InnoDB存储引擎会监控对表上索引的查找,如果观察到建立哈希索引可以带来速度的提升,则建立哈希索引,所以称之为自适应(adaptive)的。自适应哈希索引通过缓冲池的B+树构造而来,因此建立的速度很快。而且不需要将整个表都建哈希索引,InnoDB存储引擎会自动根据访问的频率和模式来为某些页建立哈希索引。
根据InnoDB的官方文档显示,启用自适应哈希索引后,读取和写入速度可以提高2倍;对于辅助索引的连接操作,性能可以提高5倍。在我看来,自适应哈希索引是非常好的优化模式,其设计思想是数据库自优化(self-tuning),即无需DBA对数据库进行调整。
通过命令SHOW ENGINE INNODB STATUS可以看到当前自适应哈希索引的使用状况,如下所示:
mysql> show engine innodb status\G;
*************************** 1. row ***************************
Status: 
=====================================
090922 11:52:51 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 15 seconds
......
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 2249, free list len 3346, seg size 5596,
374650 inserts, 51897 merged recs, 14300 merges
Hash table size 4980499, node heap has 1246 buffer(s)
1640.60 hash searches/s, 3709.46 non-hash searches/s
......
现在可以看到自适应哈希索引的使用信息了,包括自适应哈希索引的大小、使用情况、每秒使用自适应哈希索引搜索的情况。值得注意的是,哈希索引只能用来搜索等值的查询,如select * from table where index_col = 'xxx',而对于其他查找类型,如范围查找,是不能使用的。因此,这里出现了non-hash searches/s的情况。用hash searches : non-hash searches命令可以大概了解使用哈希索引后的效率。
由于自适应哈希索引是由InnoDB存储引擎控制的,所以这里的信息只供我们参考。不过我们可以通过参数innodb_adaptive_hash_index来禁用或启动此特性,默认为开启。

2.5   启动、关闭与恢复
InnoDB存储引擎是MySQL的存储引擎之一,因此InnoDB存储引擎的启动和关闭更准确地是指在MySQL实例的启动过程中对InnoDB表存储引擎的处理过程。
在关闭时,参数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存储引擎的表执行恢复操作。
参数innodb_force_recovery影响了整个InnoDB存储引擎的恢复状况。该值默认为0,表示当需要恢复时执行所有的恢复操作。当不能进行有效恢复时,如数据页发生了corruption,MySQL数据库可能会宕机,并把错误写入错误日志中。
但是,在某些情况下,我们可能并不需要执行完整的恢复操作,我们自己知道如何进行恢复。比如正在对一个表执行alter table操作,这时意外发生了,数据库重启时会对InnoDB表执行回滚操作。对于一个大表,这需要很长时间,甚至可能是几个小时。这时我们可以自行进行恢复,例如可以把表删除,从备份中重新将数据导入表中,这些操作的速度可能要远远快于回滚操作。
innodb_force_recovery还可以设置为6个非零值:1~6。大的数字包含了前面所有小数字的影响,具体情况如下。
q 1(SRV_FORCE_IGNORE_CORRUPT):忽略检查到的corrupt页。
q 2(SRV_FORCE_NO_BACKGROUND):阻止主线程的运行,如主线程需要执行full purge操作,会导致crash。
q 3(SRV_FORCE_NO_TRX_UNDO):不执行事务回滚操作。
q 4(SRV_FORCE_NO_IBUF_MERGE):不执行插入缓冲的合并操作。
q 5(SRV_FORCE_NO_UNDO_LOG_SCAN):不查看撤销日志(Undo Log),InnoDB存储引擎会将未提交的事务视为已提交。
q 6(SRV_FORCE_NO_LOG_REDO):不执行前滚的操作。
需要注意的是,当设置参数innodb_force_recovery大于0后,可以对表进行select、create、drop操作,但insert、update或者delete这类操作是不允许的。
我们来做个实验,模拟故障的发生。在第一会话中,对一张接近1 000W行的InnoDB存储引擎表执行更新操作,但是完成后不要马上提交:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update Profile set password='';
Query OK, 9587770 rows affected (7 min 55.73 sec)
Rows matched: 9999248  Changed: 9587770  Warnings: 0
start transaction语句开启了事务,同时防止了自动提交的发生,update操作则会产生大量的回滚日志。这时,我们人为地kill掉MySQL数据库服务器。
  [root@nineyou0-43 ~]# ps -ef | grep mysqld
  root     28007     1  0 13:40 pts/1    00:00:00 /bin/sh ./bin/mysqld_safe         --datadir=/usr/local/mysql/data --pid-file=/usr/local/mysql/data/nineyou0-43.pid
  mysql    28045 28007 42 13:40 pts/1    00:04:23 
  /usr/local/mysql/bin/mysqld --basedir=/usr/local/mysql 
  --datadir=/usr/local/mysql/data --user=mysql 
  --pid-file=/usr/local/mysql/data/nineyou0-43.pid 
  --skip-external-locking --port=3306 --socket=/tmp/mysql.sock
  root     28110 26963  0 13:50 pts/11   00:00:00 grep mysqld
  [root@nineyou0-43 ~]# kill -9 28007
  [root@nineyou0-43 ~]# kill -9 28045
通过kill命令,我们人为地模拟了一次数据库宕机故障,当MySQL数据库下次启动时会对update的这个事务执行回滚操作,而这些信息都会记录在错误日志文件中,默认后缀名为err。如果查看错误日志文件,可得到如下结果:
090922 13:40:20  InnoDB: Started; log sequence number 6 2530474615
InnoDB: Starting in background the rollback of uncommitted transactions
090922 13:40:20  InnoDB: Rolling back trx with id 0 5281035, 8867280 rows to undo
InnoDB: Progress in percents: 1090922 13:40:20 
090922 13:40:20 [Note] /usr/local/mysql/bin/mysqld: ready for connections.
Version: '5.0.45-log'  socket: '/tmp/mysql.sock'  port: 3306  MySQL Community Server (GPL)
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
InnoDB: Rolling back of trx id 0 5281035 completed
090922 13:49:21  InnoDB: Rollback of non-prepared transactions completed
可以看到,如果采用默认的策略,即把innodb_force_recovery设为0,InnoDB会在每次启动后对发生问题的表执行恢复操作,通过错误日志文件,可知这次回滚操作需要回滚
8 867 280行记录,总共耗时约9分多钟。
我们做再做一次同样的测试,只不过在启动MySQL数据库前将参数innodb_force_ recovery设为3,然后观察InnoDB存储引擎是否还会执行回滚操作,查看错误日志文件,可看到:
090922 14:26:23  InnoDB: Started; log sequence number 7 2253251193
InnoDB: !!! innodb_force_recovery is set to 3 !!!
090922 14:26:23 [Note] /usr/local/mysql/bin/mysqld: ready for connections.
Version: '5.0.45-log'  socket: '/tmp/mysql.sock'  port: 3306  MySQL Community Server (GPL)
这里出现了“!!!”,InnoDB警告你已经将innodb_force_recovery设置为3,不会进行undo的回滚操作了。因此数据库很快启动完成,但是你应该很小心当前数据库的状态,并仔细确认是否不需要回滚操作。

2.7   小结
本章首先介绍了InnoDB存储引擎的历史、InnoDB存储引擎的体系结构(包括后台线程和内存结构),之后又详细讲解了InnoDB存储引擎的关键特性,这些特性使得InnoDB存储引擎变得更具“魅力”,最后叙述了启动和关闭MySQL时一些配置文件参数对InnoDB存储引擎的影响。
通过本章的铺垫,后文就能更深入和全面地讲解InnoDB引擎。第3章开始介绍MySQL的文件,包括MySQL本身的文件和与InnoDB存储引擎有关的文件,之后的章节将介绍基于InnoDB存储引擎的表,并揭示其内部的存储构造。

版权声明:QQ:597507041