本文从实际使用经验出发,介绍一款开源的MySQL数据库InnoDB数据恢复工具:innodb-tools,它通过从原始数据文件中提取表的行记录,实现从丢失的或者被毁坏的MySQL表中恢复数据。例如,当你不小心执行DROP TABLE、TRUNCATE TABLE或者DROP DATABASE之后,可以通过以下方式恢复数据。
以下内容大部分参考自:Percona Data Recovery Tool for InnoDB,文档是英文的,而且写的比较晦涩,这里是个人的实战经验总结,供大家参考学习。
在介绍innodb-tools工具进行数据恢复之前,首先明确以下几点:
1、这个工具只能对InnoDB/XtraDB表有效,而无法恢复MyISAM表(注: Percona号称有一套用于恢复MyISAM表的工具,但是本人未做尝试)。
2、这个工具是以保存的MySQL数据文件进行恢复的,而不用MySQL Server运行。
3、不能保证数据总一定可被恢复。例如,被重写的数据不能被恢复,这种情况下可能需要针对系统或物理的方式来恢复,不属于本工具的范畴。
4、恢复的最好时机是当你发现数据丢失时,尽快备份MySQL数据文件。
5、使用这个工具需要手动做一些工作,并不是全自动完成的。
6、恢复过程依赖于你对丢失数据的了解程度,在恢复过程中可能需要在不同版本的数据之间做出选择。那么如果你越了解自己的数据,恢复的可能性就越大。
接下来,下面通过一个例子来介绍如何通过这个工具进行恢复。
1. 前提条件
首先,需要理解的是innodb-tools工具不是通过连接到在线的database进行数据恢复,而是通过离线拷贝数据的方式进行的。注意:不要在MySQL运行的时候,直接拷贝InnoDB文件,这样是不安全的,会影响数据恢复过程。
为了完成数据恢复,必须知道将要被恢复的表结构(列名、数据类型)。最简单的方式就是SHOW CREATE TABLE,当然后续会介绍几种可替代的方式。因此,如果有一个MySQL server作为备份,即使数据是很早的甚至表中没有记录,可以有助于使用innodb-tools工具进行恢复。不过这个不是必须的。
2. 简单例子
mysql> TRUNCATE TABLE customer;
3. 构建工具
为了构建innodb-tools工具,需要依赖于C编译器、make工具等。
1、下载解压innodb-tools工具源码:
wget https://launchpad.net/percona-data-recovery-tool-for-innodb/trunk/release-0.5/+download/percona-data-recovery-tool-for-innodb-0.5.tar.gz
tar -zxvf percona-data-recovery-tool-for-innodb-0.5.tar.gz
2、进入解压后根目录下的mysql-source目录,运行配置命令(注:不运行make命令):
cd percona-data-recovery-tool-for-innodb-0.5/mysql-source
./configure
3、完成配置步骤后,回到解压后的根目录,运行make命令,编译生成page_parser和constraints_parser工具:
cd ..
make
page_parser工具将根据InnoDB的底层实现原理,解析表的页和行结构。constraints_parser工具暂时不使用,后续还需要在定义表结构之后,重新编译生成它。
如果编译过程中出现问题,点击这里。本文使用过程中没有出现问题,故不再一一列举。
4. 提取需要的页
InnoDB页的默认大小是16K,每个页属于一个特定表中的一个特定的index。page_parser工具通过读取数据文件,根据页头中的index ID,拷贝每个页到一个单独的文件中。
如果你的MySQL server被配置为innodb_file_per_table=1,那么系统已经帮你实现上述过程。所有需要的页都在.ibd文件,而且通常你不需要再切分它。然而,如果.ibd文件中可能包含多个index,那么将页单独切分开还是有必要的。如果MySQL server没有配置innodb_file_per_table,那么数据会被保存在一个全局的表命名空间(通常是一个名为ibdata1的文件,本文属于这种情况),这时候就需要按页对文件进行切分。
4.1 切分页
运行page_parser工具进行切分:
如果MySQL是5.0之前的版本,InnoDB采取的是REDUNDANT格式,运行以下命令:
./page_parser -4 -f /path/to/ibdata1
如果MySQL是5.0版本,InnoDB采取的是COMPACT格式,运行以下命令:
./page_parser -5 -f /path/to/ibdata1
运行后,page_parser工具会创建一个pages-的目录,其中TIMESTAMP是UNIX系统时间戳。在这个目录下,为每个index ID,以页的index ID创建一个子目录。例如:
pages-1330842944/FIL_PAGE_INDEX/0-1/1-00000008.page
pages-1330842944/FIL_PAGE_INDEX/0-1/6-00000008.page
4.2 选择需要的Index ID
一般来说,我们需要根据表的主键(PRIMARY index)进行恢复,主键中包含了所有的行。以下是一些可以实现的步骤:
如果数据库仍处于运行状态,并且表没有被drop掉,那么可以启动InnoDB Tablespace Monitor,输出所有表和indexes,index IDs到MySQL server的错误日志文件。创建innodb_table_monitor表用于收集innodb存储引擎表及其索引的存储方式:
mysql> CREATE TABLE innodb_table_monitor (id int) ENGINE=InnoDB;
如果innodb_table_monitor已经存在,drop表然后重新create表。等MySQL错误日志输出后,可以drop掉这张表以停止打印输出更多的监控。一个输出的例子如下:
TABLE: name sakila/customer, id 0 142, columns 13, indexes 4, appr.rows 0
COLUMNS: customer_id: DATA_INT len 2 prec 0; store_id: DATA_INT len 1 prec 0; first_name: type 12 len 135 prec 0; last_name: type 12 len 135 prec 0; email:
type 12 len 150 prec 0; address_id: DATA_INT len 2 prec 0; active: DATA_INT len 1 prec 0; create_date: DATA_INT len 8 prec 0; last_update: DATA_INT len 4 pr
ec 0; DB_ROW_ID: DATA_SYS prtype 256 len 6 prec 0; DB_TRX_ID: DATA_SYS prtype 257 len 6 prec 0; DB_ROLL_PTR: DATA_SYS prtype 258 len 7 prec 0;
INDEX: name PRIMARY, id 0 286, fields 1/11, type 3
root page 50, appr.key vals 0, leaf pages 1, size pages 1
FIELDS: customer_id DB_TRX_ID DB_ROLL_PTR store_id first_name last_name email address_id active create_date last_update
INDEX: name idx_fk_store_id, id 0 287, fields 1/2, type 0
root page 56, appr.key vals 0, leaf pages 1, size pages 1
FIELDS: store_id customer_id
INDEX: name idx_fk_address_id, id 0 288, fields 1/2, type 0
root page 63, appr.key vals 0, leaf pages 1, size pages 1
FIELDS: address_id customer_id
INDEX: name idx_last_name, id 0 289, fields 1/2, type 0
root page 1493, appr.key vals 0, leaf pages 1, size pages 1
FIELDS: last_name customer_id
这里,我们恢复的是sakila库下的customer表,从上面可以获取其主键信息:
INDEX: name PRIMARY, id 0 286, fields 1/11, type 3
Index ID是0 256,因此我们需要恢复的InnoDB页位于0-256子目录下。
备注:参考文档原文中之描述了以上这种获取表的index ID的方法,本文在实际操作中,采取了更简单的一种方式,即直接恢复page_parser生成的所有InnoDB页。实践证明这种方法也是可行的:)
5. 生成表定义
步骤4中,我们已经找到了需要的数据,接下来需要找到表结构,创建表定义,将其编译到constraints_parser中,然后使用这个工具从InnoDB页中提取表中的行。
表定义包含了表中的列、列顺序、数据类型。如果MySQL server仍处于运行且表未被drop掉,那么简单实用SHOW CREATE TABLE就可以收集到这些信息。接下来将使用这些表结构信息来创建一个C结构体标识的表定义,然后编译到constraints_parser工具。C结构体的定义存放在include/table_defs.h中。
最简单的方式是create_defs.pl Perl 脚本,连接到MySQL server,读取SHOW CREATE TABLE的结果,输出生成的表定义到标准输出。下面是个例子,其中直接将结果重定向到了include/table_defs.h中:
If possible, the easiest way to create the table definition is with the create_defs.pl Perl script. It connects to the MySQL server and reads SHOW CREATE TABLE output, and prints the generated definition to its standard output. Here is an example:
$ ./create_defs.pl --host=localhost --user=root --password=123456 --db=sakila --table=customer > include/table_defs.h
下面是例子中的表结构:
CREATE TABLE `customer` (
`customer_id` smallint(5) UNSIGNED NOT NULL AUTO_INCREMENT,
`store_id` tinyint(3) UNSIGNED NOT NULL,
`first_name` varchar(45) NOT NULL,
`last_name` varchar(45) NOT NULL,
`email` varchar(50) DEFAULT NULL,
`address_id` smallint(5) UNSIGNED NOT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
`create_date` datetime NOT NULL,
`last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`customer_id`),
KEY `idx_fk_store_id` (`store_id`),
KEY `idx_fk_address_id` (`address_id`),
KEY `idx_last_name` (`last_name`),
CONSTRAINT `fk_customer_address` FOREIGN KEY (`address_id`) REFERENCES `address` (`address_id`) ON UPDATE CASCADE,
CONSTRAINT `fk_customer_store` FOREIGN KEY (`store_id`) REFERENCES `store` (`store_id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8
下面是生成的表定义:
#ifndef table_defs_h
#define table_defs_h
//Table definitionstable_def_t table_definitions[] = {
{
name: "customer",
{
{ /*smallint(5) unsigned*/
name: "customer_id",
type: FT_UINT,
fixed_length: 2,
has_limits: TRUE,
limits: {
can_be_null: FALSE,
uint_min_val: 0,
uint_max_val: 65535
},
can_be_null: FALSE
},
{ /*Innodb's internally used field*/
name: "DB_TRX_ID",
type: FT_INTERNAL,
fixed_length: 6,
can_be_null: FALSE
},
{ /*Innodb's internally used field*/
name: "DB_ROLL_PTR",
type: FT_INTERNAL,
fixed_length: 7,
can_be_null: FALSE
},
{ /*tinyint(3) unsigned*/
name: "store_id",
type: FT_UINT,
fixed_length: 1,
has_limits: TRUE,
limits: {
can_be_null: FALSE,
uint_min_val: 0,
uint_max_val: 255
},
can_be_null: FALSE
},
{ /*varchar(45)*/
name: "first_name",
type: FT_CHAR,
min_length: 0,
max_length: 45,
has_limits: TRUE,
limits: {
can_be_null: FALSE,
char_min_len: 0,
char_max_len: 45,
char_ascii_only: TRUE
},
can_be_null: FALSE
},
{ /*varchar(45)*/
name: "last_name",
type: FT_CHAR,
min_length: 0,
max_length: 45,
has_limits: TRUE,
limits: {
can_be_null: FALSE,
char_min_len: 0,
char_max_len: 45,
char_ascii_only: TRUE
},
can_be_null: FALSE
},
{ /*varchar(50)*/
name: "email",
type: FT_CHAR,
min_length: 0,
max_length: 50,
has_limits: TRUE,
limits: {
can_be_null: TRUE,
char_min_len: 0,
char_max_len: 50,
char_ascii_only: TRUE
},
can_be_null: TRUE
},
{ /*smallint(5) unsigned*/
name: "address_id",
type: FT_UINT,
fixed_length: 2,
has_limits: TRUE,
limits: {
can_be_null: FALSE,
uint_min_val: 0,
uint_max_val: 65535
},
can_be_null: FALSE
},
{ /*tinyint(1)*/
name: "active",
type: FT_INT,
fixed_length: 1,
can_be_null: FALSE
},
{ /*datetime*/
name: "create_date",
type: FT_DATETIME,
fixed_length: 8,
can_be_null: FALSE
},
{ /*timestamp*/
name: "last_update",
type: FT_UINT,
fixed_length: 4,
can_be_null: FALSE
},
{ type: FT_NONE }
}
},
};
#endif
如果需要,可以根据需要编辑修改include/table_defs.h;然后根据include/table_defs.h,重新编译constraints_parser工具:
$ make
gcc -DHAVE_OFFSET64_T -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE=1 -D_LARGEFILE_SOURCE=1 -g -I include -I mysql-source/include -I mysql-source/innobase/include -c tables_dict.c -o lib/tables_dict.o
gcc -DHAVE_OFFSET64_T -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE=1 -D_LARGEFILE_SOURCE=1 -g -I include -I mysql-source/include -I mysql-source/innobase/include -o constraints_parser constraints_parser.c lib/tables_dict.o lib/print_data.o lib/check_data.o lib/libut.a lib/libmystrings.a
gcc -DHAVE_OFFSET64_T -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE=1 -D_LARGEFILE_SOURCE=1 -g -I include -I mysql-source/include -I mysql-source/innobase/include -o page_parser page_parser.c lib/tables_dict.o lib/libut.a
6. 从页中提取行记录
6.1 合并页到一个文件
前面已经提到,我们需要恢复的index ID 0 286,包含数据的页位于pages-1246363747/0-286/ 目录。
total 120
-rw-r--r-- 1 root root 16384 Jun 30 05:09 1254-00001254.page
-rw-r--r-- 1 root root 16384 Jun 30 05:09 1255-00001255.page
-rw-r--r-- 1 root root 16384 Jun 30 05:09 1256-00001256.page
-rw-r--r-- 1 root root 16384 Jun 30 05:09 1257-00001257.page
-rw-r--r-- 1 root root 16384 Jun 30 05:09 50-00000050.page
-rw-r--r-- 1 root root 16384 Jun 30 05:09 74-00000050.page
输入以下命令进行合并页:
$ find pages-1246363747/0-286/ -type f -name '*.page' | sort -n | xargs cat > pages-1246363747/0-286/customer_pages_concatenated
生成的结果文件:pages-1246363747/0-286/customer_pages_concatenated,将作为constraints_parser工具的输入。
6.2 运行constraints_parser工具
下面到恢复数据最核心的步骤——运行constraints_parser工具以提取行记录。和page_parser工具一样,需要通过-5或-4参数指定InnoDB页格式(COMPACT/REDUNDANT),-f指定输入文件。
回到例子中,我们可以这样运行constraints_parser工具(下面的命令是恢复一个单一的页,也可以直接恢复经过6.1步骤合并所有页之后的文件):
$ ./constraints_parser -5 -f pages-1246363747/0-286/50-00000050.page
输出结果中每行包含表名以及表中的各个列。备注:其中可能有正确的行记录,也可能有不正确的行记录。官方文档中这个章节给出了如何调整表定义获取尽可能多的有效数据,同时过滤掉垃圾行,这里不再详细描述。
customer 0 120 "" "" "" 32770 0 "0000-00-00 00:12:80" 0
customer 0 0 "" "" "" 0 0 "9120-22-48 29:44:00" 2
customer 61953 0 "" "" "" 2816 0 "7952-32-67 11:43:49" 0
customer 0 0 "" "" "" 0 0 "0000-00-00 00:00:00" 0
... snip ...
customer 0 0 "" "" "" 0 0 "0000-00-00 00:00:00" 16777728
customer 28262 114 "" "" NULL 25965 117 "4603-91-96 76:21:28" 5111809
customer 0 82 "" "" "" 22867 77 "2775-94-58 03:19:18" 1397573972
customer 2 1 "PATRICIA" "JOHNSON" "PATRICIA.JOHNSON@sakilacustomer.org" 6 1 "2006-02-14 22:04:36" 1140008240
customer 3 1 "LINDA" "WILLIAMS" "LINDA.WILLIAMS@sakilacustomer.org" 7 1 "2006-02-14 22:04:36" 1140008240
customer 4 2 "BARBARA" "JONES" "BARBARA.JONES@sakilacustomer.org" 8 1 "2006-02-14 22:04:36" 1140008240
customer 5 1 "ELIZABETH" "BROWN" "ELIZABETH.BROWN@sakilacustomer.org" 9 1 "2006-02-14 22:04:36" 1140008240
customer 6 2 "JENNIFER" "DAVIS" "JENNIFER.DAVIS@sakilacustomer.org" 10 1 "2006-02-14 22:04:36" 1140008240
customer 7 1 "MARIA" "MILLER" "MARIA.MILLER@sakilacustomer.org" 11 1 "2006-02-14 22:04:36" 1140008240
customer 8 2 "SUSAN" "WILSON" "SUSAN.WILSON@sakilacustomer.org" 12 1 "2006-02-14 22:04:36" 1140008240
customer 9 2 "MARGARET" "MOORE" "MARGARET.MOORE@sakilacustomer.org" 13 1 "2006-02-14 22:04:36" 1140008240
... snip ...
customer 0 0 "" "" "" 0 0 "0000-00-00 00:00:00" 0
customer 0 0 "" "" "" 0 0 "7679-35-98 86:44:53" 720578985
7. 导入数据到数据库中
最后,为了完成数据恢复,需要将步骤6中constraints_parser工具的输出结果,使用LOAD DATA INFILE命令导入到数据库中。命令如下:
LOAD DATA INFILE '/tmp/customer_data.tsv'
REPLACE INTO TABLE customer
FIELDS TERMINATED BY '\t'
OPTIONALLY ENCLOSED BY '"'
LINES STARTING BY 'customer\t'
(customer_id, store_id, first_name, last_name, email,
address_id, active, create_date, @last_update)
SET last_update = FROM_UNIXTIME(@last_update);
至此,完成了数据的恢复和导入过程。希望大家不会有机会去实践这篇文章介绍的方法。