记一次删库救火经历&总结数据恢复的一些方法
事出
前几天部门上线了个新页面,结果刚上线不到一小时就出现了一点小锅,需要临时改代码。本来代码快要改完了,突然群里面传来了一句话
救火
当时心里面就凉了一大半。删数据就算了。。偏偏删的还是最重要的那个表。而这个活动今年刚增加了二维码分享功能,还没上线一小时就记录了341封信件,用户量正在指数级增加,停服是不可能停服的了(错误想法),顿时整个人就懵了。
尝试使用万能的bin log
在mysql中,有一种叫bin log
的东西,它是用于记录对数据库数据进行更新操作的所有SQL语句,同时还会在每条语句上附加上timestamp。理论上如果保留了数据库建立以来所有的bin log,无论进行了什么操作都能将数据恢复回来,可以说是删库后的一根救命稻草了。
由于之前有过数据恢复的经验,意识恢复的时候立刻登上服务器查看mysql的配置文件,然而
还有一点热量的心瞬间凉透了。
疾病乱投医
之后我们几个部长疯狂的在网上搜索资料,祈祷仍然有救(错误想法,当时就该立刻停掉mysql并导出所有数据)
经过一段时间的搜索,在前人删库的经验上得到了以下的指南
如果 数据库引擎是MyISAM
则 收拾东西跑路
如果 数据库引擎是Innodb
则 还能抢救一下
赶紧看一下数据库配置,发现正好用的是Innodb引擎,似乎燃起了一点点希望。
同时,在看到这篇文章的介绍中强调了要立即关闭mysql导出数据时,才想起早该这样做,于是赶紧将该数据库下的所有文件都拷贝到另一文件夹中并拖到本地进行操作。
事情并没有那么顺利
回答的作者正是某数据库恢复工具的作者,于是我们找到了他开源的工具
undrop-for-innodb
并根据指引进行了一番尝试。
该工具要求被删库的数据库文件夹下的libdata1
文件
看了下介绍大概说的是从libdata
中读出被删表的表结构,并据此推断出实际存储到数据库中的数据格式,然后将整个服务器硬盘dump出来后,通过raw的方式读取并匹配这样的数据结构模式,以达到数据恢复的目的。
这种方式还是非常巧妙地,但是对于我们而言代价很高,整个服务器的硬盘有1t,将其dump下来需要很长时间。并且我在使用该工具的过程中,发现貌似并没有读取出原表的索引,所以也只能暂时将其放入备选方案中。
BTW:这个工具直接编译会有编译错误,具体参照issue里面的这个回复修改代码即可
黎明前的曙光
时间已经到了半夜2点,然而依然没有什么头绪。我翻了下拖下来的文件,发现有俩个文件貌似有点意思
它们分别为ib_logfile0
和 ib_logfile1
看到log
之后仿佛就有了希望,于是搜索了一番,发现这俩个文件是这个作用的:
Innodb是基于事务的数据库引擎,为了防止执行事务中途数据库崩溃导致数据不一致,它会在每次执行命令前将事务log进这俩个文件中。
看到这里瞬间整个人精神了,这俩个文件各有48M,而我们才上线1小时,望着里面写的日记应该还不算多,或许能从里面还原出所有的数据?
冥冥之中佛祖保佑
赶紧拿来一条已知数据中数据类型是int的数据在文件内搜索,果然找到了整个int,然而前后都是乱码。将编码调整为UTF-8
后(notepad++是个好东西)出现了大量的中文!说明数据是还能抢救回来的。
但是日记文件中并没有任何规律。搜索一番之后也并没有找到任何关于整个文件结构的说明。有人曾经在mysql支持论坛中提问过这个问题,但是并没有得到有效的答复。想想也是,这个只是一个类似于临时文件的东西,再且其他人删库的话一定是删了上线很久的数据库了,这俩个文件中的数据早已被覆盖,拿来也没用。所以估计要想弄明白的话要去翻mysql的源代码。
然而,冥冥之中上github搜索了一波,发现了一个好像是学生的人写的课程作业,就是用来尝试解析ib_logfile中的增删改语句
https://github.com/KasperFridolin/mysql_forensics
但是在使用过程中,发现久久没有得到输出,并且内存一直在增长,不知道是不是脚本出现了什么bug,也只好暂时搁置。
走向胜利
于是乎,决定手写脚本导出数据。在比对每一条数据后发现,刚好有一个字段存的要么是半年
要么是一年
。于是决定全文搜索这俩个关键字,然后根据特征将每条数据分离出来。
继续观察后发现,刚好这个固定字段的前一个字段的长度是固定的,然后每条记录后面都会出现表的名字的字样,并且在数据的末尾会有一个二进制标记符表示记录的结束。说干就干,拿起PHP就写代码(没错我就是喜欢用PHP写CLI脚本23333)
于是有了这下面的一段代码
<?php
$all_data = file_get_contents("ib_logfile1_other.txt");
$in = fopen("ib_logfile1_other.txt","r");
$out = fopen("out.txt", "rw");
$pos = 0;
$data = "";
$count = [];
function find0x99($data,$start){
$len = strlen($data);
while($start < $len)
{
if($data[$start] == chr(153))
{
return $start;
}
$start++;
}
return false;
}
function backfind0x99($data,$start){
while($start > 0)
{
if($data[$start] == chr(153))
{
return $start;
}
$start--;
}
return false;
}
while($pos < strlen($all_data)) {
$tmp = strpos($all_data, "一年", $pos);
if($tmp ==false) break;
$tmp = $tmp - 10;
$tmp2 = strpos($all_data,'qrcode_msg',$tmp);
$tmp3=strpos($all_data,"online_msg",$tmp);
if($tmp3>0 && $tmp3<$tmp2)
{
$pos = $tmp3;
continue;
}
if($tmp2 ==false) break;
$tmp2 = backfind0x99($all_data,$tmp2);
if($tmp2 ==false) break;
$target = substr($all_data,$tmp,$tmp2-$tmp);
$target = str_replace(" 一年","一年 ",$target);
$id = substr($target,0,8);
$file = fopen("out/$id.txt", "a");
fwrite($file, $target ."\n\n\n\n");
fclose($file);
$data =$data . $target . "\n\n\n\n";
$pos = $tmp2 + 1;
}
$pos = 0;
while($pos < strlen($all_data)) {
$tmp = strpos($all_data, "半年", $pos);
if($tmp ==false) break;
$tmp = $tmp - 10;
$tmp2 = strpos($all_data,'qrcode_msg',$tmp);
$tmp3=strpos($all_data,"online_msg",$tmp);
if($tmp3>0 && $tmp3<$tmp2)
{
$pos = $tmp3;
continue;
}
if($tmp2 ==false) break;
$tmp2 = backfind0x99($all_data,$tmp2);
if($tmp2 ==false) break;
$target = substr($all_data,$tmp,$tmp2-$tmp);
$target = str_replace(" 半年","半年 ",$target);
$id = substr($target,0,8);
$file = fopen("out/$id.txt", "a");
fwrite($file, $target ."\n\n\n\n");
fclose($file);
$data =$data . $target . "\n\n\n\n";
$pos = $tmp2;
}
file_put_contents("out.txt", $data);
最后,成功将所有数据挽救了回来
总结
首先,最重要的一点是,在线上数据库上执行SQL语句一定要谨慎再谨慎。
这次某人删库是因为精神状态不好,本来只是想select一下,于是翻mysql的执行记录,找到一条差不多的语句就无意识地按下了enter。而实际上,执行的语句恰好是上线前清空数据表的语句。有个定律好像是说墨菲定律,如果你觉得某件不可能的事情可能发生,那他很大概率会发生。谁会想到竟然会出了删库这种事情,而且起因又这么戏剧化
其次,保存好bin log+定期异地备份。凡事都留一手冗余,因为谁也不知道会发生什么。
再者,如果真的出事故了第一件事要做的事是保留现场,立即关掉mysql+关闭服务器,以防被删除的数据被覆盖了,以便留下哪怕是那么一点点希望。而这次我们得以恢复数据,完全靠的是运气,正好是刚上线就出事故了,写入的数据量少还没被覆盖。
冥冥之中,佛祖保佑