innodb_fast_shutdown
innodb_fast_shutdown告诉 InnoDB 在它关闭的时候该做什么工作。
有三个值可以选择(默认值为1):
- 0:表示在 InnoDB 关闭的时候,需要 purge all,merge insert buffer,flush dirty pages。这是最慢的一种关闭方式,但是restart的时候也是最快的。后面将介绍purge all,merge insert buffer,flush dirty pages这三者的含义。
- 1:表示在 InnoDB 关闭的时候,它不需要 purge all,merge insert buffer,只需要flush dirty page。
- 2:表示在 InnoDB 关闭的时候,它不需要 purge all,merge insert buffer,也不进行flush dirty page,只将log buffer里面的日志flush到log files。因此等下进行恢复的时候它是最耗时的。
那么在mysql restart的时候它的恢复流程(也称作crash recovery)是怎么样的呢?
- 如果在上次关闭innodb的时候是在innodb_fast_shutdown=2或是mysql crash这种情况,那么它会利用redo log重做那些已经提交了的事务。
- 接下来的操作就是这么几个:
- Rollback uncompleted transitions取消那些没有提交的事务。
- Purge all清除无用的undo页。
- Merge insert buffer合并插入缓冲。
下面详解purge all、merge insert buffer、flush dirty page
Purge all
这个操作主要是删除那些无用的undo页。对于delete操作,innodb是通过先将要删除的那一行标记为删除,而不是马上清除这一行,因为innodb实现了MVCC,这些undo段用来实现MVCC机制。MVCC也就是常说的多版本控制,锁不阻塞读,读也不阻塞写,这样大大提高了并发性。那么在一致性读的时候,怎么才能找到和事务开始的那个版本呢?对于主键索引,每个行都有一个事务ID和一个undo ID,这个undo ID指向了这行的先前版本的位置。对于非主键索引,也就是常说的secondary index,是通过先找主键索引再找到undo段。而对于update操作,则是先标记删除,然后insert一个新的行,接下来如果有一致性读,那么查找old version的行的原理和delete操作是一样的,详情见[1]。现在接着说purge all操作,随着DML的操作越来越多,那么回滚段必然也会越来越多导致占用了许多磁盘空间,那么innodb就会定期删除一些无用的undo页,首先,innodb重启的时候必然undo页都会无效所以会进行purge all操作,另外,随着时间的推移必然一些事务已经完成,它们已不再需要某些undo页,那么这些undo在mysqld running的时候也会定期的进行清除,主要是在master thread中进行,虽然mysql5.5里面增加了一个参数innodb_purge_threads来进行purge工作,但是这个参数的默认值是0,手册上解释说这个功能在mysql5.5中还不完善,增加它的目的只是表明这是innodb的发展方向。
Merge insert buffer
Insert buffer是innodb的一个特性之一,在非聚簇、且不是唯一索引(即非主键索引、非唯一索引)的情况下,如果插入的索引行所属的页在buffer pool中就直接更新这个页,否则它会将这个索引行插入到insert buffer中,然后定期对这个insert buffer进行合并(合并的本质工作就是将insert buffer中的信息更新到真正的索引文件中去)。因为innodb的secondary index是非聚簇的,那么插入很有可能带来大量的随机I/O,而如果利用insert buffer对一些属于相同页的行进行合并,那么就会减少随机IO从而提高性能。但是这里需要注意的是,insert buffer和doublewrite buffer是类似的概念,他实际上属于system tablespace中的一部分[2],正由于它也是持久化存储,那么在服务器宕机或是重启之后这些信息不会丢失,所以也就有了在前面介绍innodb_fast_shutdown时所说:在innodb重启时,可能需要进行merge insert buffer。那么在什么情况下需要对insert buffer进行merge操作呢?
a> 在innodb restart的时候。
b> master thread会定期的进行merge操作。
c> 每次读取secondary index page时,如果所需页不在buffer pool,而这些页在insert buffer中的时候,这时需要先对insert buffer进行合并,然后才能被读取。为什么这样呢?因为所有插入的索引行所属的页如果不在buffer pool中,而又在insert buffer中,那么它一定代表了页的最新状态(不理解?因为每次插入索引行的时候,如果所需页不在buffer pool中就直接插入到insert buffer中,而一旦insert buffer merge后相关的行也就不在insert buffer更新secondary index page了)。这时或许你会问那么为什么不直接读取insert buffer中的页然后继续操作而一定要合并(更新到索引文件)呢?因为在innodb中是数据文件(也就是主键索引)和索引文件缓存的,在insert buffer中读取了需要的页后,那么必然就会在buffer pool中缓存了这个页,而如果这个页还留在insert buffer中却不更新到secondary index page去,那么,第一,这将不能保证索引文件得到更新;第二,insert buffer的空间会被占用。而如果这一步将insert buffer 合并后,不但减小了insert buffer的使用空间,而且将这merge操作完成了一部分,减小了以后merge的负担(不是有句话叫做今日事今日毕么),不过这也减慢了读的操作,因为读操作必须等待这个页的合并。
Flush dirty page
这是最好理解的一个概念了,刷新脏页到磁盘。Innodb是数据文件和索引文件缓存的(innodb中的数据文件本质上也是索引文件,只是习惯这么称呼而已),从磁盘读到buffer中的文件被修改后,那么就成了dirty page脏页。而如果这些修改页的操作被提交了之后这些页就必须被flush到磁盘上。
啰嗦了这么久基本上将mysql的insert buffer工作原理大致说清楚了,不过需要注意的是在mysql5.5中这个insert buffer已经改名了,叫做change buffer,不见包含了insert buffer,而且包括了update buffer,delete buffer。最后提一句,随着SSD、Fusion IO这类型存储出现,很多时候我们考虑随机IO带来的影响或许对它们就不适用了。
因为没有读源码,这些理解是通过读其他的资料而来的,所以还留下了几个问题:
1. 实现insert buffer的数据结构是什么?我想应该是树状结构,因为这会为合并那一步提升效率。理由:第一,如果是无序链表的最开始的插入效率可能会比较高,但是最终判断哪些行在相同页或是相邻页的时候需要排序,这里的代价会比较高。而有序的链表在性能上没有二叉树这种结构效率高。
2. Insert buffer占多大空间?如果很小那岂不是只能容纳几行?那么在系统压力的时候,有空间来应付插入压力么?而如果比较大的,那么怎么保证在合并时候的效率?
innodb_force_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,大的数字包含了前面所有小数字的影响,具体情况如下。
- 1(SRV_FORCE_IGNORE_CORRUPT):忽略检查到的corrupt页。
- 2(SRV_FORCE_NO_BACKGROUND):阻止主线程的运行,如主线程需要执行full purge操作,会导致crash。
- 3(SRV_FORCE_NO_TRX_UNDO):不执行事务回滚操作。
- 4(SRV_FORCE_NO_IBUF_MERGE):不执行插入缓冲的合并操作。
- 5(SRV_FORCE_NO_UNDO_LOG_SCAN):不查看撤销日志(Undo Log),InnoDB存储引擎会将未提交的事务视为已提交。
- 6(SRV_FORCE_NO_LOG_REDO):不执行前滚的操作。
需要注意的是,当设置参数innodb_force_recovery大于0后,可以对表进行select、create、drop操作,但insert、update或者delete这类操作是不允许的。
因为错误日志里面提示出现了坏页,导致数据库崩溃,所以这里把innodb_force_recovery 设置为1,忽略检查到的坏页。重启数据库之后,正常了,没有出现上面的错误信息。找到错误信息出现的表:index “PRIMARY” of table “maitem”.”email_status”
数据页面的主键索引(clustered key index)被损坏。这种情况和数据的二级索引(secondary indexes)被损坏相比要糟很多,因为后者可以通过使用OPTIMIZE TABLE命令来修复,但这和更难以恢复的表格目录(table dictionary)被破坏的情况来说要好一些。
下面是实验,模拟故障的发生。在第一会话中,对一张接近1000W行的InnoDB存储引擎表执行更新操作,但是完成后不要马上提交:
1 2 3 4 5 6 |
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数据库服务器。
1 2 3 4 5 6 |
$ 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 $ kill -9 28007 $ kill -9 28045 |
通过kill命令,我们人为地模拟了一次数据库宕机故障,当MySQL数据库下次启动时会对update的这个事务执行回滚操作,而这些信息都会记录在错误日志文件中,默认后缀名为err。如果查看错误日志文件,可得到如下结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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会在每次启动后对发生问题的表执行恢复操作,通过错误日志文件,可知这次回滚操作需要回滚8867280行记录,总共耗时约9分多钟。