Percona Data Recovery Tool 单表恢复

时间:2021-01-28 21:34:04

前几天写过update或者delete忘加where条件的数据恢复。今天介绍一款开源的MySQL数据库InnoDB数据恢复工具:innodb-tools,它通过从原始数据文件中提取表的行记录,实现从丢失的或者被毁坏的MySQL表中恢复数据。例如,当你不小心执行DROP TABLE、TRUNCATE TABLE之后,可以通过以下方式恢复数据。

在介绍innodb-tools工具进行数据恢复之前,首先明确以下几点:

1、这个工具只能对InnoDB/XtraDB表有效,而无法恢复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> use book;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A Database changed
mysql> select count(*) from million_words;
+----------+
| count(*) |
+----------+
| 1000000 |
+----------+
1 row in set (0.13 sec) mysql> truncate million_words;
Query OK, 0 rows affected (0.23 sec) mysql>

3.构建工具

1、下载解压innodb-tools工具源码:

安装依赖,否则抛出如下错误:

/usr/bin/ld: cannot find -lrt
collect2: ld returned exit
status
make: *** [page_parser] Error
[root@localhost ~]# yum install glibc-static -y
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 -xvf percona-data-recovery-tool-for-innodb-0.5.tar.gz -C /usr/local/
cd /usr/local
ln -s percona-data-recovery-tool-for-innodb-0.5 percona-data-recovery-tool

2、进入解压后根目录下的mysql-source目录,运行配置命令(注:不运行make命令):

[root@localhost mysql-source]# cd /usr/local/percona-data-recovery-tool/mysql-source/
[root@localhost mysql-source]# ./configure

3、完成配置步骤后,回到解压后的根目录,运行make命令,编译生成page_parserconstraints_parser工具

[root@localhost mysql-source]# cd ..
[root@localhost percona-data-recovery-tool]# make

page_parser工具将根据InnoDB的底层实现原理,解析表的页和行结构。constraints_parser工具暂时不使用,后续还需要在定义表结构之后,重新编译生成它。

4. 提取需要的页

InnoDB页的默认大小是16K,每个页属于一个特定表中的一个特定的index。page_parser工具通过读取数据文件,根据页头中的index ID,拷贝每个页到一个单独的文件中。

如果启用了innodb_file_per_table=1,也就是独立表空间文件,那么将无法完全恢复数据,本人也已经测试过,官方文档也没有提到启用独立表空间是否可以成功,在官方文档中,是设置innodb_file_per_table=0。

参考资料如下:

http://www.percona.com/docs/wiki/innodb-data-recovery-tool:mysql-data-recovery:example_data_loss_scenario

4.1 切分页

运行page_parser工具进行切分:

如果MySQL是5.0之前的版本,InnoDB采取的是REDUNDANT格式,运行以下命令:

./page_parser - -f /path/to/ibdata1

如果MySQL是5.0以后的版本,InnoDB采取的是COMPACT格式,运行以下命令:

./page_parser - -f /path/to/ibdata1

运行后,page_parser工具会创建一个pages-<TIMESTAMP>的目录,其中TIMESTAMP是UNIX系统时间戳。在这个目录下,为每个index ID,以页的index ID创建一个子目录。例如:

[root@localhost percona-data-recovery-tool]# ./page_parser - -f /data/mysql/ibdata1
Opening file: /data/mysql/ibdata1:
ID of device containing file
inode number
protection
number of hard links
user ID of owner
group ID of owner
device ID (if special file)
total size, in bytes
blocksize for filesystem I/O
number of blocks allocated
time of last access
time of last modification
time of last status change
Size to process in bytes
Disk cache size in bytes
1.00% done. -- :: ETA(in : hours). Processing speed: B/sec
2.00% done. -- :: ETA(in : hours). Processing speed: B/sec
8.80% done. -- :: ETA(in : hours). Processing speed: B/sec
9.80% done. -- :: ETA(in : hours). Processing speed: B/sec
20.80% done. -- :: ETA(in : hours). Processing speed: B/sec
28.25% done. -- :: ETA(in : hours). Processing speed: B/sec
40.96% done. -- :: ETA(in : hours). Processing speed: B/sec
51.43% done. -- :: ETA(in : hours). Processing speed: B/sec
56.49% done. -- :: ETA(in : hours). Processing speed: B/sec
76.23% done. -- :: ETA(in : hours). Processing speed: B/sec
83.23% done. -- :: ETA(in : hours). Processing speed: B/sec
84.74% done. -- :: ETA(in : hours). Processing speed: B/sec
90.79% done. -- :: ETA(in : hours). Processing speed: B/sec
99.00% done. -- :: ETA(in : hours). Processing speed: B/sec
[root@localhost percona-data-recovery-tool]#
[root@localhost percona-data-recovery-tool]# ll pages-/FIL_PAGE_INDEX/-
total
-rw-r--r-- root root Mar : -.page

4.2 选择需要的Index ID

一般来说,我们需要根据表的主键(PRIMARY index)进行恢复,主键中包含了所有的行。以下是一些可以实现的步骤:

如果数据库仍处于运行状态,并且表没有被drop掉,那么可以启动InnoDB Tablespace Monitor,输出所有表和indexes,index IDs到MySQL server的错误日志文件。创建innodb_table_monitor表用于收集innodb存储引擎表及其索引的存储方式:

mysql> CREATE TABLE test.innodb_table_monitor (id int) ENGINE=InnoDB;
Query OK, 0 rows affected (0.31 sec) mysql>

如果innodb_table_monitor已经存在,drop表然后重新create表。等MySQL错误日志输出后,可以drop掉这张表以停止打印输出更多的监控。一个输出的例子如下:

TABLE: name book/million_words, id , flags , columns , indexes , appr.rows
COLUMNS: id: DATA_INT DATA_UNSIGNED DATA_BINARY_TYPE DATA_NOT_NULL len ; word: DATA_VARMYSQL DATA_NOT_NULL len ; DB_ROW_ID: DATA_SYS prtype len ; DB_TRX_ID: DATA_SYS prtype len ; DB_ROLL_PTR: DAT
A_SYS prtype len ;
INDEX: name PRIMARY, id , fields /, uniq , type
root page , appr.key vals , leaf pages , size pages
FIELDS: id DB_TRX_ID DB_ROLL_PTR word
INDEX: name word, id , fields /, uniq , type
root page , appr.key vals , leaf pages , size pages
FIELDS: word id

这里,我们恢复的是sakila库下的customer表,从上面可以获取其主键信息:

INDEX: name PRIMARY, id 374, fields /, uniq , type 

Index ID是0 374,因此我们需要恢复的InnoDB页位于0-374子目录下。

备注:参考文档原文中之描述了以上这种获取表的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中:

[root@localhost percona-data-recovery-tool]# ./create_defs.pl --host=127.0.0.1 --user=root --password=yayun --db=book --table=million_words  > include/table_defs.h

下面是生成的表定义:

#ifndef table_defs_h
#define table_defs_h // Table definitions
table_def_t table_definitions[] = {
{
name: "million_words",
{
{ /* int(10) unsigned */
name: "id",
type: FT_UINT,
fixed_length: , has_limits: FALSE,
limits: {
can_be_null: FALSE,
uint_min_val: ,
uint_max_val: 4294967295ULL
}, can_be_null: FALSE
},
{ /* */
name: "DB_TRX_ID",
type: FT_INTERNAL,
fixed_length: , can_be_null: FALSE
},
{ /* */
name: "DB_ROLL_PTR",
type: FT_INTERNAL,
fixed_length: , can_be_null: FALSE
},
{ /* varchar(50) */
name: "word",
type: FT_CHAR,
min_length: ,
max_length: , has_limits: FALSE,
limits: {
can_be_null: FALSE,
char_min_len: ,
char_max_len: ,
char_ascii_only: TRUE
}, can_be_null: FALSE
},
{ type: FT_NONE }
}
},
}; #endif

如果需要,可以根据需要编辑修改include/table_defs.h;然后根据include/table_defs.h,重新编译constraints_parser工具:

[root@localhost percona-data-recovery-tool]# make
gcc -DHAVE_OFFSET64_T -D_FILE_OFFSET_BITS= -D_LARGEFILE64_SOURCE= -D_LARGEFILE_SOURCE= -Wall -O3 -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= -D_LARGEFILE64_SOURCE= -D_LARGEFILE_SOURCE= -Wall -O3 -g -I include -I mysql-source/include -I mysql-source/innobase/include -c print_data.c -o lib/print_data.o
gcc -DHAVE_OFFSET64_T -D_FILE_OFFSET_BITS= -D_LARGEFILE64_SOURCE= -D_LARGEFILE_SOURCE= -Wall -O3 -g -I include -I mysql-source/include -I mysql-source/innobase/include -c check_data.c -o lib/check_data.o
gcc -DHAVE_OFFSET64_T -D_FILE_OFFSET_BITS= -D_LARGEFILE64_SOURCE= -D_LARGEFILE_SOURCE= -Wall -O3 -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= -D_LARGEFILE64_SOURCE= -D_LARGEFILE_SOURCE= -Wall -O3 -g -I include -I mysql-source/include -I mysql-source/innobase/include -static -lrt -o page_parser page_parser.c lib/tables_dict.o lib/libut.a
[root@localhost percona-data-recovery-tool]#

6. 从页中提取行记录

6.1 合并页到一个文件

前面已经提到,我们需要恢复的index ID 0 374,包含数据的页位于pages-1394180806/FIL_PAGE_INDEX/0-374/ 目录。

[root@localhost percona-data-recovery-tool]# cd pages-/FIL_PAGE_INDEX/-/
[root@localhost -]# ll
total
-rw-r--r-- root root Mar : -.page
-rw-r--r-- root root Mar : -.page
-rw-r--r-- root root Mar : -.page
-rw-r--r-- root root Mar : -.page
-rw-r--r-- root root Mar : -.page
-rw-r--r-- root root Mar : -.page
-rw-r--r-- root root Mar : -.page
-rw-r--r-- root root Mar : -.page
-rw-r--r-- root root Mar : -.page
-rw-r--r-- root root Mar : -.page
................................
................................

输入以下命令进行合并页:

[root@localhost percona-data-recovery-tool]# find pages-/FIL_PAGE_INDEX/-/ -type f -name '*.page' | sort -n | xargs cat > pages-/FIL_PAGE_INDEX/-/customer_pages_concatenated
[root@localhost percona-data-recovery-tool]#

生成的结果文件:pages-1394180806/FIL_PAGE_INDEX/0-374/customer_pages_concatenated,将作为constraints_parser工具的输入。

6.2 运行constraints_parser工具

下面到恢复数据最核心的步骤——运行constraints_parser工具以提取行记录。和page_parser工具一样,需要通过-5或-4参数指定InnoDB页格式(COMPACT/REDUNDANT),-f指定输入文件。

回到例子中,我们可以这样运行constraints_parser工具

我们可以这样运行constraints_parser工具(下面的命令是恢复一个单一的页,也可以直接恢复经过6.1步骤合并所有页之后的文件):

./constraints_parser - -f pages-/FIL_PAGE_INDEX/-/-.page

会输出恢复数据相关语句:

LOAD DATA INFILE '/usr/local/percona-data-recovery-tool/dumps/default/million_words' REPLACE INTO TABLE `million_words` FIELDS TERMINATED BY '\t' OPTIONALLY ENCLOSED BY '"' LINES STARTING BY 'million_words\t' (id, word);

既然这样,那么我们就创建dumps/default/文件夹

[root@localhost percona-data-recovery-tool]# pwd
/usr/local/percona-data-recovery-tool
[root@localhost percona-data-recovery-tool]# mkdir dumps/default -p
[root@localhost percona-data-recovery-tool]#

恢复全部页的数据到/dumps/default/million_words

[root@localhost percona-data-recovery-tool]# ./constraints_parser - -f pages-/FIL_PAGE_INDEX/-/customer_pages_concatenated >> dumps/default/million_words

输出提示如下,省略了一些内容:

95.98% done
96.42% done
96.86% done
97.30% done
97.74% done
98.19% done
98.63% done
99.07% done
99.51% done
99.96% done
LOAD DATA INFILE '/usr/local/percona-data-recovery-tool/dumps/default/million_words' REPLACE INTO TABLE `million_words` FIELDS TERMINATED BY '\t' OPTIONALLY ENCLOSED BY '"' LINES STARTING BY 'million_words\t' (id, word);

7. 导入数据到数据库中

mysql> use book
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A Database changed
mysql> LOAD DATA INFILE '/usr/local/percona-data-recovery-tool/dumps/default/million_words' REPLACE INTO TABLE `million_words` FIELDS TERMINATED BY '\t' OPTIONALLY ENCLOSED BY '"' LINES STARTING BY 'million_words\t' (id, word);
Query OK, 1062600 rows affected (14.99 sec)
Records: 1031300 Deleted: 31300 Skipped: 0 Warnings: 0 mysql> select count(*) from million_words;
+----------+
| count(*) |
+----------+
| 1000000 |
+----------+
1 row in set (0.20 sec) mysql>

可以看见数据已经恢复回来,我测试的MySQL版本如下:

mysql> select version();
+-------------+
| version() |
+-------------+
| 5.5.25a-log |
+-------------+
1 row in set (0.00 sec) mysql> show variables like 'innodb_file_per_table';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_file_per_table | OFF |
+-----------------------+-------+
1 row in set (0.00 sec) mysql>

希望各位小伙伴永远不需要用到此方法,备份才是王道啊!

参考资料如下:

http://www.percona.com/docs/wiki/innodb-data-recovery-tool:mysql-data-recovery:start

http://hidba.org/?p=852