MLOG_CHECKPOINT缺失下紧急数据恢复
生产环境MySQL数据库,架构是一主一从,可以双机切换,MySQL版本是5.7.24-enterprise-commercial-advanced-log。当时是存储MySQL的机房突然断电。主从机器重启后,MySQL都无法启动。两台机器都是同样错误,错误日志显示:
2020-10-27T00:58:41.032944Z 0 [Note] InnoDB: Log scan progressed past the checkpoint lsn 20171067203814
2020-10-27T00:58:41.136783Z 0 [Note] InnoDB: Doing recovery: scanned up to log sequence number 20171072446464
2020-10-27T00:58:41.222121Z 0 [Note] InnoDB: Doing recovery: scanned up to log sequence number 20171077689344
2020-10-27T00:58:41.312905Z 0 [Note] InnoDB: Doing recovery: scanned up to log sequence number 20171082932224
2020-10-27T00:58:41.401125Z 0 [Note] InnoDB: Doing recovery: scanned up to log sequence number 20171088175104
2020-10-27T00:58:41.493480Z 0 [Note] InnoDB: Doing recovery: scanned up to log sequence number 20171093417984
2020-10-27T00:58:41.579078Z 0 [Note] InnoDB: Doing recovery: scanned up to log sequence number 20171098660864
2020-10-27T00:58:41.608482Z 0 [Note] InnoDB: Doing recovery: scanned up to log sequence number 20171100172800
2020-10-27T00:58:41.608573Z 0 [ERROR] InnoDB: Ignoring the Redo log due to missing MLOG_CHECKPOINT between the checkpoint 20171067203814 and the end 20171100172800.
2020-10-27T00:58:41.608587Z 0 [ERROR] InnoDB: Plugin initialization aborted with error Generic error
2020-10-27T00:58:41.808775Z 0 [ERROR] Plugin 'InnoDB’ init function returned error.
2020-10-27T00:58:41.808802Z 0 [ERROR] Plugin 'InnoDB’ registration as a STORAGE ENGINE failed.
2020-10-27T00:58:41.808817Z 0 [ERROR] Failed to initialize builtin plugins.
2020-10-27T00:58:41.808823Z 0 [ERROR] Aborting
MySQL源码:
/* Look for MLOG_CHECKPOINT. /
recv_group_scan_log_recs(group, &contiguous_lsn, false);
/ The first scan should not have stored or applied any records. */
ut_ad(recv_sys->n_addrs == 0);
ut_ad(!recv_sys->found_corrupt_fs);
if (recv_sys->found_corrupt_log && !srv_force_recovery) {
log_mutex_exit();
return(DB_ERROR);
}
if (recv_sys->mlog_checkpoint_lsn == 0) {
if (!srv_read_only_mode
&& group->scanned_lsn != checkpoint_lsn) {
ib::error() << "Ignoring the redo log due to missing"
" MLOG_CHECKPOINT between the checkpoint "
<< checkpoint_lsn << " and the end "
<< group->scanned_lsn << ".";
if (srv_force_recovery < SRV_FORCE_NO_LOG_REDO) {
log_mutex_exit();
return(DB_ERROR);
}
}
在MySQL 5.7版本以前,InnoDB恢复的时候需要依赖数据字典,因为InnoDB根本不知道某个具体的space对应的ibd文件是哪个,这些信息都是数据字典维护的。而且在恢复前,需要把所有的表空间全部打开,如果库中有数以万计的表,把所有表打开一遍,整个过程就会很慢。那么MySQL 5.7在这上面做了哪些改进呢?
其实很简单,针对上面的问题,InnoDB在Redo log中增加了两种Redo log的类型来解决。MLOG_FILE_NAME用于记录在checkpoint之后所有被修改过的信息(space, filepath);MLOG_CHECKPOINT用于标志MLOG_FILE_NAME的结束。
上面两种Redo log类型的添加,完美解决了前面遗留的问题,Redo log中保存了后续需要恢复的space和filepath对。因此在恢复的时候,只需要从checkpoint的位置往后扫描到MLOG_CHECKPOINT的位置,这样就能获取到需要恢复的space和filepath,在恢复过程中,只需要打开这些ibd文件即可,当然由于space和filepath的对应关系通过Redo存了下来,恢复的时候也不再依赖数据字典。
这里需要强调的一点就是MLOG_CHECKPOINT在每个checkpoint点中最多只存在一次,如果出现多次MLOG_CHECKPOINT类型的日志,则说明Redo已经损坏,InnoDB会报错。最多存在一次,那么会不会有不存在的情况呢?
答案是肯定的,在每次checkpoint过后,如果没有发生数据更新,那么MLOG_CHECKPOINT就不会被记录。所以只要简单查找下Redo log最新一个checkpoint后的MLOG_CHECKPOINT是否存在,就能判定上次MySQL是否正常关机。5.7版本的MySQL在InnoDB进行恢复的时候,也正是这样做的。MySQL 5.7在进行恢复的时候,一般情况下需要进行最多3次的Redo log扫描:
第一次Redo log的扫描,主要是查找MLOG_CHECKPOINT,不进行Redo log的解析,如果没有找到MLOG_CHECKPOINT,则说明InnoDB不需要进行recovery,后面的两次扫描可以省略,如果找到了MLOG_CHECKPOINT,则获取MLOG_FILE_NAME到指定列表,后续只需打开该链表中的表空间即可。
MySQL会在Redo log写一个一字节的最新的MLOG_CHECKPOINT 标记,用来标记在此之前的Redo都已checkpoint完成。因为异常断电,已找不到这个标记,整个Redo日志文件都会被忽略,innodb引擎启动失败。若是有备份的话,可以用备份恢复,再通过binlog追上最新数据。若是没有备份的话,通过救援模式启动数据库,导出数据,再导入恢复。
1.查找备份文件
发现备份脚本在很久之前就没工作了,备份文件不是最新的。无法使用备份恢复。
2.删除Redo文件后启动
主库备份了Redo文件后,删除Redo文件,启动,失败。报错:
[ERROR] InnoDB: Page [page id: space=0, page number=7] log sequence number 20170595558339 is in the future! Current system log sequence number 10136858555422.
[ERROR] InnoDB: Your database may be corrupt or you may have copied the InnoDB tablespace but not the InnoDB log files. Please refer to http://dev.mysql.com/doc/refman/5.7/en/forcing-innodb-recovery.html for information about forcing recovery.
LSN(log sequence number)日志序列号,占用8字节,LSN主要用于发生crash时对数据进行recovery,LSN是一个一直递增的整型数字,表示事务写入到日志的字节总量。
LSN不仅只存在于重做日志中,在每个数据页头部也会有对应的LSN号,该LSN记录当前页最后一次修改的LSN号,用于在recovery时对比重做日志LSN号决定是否对该页进行恢复数据。
上面的报错显示,数据在磁盘的page包含了较新的LSN,而此时系统system表空间头的LSN却是旧LSN。
3.设置innodb_force_recovery的参数值
先是设置innodb_force_recovery=3,启动失败,后设置为4,设置为5,设置为6,最终启动成功。
innodb_force_recovery的参数说明:
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):不查看重做日志,InnoDB存储引擎会将未提交的事务视为已提交。
6(SRV_FORCE_NO_LOG_REDO):不执行前滚的操作。
这时候,主库是只读状态,可以导出数据。
4.导出
假如数据量不大的情况下,可以直接全库导出。
因为当前数据目录大容量占有1.7T,恢复业务的紧迫性和磁盘空间的不足,跟研发业务部门沟通,打算恢复基本的数据,几个超百G的大表只恢复近期数据。
1)导出系统库MySQL库(里面含有原实例账号数据)
mysqldump -uroot -p --databases mysql >mysql.sql
2)导出所有业务库表结构
mysql -uroot -p -e’show databases;’|grep -E -v “Database|mysql|sys|information_schema|performance_schema” | xargs mysqldump -uroot -p -d --triggers --routines --events --skip-lock-tables --set-gtid-purged=off --single-transaction --databases >alldb_desc.sql
3)导出所有业务库数据
忽略大表的数据没导出(根据实际情况,假如多个库的,分别导出,多个大表的,忽略多个),加快恢复速度。
mysqldump -uroot -p --set-gtid-purged=off -t 库名 --ignore-table=库名.忽略表名1 --ignore-table=库名.忽略表名2>库名.sql
4)导出大表部分数据
只保留近期数据(例如一个月的),过滤条件导出(根据实际情况,如果多个大表,多次导出)
mysqldump -uroot -proot --databases 库名 --tables 大表名 --where='ENTRY_DATE>=str_to_date('2020-10-01 00:00:00’,’%Y-%m-%d %H:%i:%s’)’ > 库名_大表名.sql
如果数据量大,导出时间长,可以考虑后台方式执行nohup 命令 &>>nohup.log
5.初始化实例
1)在从库机器上删除已存在数据和日志,初始化MySQL实例
mysqld --initialize --user=mysql
(备注:若是MySQL 5.6版本,初始化实例命令是mysql_install_db --user=mysql --defaults-file=/etc/my.cnf)
2)获取临时密码
cat mysqld.log|grep password
mysql -uroot -p临时密码
(备注:若是MySQL 5.6版本,初始化密码为空)
3)修改root密码
alter user 'root’@'localhost’ identified by 'Root_123’;
6.导入
1)修改参数,加快导入
为了加快导入速度,建议关闭双1,甚至可以暂时关闭binlog。
修改前查参数值:
show GLOBAL variables like 'foreign_key_checks’;
show GLOBAL variables like 'unique_checks’;
show GLOBAL variables like 'innodb_flush_log_at_trx_commit’;
show GLOBAL variables like 'sync_binlog’;
执行如下加速操作:
SET GLOBAL foreign_key_checks=0;
SET GLOBAL unique_checks=0;
SET GLOBAL innodb_flush_log_at_trx_commit=0;
SET GLOBAL sync_binlog=0;
2)数据导入
在主库导出的备份文件,传输到从库机器上,依次导入:
mysql -uroot -p <mysql.sql
导入MySQL库后,记得执行flush privileges,重新加载权限表到内存:
mysql -uroot -p < alldb_desc.sql
mysql -uroot -p <库名.sql
mysql -uroot -p 库名 <库名_大表名.sql
如果数据量大,导入出时间长,可以考虑后台方式执行nohup 命令 &>>nohup.log。
数据恢复完成,指定这个实例为主库,恢复业务。后期重做从库,恢复数据同步复制。
1、–all-databases , -A
含义:导出全部数据库。
示例:
mysqldump -uroot -p --all-databases
2、–add-drop-database
含义:每个数据库创建之前添加drop数据库语句。
示例:
mysqldump -uroot -p --all-databases --add-drop-database
3、–add-drop-table
含义:每个数据表创建之前添加drop数据表语句。(默认为打开状态,使用–skip-add-drop-table取消选项)
示例:
mysqldump -uroot -p --all-databases (默认添加drop语句)
mysqldump -uroot -p --all-databases –skip-add-drop-table (取消drop语句)
4、–databases, -B
含义:导出几个数据库。参数后面所有名字参量都被看作数据库名。
示例:
mysqldump -uroot -p --databases test mysql
5、–events, -E
含义:导出事件。
示例:
mysqldump -uroot -p --all-databases --events
6、–flush-logs
含义:开始导出之前刷新日志。
请注意:假如一次导出多个数据库(使用选项–databases或者–all-databases),将会逐个数据库刷新日志。除使用–lock-all-tables或者–master-data外。在这种情况下,日志将会被刷新一次,相应的所有表同时被锁定。因此,如果打算同时导出和刷新日志应该使用–lock-all-tables 或者–master-data 和–flush-logs。
示例:
mysqldump -uroot -p --all-databases --flush-logs
7、–flush-privileges
含义:在导出MySQL数据库之后,发出一条FLUSH PRIVILEGES 语句。为了正确恢复,该选项应该用于导出MySQL数据库和依赖MySQL数据库数据的任何时候。
示例:
mysqldump -uroot -p --all-databases --flush-privileges
8、–force
含义:在导出过程中忽略出现的SQL错误。
示例:
mysqldump -uroot -p --all-databases --force
9、–host, -h
含义:需要导出的主机信息
示例:
mysqldump -uroot -p --host=localhost --all-databases
10、–ignore-table
含义:不导出指定表。指定忽略多个表时,需要重复多次,每次一个表。每个表必须同时指定数据库和表名。
示例:
–ignore-table=database.table1 --ignore-table=database.table2 ……
mysqldump -uroot -p --host=localhost --all-databases --ignore-table=mysql.user
11、–master-data
含义:该选项将binlog的位置和文件名追加到输出文件中。如果为1,将会输出CHANGE MASTER 命令;如果为2,输出的CHANGE MASTER命令前添加注释信息。该选项将打开–lock-all-tables 选项,除非–single-transaction也被指定(在这种情况下,全局读锁在开始导出时获得很短的时间;其他内容参考下面的–single-transaction选项)。该选项自动关闭–lock-tables选项。
示例:
mysqldump -uroot -p --host=localhost --all-databases --master-data=1;
mysqldump -uroot -p --host=localhost --all-databases --master-data=2;
12、–no-create-db, -n
含义:只导出数据,而不添加CREATE DATABASE 语句。
示例:
mysqldump -uroot -p --host=localhost --all-databases --no-create-db
13、–no-create-info, -t
含义:只导出数据,而不添加CREATE TABLE 语句。
mysqldump -uroot -p --host=localhost --all-databases --no-create-info
14、–no-data, -d
含义:不导出任何数据,只导出数据库表结构。
示例:
mysqldump -uroot -p --host=localhost --all-databases --no-data
15、–password, -p
含义:连接数据库密码。
16、–port, -P
含义:连接数据库端口号。
17、–routines, -R
含义:导出存储过程以及自定义函数。
示例:
mysqldump -uroot -p --host=localhost --all-databases --routines
18、–single-transaction
含义:该选项在导出数据之前提交一个BEGIN SQL语句,BEGIN 不会阻塞任何应用程序且能保证导出时数据库的一致性状态。它只适用于多版本存储引擎,仅InnoDB。本选项和–lock-tables选项是互斥的,因为LOCK TABLES 会使任何挂起的事务隐含提交。要想导出大表的话,应结合使用–quick 选项。
示例:
mysqldump -uroot -p --host=localhost --all-databases --single-transaction
19、–tables
含义:覆盖–databases (-B)参数,指定需要导出的表名。
示例:
mysqldump -uroot -p --host=localhost --databases test --tables test
mysqldump -uroot -p --host=localhost --all-databases --triggers
20、–user, -u
含义:指定连接的用户名。
21、–where, -w
含义:只转储给定的WHERE条件选择的记录。请注意如果条件包含命令解释符专用空格或字符,一定要将条件引用起来。
示例:
mysqldump -uroot -p --host=localhost --all-databases --where=” user=’root’”