From Zero To X

zTrix's Blog

MooseFS metadata.mfs 数据恢复纪实

从这周四下午到周六早上这 40 个小时真是紧张惊险又刺激。以至于我要写篇 blog 来记录一下。

数据丢失! 天灾人祸!

实验室跑了一套自己的网盘系统,其中后台数据存储用的是 Moosefs 网络分布式文件系统。这个系统用起来一直很好,直到 2 天前…

周四下午,机房突然断电,听说是隔壁机房安装照明灯的时候不小心短路造成的跳闸(这机房真是!!随便就断电!!还没有UPS!!)。好吧,断电这种事情,也经历多次了,所以大家都不在意。

来电之后,服务器都自动启动了。但是过了很久,服务都没自动启动——服务都是有自动启动脚本的,平常断电之后服务都会自动启动——于是我就手动 ssh 到服务器去查,发现 moosefs 的 master 没有运行。我想可能是自动启动失败了,就尝试手动启动它。结果发现仍然不能启动,给我的提示信息是,metadata.mfs 文件不存在。

can't open metadata file
if this is new instalation then rename metadata.mfs.empty as metadata.mfs
init: file system manager failed !!!
error occured during initialization - exiting

这个时候我才想起来,moosefs 运行的时候,会把 metadata.mfs 重命名成 metadata.mfs.back,正常结束的时候再重命名回去,而这次是突然断电,所以 metadata.mfs 不存在,mfsmaster 拒绝启动。

于是我再把 metadata.mfs.back 重命名成 metadata.mfs,再次启动,结果又出了错误信息:

loading metadata ...
can't read metadata header
init: file system manager failed !!!
error occured during initialization - exiting

突然我心里一紧,感觉到问题有点严重。难道断电导致了文件损坏?于是我去检查这个文件,仔细一看,文件大小居然是 0 !也就是说之前的 200M metadata 信息全部丢失!直接导致 moosefs 里面存的 50T 数据全部变成了废数据。并且,这个文件之前因为一直没人注意,还是没有备份的!!这真是天灾人祸并发呀!

丢失原因?

难道有人不小心动了这个文件,或者有人有误操作?我把操作历史看了几遍,确认没有人动这个文件,也没有误操作。那文件是怎么就变成 0 了的呢?

实验室展开了火热的讨论。最终总结出这么几个可能导致文件丢失的原因:

  • ext4 文件系统的 delay allocation 导致的。moosefs 在持久化 metadata 信息的时候,会重写整个文件。他会首先把之前的 metadata.mfs.back 文件重命名成 metadaba.mfs.back.tmp,然后开始写 metadata.mfs.back 文件,写完了 fclose 之后,删除 metadata.mfs.back.tmp。如果断电的时候,正常在进行这么一个过程,那么有可能 fclose 之后,metadata.mfs.back.tmp 也删除了,但是 ext4 的 delay allocation 特性导致文件还没有真正写入到磁盘。(也有可能文件写入了磁盘 block,但是文件 inode 还没有更新)。我们在网上发现有不少人也遇到过同样的断电之后文件大小变成 0 的问题,如这个:http://www.symantec.com/connect/blogs/ext4-data-recovery-how-recovery-lost-files-ext4-file-system-linux
  • 有可能是 moosefs 写 metadata 失败导致的。阅读了相关源代码之后,我们发现 moosefs 写文件和 fclose 的时候居然从来不判断返回值!有些地方有判断返回值,但是也仅仅打印一句 log,既不做相应错误处理,也不停止。于是就有可能是写文件失败,fclose 也失败,但是它不查返回值,所以又把之前的 metadata.mfs.back.tmp 删除了。
  • 有可能是 ext4 修复导致的。断电造成文件系统出现不一致,然后自动修复之后造成了数据丢失。这突然让我想起了我去年 11 月的遭遇(https://twitter.com/#!/zhuwl08/status/129538485156716544),当时写代码写着写着居然代码文件变成了目录,代码也丢了。我用的也是 ext4,这不断电都可以出这样的事情,断电丢个文件也就不算什么了。可见 ext4 还是不够靠谱阿。

数据恢复

再怎么分析丢失原因也于事无补了,于是大家开始各种尝试恢复数据。我们先把服务器关机,用一个启动盘启动,然后把整个分区 dd 出来,每人发一份尝试恢复。

数据恢复软件

首先尝试的是 extundelete 和 ext3grep,尝试了几次之后,发现实在是太弱了,不仅啥都恢复不出来,给个 --restore-all 参数居然直接就崩溃了。

随后发现 R-Linux 这个软件貌似比较强大,于是立刻去下载了一个。试了一下,果然很强大!能进行各种恢复:按时间,按文件类型,按目录。于是开始用它来跑。

我们先试了目录恢复,恢复出了两个文件,但是大小也是 0。再尝试按时间恢复,把最近 5 个月的全部恢复出来。这一下恢复出了一堆数据。居然之前已经删除的各种 log 什么的都还能恢复出来!但是一番搜索之后,我们发现,几个月前的数据都能恢复,单独这个当天的数据怎么也恢复不出来。折腾了很久之后,R-Linux 能耍的花样我们都试了一遍,结果是一无所获。大家都有点灰心丧气,觉得用软件恢复可能性不大了。

另外一个尝试的软件是 photorec,据说也比较强大,但是我没有用过。一位师兄尝试了之后说 photorec 结果还不错,能找到一些开头信息,但是,也找不全,或者文件大小太大,居然有 2G+。那个文件显然没有这么大。

数据恢复公司

希望在恢复软件上破灭之后,又在专业数据恢复公司上升起。我们打听到飞客数据恢复中心很不错,有个师兄就拿着数据去了中关村。经过焦急的等待,最终,等来的还是坏消息:inode 被覆盖了,无法恢复数据。

自己动手

看来得自己动手了。有人研究 ext4,有人研究是否有可能重建索引,我则去研究 medatata.mfs 文件格式。

moosefs 最坑爹的一点就是:没文档,代码没注释,所以想了解 metadata.mfs 文件格式的话,只能阅读源代码了。

header

首先发现的一点就是,metadata.mfs 前 8 个字节是 magic number: “MFSM 1.5”。于是我写了个 c 程序,从头到尾搜索了一遍,总共发现了 17 个。能找到文件开头就标志还有希望!

由于文件开头总是应该在 block 的起始(查了一下 我们用的 ext4 blocksize 是 4096),所以排除了其中 7 个位置不能被 4096 整除的,还剩下 10 个。

继续看代码,发现 9 – 12 字节是 maxID,越大表示文件越新。把 10 个开头都看了一遍,发现其中第 9 个最新,文件相对于分区 image 的 offset 为:50440699904 byte。

于是我准备从这里开始,顺藤摸瓜找下去,看看能找到多少有用的信息。如果这个文件格式特征明显的话,应该是能够顺藤摸瓜搜索拼接出来的。

写程序解析 medadata.mfs

花了几个小时时间,我利用 moosefs 的源码,写了一个程序去解析 metadata.mfs 文件。一旦解析出错,程序就会自动报错退出,从而知道拼接出来的文件是错误的。这样我就可以去看文件在这个地方大概是什么样的,下面应该是什么样,找出特征写程序搜索。

写完程序之后,把刚才的第 9 个开头开始的前面几M dd 出来。然后跑了一遍程序。运气不错!连续 16M 的数据都是属于 metadata.mfs 的,到了 16M 和 17M 的交界处,程序解析出错了,说明文件到这里不连续了,那么下一段文件在哪呢?是否还在磁盘上呢?

进一步分析发现,这前 16M 数据都是 metadata.mfs 的 node 信息。

这里顺便提一下 metadata.mfs 的文件格式。这个文件由好几个部分组成:首先是 header, 24bytes, 然后是 node 信息,最后用一个空 node 标志结束,然后是 edge 信息,同样用一个空 edge 标志结束。这两个部分比较大,node 部分基本占总文件的一半,edge 部分占四分之一。

node 和 edge 之后,是比较小的一些杂乱信息。最后一部分是比较长的 chunk 信息,最后用一个空 chunk 来标志结束。也就是说这个文件基本上每个区的结束都是用这个区的一个空数据结构来标志的,而不是有一个区 header 指定长度。

找到前面 16M 之后,我就开始琢磨怎么寻找下一段数据。用 xxd 看了一下前面 16M 数据,发现每个 node 信息数据结构里面都有 4 个时间,分别是 atime, ctime, mtime 和 trash time(单位是 s,from epoch)。系统是今年才开始运行的,所以时间都是 2012 年之后,表示成 16 进制的话,就是 4f xx xx xx。于是特征就有了!如果一个 block 能接的上这个文件的前 16M 的话,他里面肯定会有很多时间信息,那就会有很多 4f。

我很快写了一个 c 程序,以 4096(block size) 为单位去读整个分区文件,然后统计这个 block 里面的 4f 的数量,如果超过一个 threshold 的话,说明很可能属于这个文件,那就可以继续分析接的上的问题了。程序跑了十几分钟,结果很不理想,因为太多了。大概有 4000 多个区块组都包含很多 4f,也就是说这 4000 个都有可能是这个文件的,也都有可能是正好接上这个文件的。刚刚才兴奋起来,这下又苦恼了。

不过天无绝人之路。我突然想到,说不定 ext4 分配数据区块给文件的时候,会向后找尽量近的地方。我就看了一下前面 16M 数据在我的分析结果里面的位置,然后发现,在它 32 M 之后,这个有很多 4f 这个特征又明显了起来。我把 32M 之后的数据 dd 了出来。那怎么知道是否和前面 16M 接的上呢?

我又看了一眼代码,发现每个 node 都有一个 id,于是加了一个 printf 去把这个 id 打印出来。结果让我非常惊喜,id 是连续的!所以如果正好能接得上的话,id 应该正好连续!我连忙把前 16M 的数据的最后一个 node id 打了出来,又用 xxd 看了一下后面的数据,id 真的是能正好接上的!于是文件的第二段就这么找到了!!

赶紧把两段数据拼接起来,跑了一遍程序,发现第二段还比较长,两段数据加起来有 56M 之多。到目前为止,文件的前 56M 已经被我恢复出来了。

用同样的方法继续寻找,我很快得到了文件的第 3 4 5 6 段。这样就有了 128M 的数据了。到了 128M 的时候,node 区结束了,到了 edge 区了。

上面的方法不能继续用了,新的挑战来了。虽然方法不能继续用,但是思想肯定还是一致的——果然,edge 区有两个 id 可以用,一个 parent id,一个 child id。用程序打印了一下 id 发现,虽然不是严格连续,但是规律性很明显,基本上是 parent id 很久不变,child id 递减,减到 0 之后,parent id 就变一个。

有了这个特征之后,edge 区也很容易分析了。用这个特征,我恢复出了文件前 168M 的数据。令人兴奋的一点是,这 168M 里面,edge 区在最后 8M 结束了,同时 edge 区之后的几个比较小的区直接也包含在这 8M 里了,最后一个大去 chunk 区在这 8M 里面开始。于是就不需要分析中间几个小区了,只需要分析 chunk 区就行了。胜利就在眼前了!

chunk 区这次也有 id,叫 chunk id,但是,它不连续了。只能分析其他特征了。经过观察发现,chunk 区每个 chunk 大小是固定的,都是 16 字节,前 8 字节是 chunk id,没有明显规律,中间 4 字节是 version,后面 4 字节是 lockedto,而大多数 chunk 的 version 和 lockedto 都很小,基本都是小于 10 的值。哈哈,特征又来了!

我又写了一个 c 程序,每 16 字节为一组进行分析,如果这 16 字节的后面 8 个字节表示成两个 int 值都很小,就符合特征。如果连续很长的数据都符合特征,那么就很可能是 metadata.mfs 的 chunk 区。另外,虽然 chunk id 不严格连续,但是也能找到规律,那就是一段范围内 chunk id 的高位总是相同的,所以我只需要手动分析高位就能尝试连接了。

经过很久的分析和处理,进展越来越多,最后,当我恢复到 200M 左右的时候,程序出错了。到出错的现场查看发现,在一个 block 中间,某个 chunk 之后,数据全是 0 了。而 chunk 区的结束标志就是 0!

这,到底是完成了?!还是出错了?数据被覆盖了?我努力让自己冷静,如果是数据被覆盖的话,那应该整个 block 都被用了,而这是在 block 中间,所以被覆盖的可能性应该不大。当然,如果写一个文件,fseek 到某个位置之后才开始写的话,还是会造成这种中间开始覆盖的。但是我觉得这种可能性不大。另外,根据 chunk id 来看,和现有 chunk 应该数量差不多。所以,我觉得应该是恢复完成的可能性很大!

于是我把文件结束在了 216227836 byte 的地方,在把文件重新跑了一次,果然解析成功了!

再把仍然保存的 moosefs 最新 changelog 拿出来,用 mfsmetarestore 去生成最新的 metadata.mfs 文件,也执行成功了!这说明我恢复出来的文件就是正确的!否则的话,mfsmetarestore 在 restore 的过程中应该会遇到 chunk id 找不到的情况。

真的成功了!!启动 moosefs,一切都显示正常,mfsmount 了之后,文件都能正常访问!! 在分析了将近 20 个小时之后,终于成功了!!

最终恢复出的文件有 200 多 M,由 13 个片段组成:

| offset (4k size) | size |
| ---------------- | ---- |
|     12314624     | 16M  |
|     12322816     | 40M  |
|     12341248     | 8M   |
|     12345344     | 16M  |
|     12365824     | 32M  |
|     12388352     | 16M  |
|     12398592     | 8M   |
|     12404736     | 24M  |
|     12421120     | 8M   |
|     12425216     | 8M   |
|     12431360     | 16M  |
|     12460032     | 8M   |
|     12478464     | 6M+  |

可见文件数据块分配的时候,offset 是逐渐递增的,并且每个块都在 8M 以上。这些特性也给数据恢复带来了帮助。否则要是文件片段有上百个,每个大小几十 K 的话,那至少要恢复几天才有可能。

后记和总结

回想整个恢复的过程,由于 inode 信息都丢失了,所以能做的也就是全盘搜索数据块找特征,然后尝试拼接。所幸 metadata.mfs 文件特征明显,当然最重要的还是各种 id 都比较连续,使得找出数据块之后拼接成为可能。另外一个很重要的原因就是运气,200 多 M 的数据在磁盘上一个字节都没有丢失。不然中间只要丢一个 block,整个恢复都会失败。

所以现在看来,听起来好高深的数据恢复,其实就是 写程序找特征 + 尝试拼接 + 撞大运。

关于 moosefs

在长达 20 个小时的数据恢复过程中,我做的最多的一件事情就是骂 moosefs 的作者。因为真的不能忍受他写的代码了。没有文档就算了,居然上千行的程序一点注释都没有!

然后程序里面有各种 if define 条件编译,在 vim 里面还没法匹配 ifdef 和 endif,让我看代码的时候根本不知道哪块代码是运行的代码。这给写解析程序也带来了巨大的麻烦。真不知道作者是怎么理清这么复杂的逻辑的。

另外一个不得不说的地方就是,前面提过的,写文件和关闭文件居然不判断返回值,少有的判断返回值的地方还没有错误处理,直接打印一句 log 草草了事,后面仍然删文件。

当然,数据丢失也是我们自己的过错,对 moosefs 没有足够的了解和重视,以至于忘了备份如此重要的文件。

不管怎样,经历了这一次波折之后,还是学到了很多东西的,也有了一点点数据恢复的经验。生活就是如此跌宕起伏才有意义,C’est La Vie!

Comments