http://p8.qhimg.com/t01ef9eeb6297e9de30.png



前言


某天,在测试一张新数据表的字段时,由于在phpmyadmin不断地删除该数据表,导致一时不慎将整个数据库drop掉,当时立马就吓尿了,那是我们运营了一年的宝贵数据,突然就全没了,虽然还有两个月前的一次备份数据,但中间改动的很多新结构以及产生的新数据都没法恢复。情急之下,立刻关掉数据库服务器,在网上寻找各种不靠谱的硬盘数据恢复软件,试图通过恢复mysql相关的数据库文件来找回,但是都没有效果,只能找到一些数据表的结构文件(.frm),然而这些结构我其实都有备份,没什么卵用。由于之前网上看过通过mysql相关文件可以从不同机器恢复数据,坚信恢复数据是可能的,所以不断寻求方法。后来,先找了认识的一个研发同事帮忙,他告诉我他都是用的别人管理的数据库服务,会自动备份,不需要自己维护数据库,并且告诉我恢复不了;然后他又帮我找到了另一个DBA同事,来到了现场帮我看,看我没有开启mysql的binlog配置,又给了我一个结论:没办法恢复了!当时我就真慌了,于是带着压力和微弱的信念,最终找到了可行的恢复方案,特此记录。

这里事先说明一下,本人使用的mysql数据库是集成在xampp套件下的默认配置,并且我的数据库表都是使用的Innodb存储引擎(其他引擎有些好像还更好恢复),数据库的表结构也都事先有备份(没备份也有办法恢复,可以从.frm文件或者后面提到的项目工具提供的一些功能)。由于mysql配置中"innodb_file_per_table=OFF",表示所有的数据库表都在同一个数据空间,存储在一个ibdata1文件中(在mysql数据根目录中),所以最终我只需要将该ibdata1文件保存好(尽可能不被多次覆盖,最好误删的第一时间就备份好),就可以使用下面的开源工具来提取数据,恢复误删数据库的所有数据。


准备环境


获取源码

新建一个项目目录undrop-for-innodb,进入后使用git命令获取源码(或者解压我附件打包的源码):

1
git clone https://github.com/twindb/undrop-for-innodb.git

整个项目目录结构如下图所示:

t0114ebf906fb56c9af.jpg

编译项目

一条make命令直接编译,成功后会生成3个程序:

http://p3.qhimg.com/t0163b82b2635eafc05.jpg

导入测试数据

进入测试数据库目录sakila,其中有该数据库的各种数据表结构,先解压数据备份文件:

1
tar -zxvf sakila-db.tar.gz

然后进入mysql,新建一个数据库sakila:

1
CREATE DATABASE IF NOT EXISTS sakila default charset utf8 COLLATE utf8_general_ci;

这里我们先导入一张表actor的结构,然后导入全部数据(由于缺乏其它表结构会报错,忽略,确保actor的数据导入成功即可):

1
2
source ./actor.sql
source ./sakila-db/sakila-data.sql

确认测试数据导入成功:

http://p0.qhimg.com/t018df94bced763c528.jpg

接着,高潮来了,将整个数据库直接删掉:

1
drop database sakila;

傻了吧,这时如果我告诉你恢复不了了,你是不是要偷笑了(幸亏不是你的数据库)!别急,赶紧拷贝一份前面所说的数据空间文件ibdata1:

1
2
3
4
mkdir backup
cd backup/
sudo cp /var/lib/mysql/ibdata1 ./
sudo chmod +r ibdata1

OK,准备工作已经完成,下面就用编译的undrop项目工具,来进行数据恢复工作。


数据恢复


现在确认一下数据恢复的必要条件:一份ibdata1数据文件,一份要恢复的数据库的表结构(如本文以测试数据库sakila为例,恢复其中的actor表数据,需要actor表的结构,具体在actor.sql描述如下图;如果没有此文件可用其他方法得到表结构,如下文将提到的.frm文件恢复)。

http://p9.qhimg.com/t01686821004e6200d6.jpg

解析数据文件

首先,由于mysql将Innodb驱动的数据使用B+tree索引在了数据空间文件ibdata1中,所以需要使用stream_parser工具进行解析:

1
./../stream_parser -f ./ibdata1

解析完成后,可以看到同目录下生成一个pages-ibdata1目录,其中包含两个子目录,一个是包含按索引排序的数据页目录,另一个是包含相关类型的数据目录:

http://p2.qhimg.com/t01e8d883a7fd068367.jpg

我们下面将主要关注的是第一个子目录即索引好的数据页目录,因为我们要恢复的数据就在里面,其中第一个页文件(0000000000000001.page)里包含所有数据库的表信息和相关的表索引信息,类似一个数据字典,可以使用项目提供的一个脚本recover_dictionary.sh将其内容放到一个test数据库里详细的查看,这里就不做演示了。

解析页文件

既然第一个页文件包含所有数据库表的索引信息,我们就需要先解析它,以模拟mysql查询数据的过程,最终才能找到要恢复的数据。c_parser工具可以用来解析页文件,不过需要提供该页文件的一个内部结构(表结构)。好在,undrop项目已经帮我们准备好了一切,项目根目录下有个dictionary目录,里面就包含数据字典用到相关表结构,如用来解析第一个页文件的表结构在SYS_TABLES.sql文件如下:

http://p2.qhimg.com/t01ff6e68a324848e6f.jpg

于是,就可以开始恢复工作了:

1
./../c_parser -4Df pages-ibdata1/FIL_PAGE_INDEX/0000000000000001.page -t ./../dictionary/SYS_TABLES.sql  | grep actor

该命令使用c_parser工具解析数据库表索引信息并过滤出我们想要恢复的actor表:

http://p1.qhimg.com/t018abed22b749d9b8c.jpg

找到actor表后,得到该表的一个主索引值(如图所示为25),通过这个索引值,再到另外一张表去查询该actor表所有的索引信息,该表的结构在"dictionary/SYS_INDEXES.sql"文件中可以看到,而此表对应的数据页文件是第三个数据页0000000000000003.page,于是:

1
./../c_parser -4Df pages-ibdata1/FIL_PAGE_INDEX/0000000000000003.page -t ./../dictionary/SYS_INDEXES.sql | grep 25

同样能够解析出相关的索引数据:

t01c886487fe93da820.jpg

这里到了关键的时候,上图找到了actor表的两个索引信息(消重后),分别是"PRIMARY"和"idx_actor_last_name",分别对应于actor表结构的主键和索引键idx_actor_last_name,其对应在mysql存储中的索引值为54和55,此索引值编号对应的数据页文件中即存储了该索引的全部数据!所以解析的方法也差不多(有所差异,见如下命令),都需要输入一个参数即该数据表的结构以便能够正确解析出数据:

1
./../c_parser -5f pages-ibdata1/FIL_PAGE_INDEX/0000000000000054.page -t ./../sakila/actor.sql | more

此处我们选择的是主键索引对应的数据页文件进行解析(另外一个索引键应该也可以,只不过方法可能需要有所区别),终于顺利解析见到了激动人心的数据:

http://p4.qhimg.com/t01479726ea921f2a18.jpg

值得一提的是,如果所输入的表结构不正确(包括字段和索引,如果注释太长可以去掉否则可能报错),解析出来的数据就会出错,显示成错位的状况,这也是为什么我们需要事先拿到正确数据表结构的原因。最后,只需要将其转储到一个sql文件里就可以方便导入到数据库了:

1
./../c_parser -5f pages-ibdata1/FIL_PAGE_INDEX/0000000000000054.page -t ./../sakila/actor.sql > ./sakila_actor 2> ./sakila_actor.sql

此命令会在当前目录生成两个文件(分别是sakila_actor和sakila_actor.sql),其中sakila_actor.sql只是一个引导性文件,其内部调用命令语句“LOAD DATA LOCAL INFILE”加载sakila_actor文件内的真实数据,并且忽略外键检查(“SET FOREIGN_KEY_CHECKS=0”)。所以,导入到数据库之前,需要先根据数据表结构建好相应的数据表,再进行加载:

1
2
3
4
CREATE DATABASE IF NOT EXISTS sakila default charset utf8 COLLATE utf8_general_ci;
use sakila;
source ./../sakila/actor.sql
source ./sakila_actor.sql        #可能出错,原因是sakila_actor.sql里sakila_actor的路径写成默认的,需要调整成当前路径

至此,查询一下该数据表,可以看到全部数据正常恢复,使用mysqldump程序可以将其备份成可移植的sql数据文件,恢复工作顺利完成!


恢复数据表结构


前面提到数据表结构的重要性,顺便提一下数据表结构的恢复。undrop项目提供sys_parser工具(默认没有编译,需要自行安装相关的开发包环境进行编译),据说可以从ibdata1文件恢复出表结构,本人没有进行试验,只能说这是比较终极的数据恢复方案。这里主要提一下众所周知的方法:从frm文件恢复数据表结构。其实恢复过程比较简单,就是需要拿到待恢复数据库sakila的相关frm文件(默认在“/var/lib/mysql/sakila”),如本例可以找到actor.frm文件,然后只要通过mysql新建一个测试数据库,且在里面新建一张任意结构的Innodb表actor,最后替换一下该测试数据库下对应的frm文件重启mysql服务即可(其实在本人机器上测试,后面查询表结构时直接出错,应该和mysql的版本有关系,不过我相信众位机油们这点小问题应该不算什么哈)。


总结


本文就自己所遇到的场景做了一次数据恢复的演示,其本质是利用Innodb引擎索引数据的原理来对数据空间文件的数据进行提取。通过本文,希望遇到类似情形的朋友可以作为参考,不轻易放弃对数据的恢复,同时也奉劝各位做好数据备份工作,因为偷懒少写的那几行代码,有可能让你付出惨痛的代价,天灾人祸无可避免。另外,服务器安全维护工作也很重要,如果被人拿走了本文所说的重要文件ibdata1(默认需要root权限),那就和被人脱裤差不多了。



本文由 安全客 原创发布,作者:维一零