HDDの物理障害から立ち直る

HDDがある日突然ファイルが読めなくなった。言われてみれば予兆はあった。Picasaでファイルを開くと奇妙に遅いとかである。しかし、USB接続していたのでS.M.A.R.T.も読んでなかったし、音もちゃんと聞いていなかった。なによりバックアップを取っていなかった。そういう状況で、突然ファイルは見えなくなった。なので、とにかくぶっ壊れたディスクから中身を吸いださねばならない。
以下の内容は作者の個人的体験を記述したものです。またコマンドはあなたのPCのデータを破壊する可能性があります。コマンド例を信用せずよく自身で調べ自身の責任のもとで実行してください。

修復業者

HDDの修復業者はあっちこっちで広告を出しているものの、結局のところオントラックというところに頼むのがいいという事は分かった。ただ、オントラックはハイクオリティ・ハイペイメントである。個人相手には「ディスカウント」しているが、それでも20万円という額が必要になる。ここでこれに飛びつく気にはなれなかった。

無料で修復 - Windows

じゃあ、という事で当然思いつくのがddなんだけど、まずはWindowsNTFSを話してサルベージすることにした。Windows上でアッチコッチのフォルダに行って

xcopy /s/e/c/f/h/j SRC DST

とやって'n'を連打した。後のほうではnnnnnnnnのようにnを続けて入力した物を改行の後に続けてコピペすることで標準入力に流せることが分かり少しは作業がはかどった。これで大半のデータをコピーすることには成功した。しかしなにぶん完全に取り切った感じがしない。

無料で修復 - dd

次に考えるのは当然ddである。ddとはUnixに存在する「ブロックデバイスをとにかく前からコピーする」ツールである。

dd if=/dev/sdX of=/path/to/image bs=4096 conv=noerror,sync count=XXXX

こういう時は一回の読み書きサイズをHDD自体のセクタサイズの512バイトに合わせてbs=512にするのが定石である。さもないと、例えばbs=4096とした時に4096バイト中の512バイトが1箇所エラーになっただけで周りのセクタもエラー扱いにされてしまうからだ。しかし、これを実行しても全く終わる気配がない。最初100GB少しまでは順調に進んだが、途中から0.5kB/sというHDDが動作しているかすら疑わしい速度まで落ちてしまった。この原因を考えるに、ddは前から順番にreadシステムコールを発行してファイルを読もうとする。つまり、badな領域を読む際に大きな時間がかかると、次のデータ領域を先読みすることもなくただただ時間がかかることになってしまう。こういうオーバーヘッドをシーケンシャルに蒙るのはバカのすることで、本当は並列にI/Oを発行して同時に複数のreadを待てば相当な高速化が図れるはずである。

無料で修復 - GNU ddrescue ←イマココ!

そこで頭のいいツールを(今更ながら)ググった結果あっけなく見つかった。Ddrescue - GNU Project - Free Software Foundation (FSF)である。GNU ddrescueであって、dd_rescueとは別物なのには気をつけたい。どれくらい頭のいいツールかを見てみよう。

GNU ddrescue はまずデータの良好な部分を先に復旧し、不良だったり読み込みが遅かったりする領域を後回しにして、復旧を効率よく進行させます。こうすると故障したドライブから最終的に復旧できるデータの量を最大化することができます。
標準の dd ユーティリティも故障したドライブからデータを保存するのに使うことは可能ですが、データを前から順番に読んでいくのでエラー領域がドライブの前の方にあった場合データを救出する先にドライブを駄目にしてしまいます。
他にエラーが見つかったときは小さめのサイズで読み込み直すようなプログラムもありますが、結局前からデータを読むことには変わりありません。前からエラー領域を延々と読むことはただ時間がもったいないだけでなく、データの読み込みをする前にディスクの表面やヘッドなどドライブの機構にダメージを与えてしまう事になります。結局、残っている良好なデータを救出できる可能性を自分でなくしてしまうわけです。
ddrescue のアルゴリズムは以下のとおりです。ちなみに、ユーザーは処理をいつでも中断することができますが、不良なドライブを読み込んでいるときはカーネルが読み込みを断念するまで長時間ブロックすることがあります。

  1. オプションでログファイルから以前の実行状態を読み込みます。ログファイルがなかったり空だったりする場合は全域が non-tried だとして扱います。
  2. 入力ファイルのうち全ての non-tried の部分を読み込み、そのうち失敗したブロックをnon-trimmedとしてマークして読み飛ばします。non-tried の部分だけが大きなブロックサイズで読まれます。

Rescue domain
Block or set of blocks to be acted upon (rescued, listed, etc). You can define it with the options `--input-position', `--max-size' and `--domain-logfile'. The rescue domain defaults to the whole input file or logfile. その他の trimming, splitting や retrying はセクタ単位で行います。各セクタを最大2回は読みこもうとします。最初は大きなブロックサイズで読み込もうとし、次は1セクタごとに読み込もうとします。

  1. non-trimmed ブロックを後ろから1セクタ毎に不良セクタが見つかるまで読み込みます。途中で不良セクタが見つかれば bad-sector とマークし、残りのブロックを non-split とマークします。
  2. non-split ブロックを1セクタ毎に前から読み込み、不良セクタがあれば bad-sector とマークします。ブロック内にある程度多くの不良セクタが見つかれば、そのブロックを半々に分けて後半のブロックを読み込みます。再帰的にこれを続ければログファイルが長くなりすぎることなく大きな non-split ブロックを分割していくことができます。
  3. オプションで不良セクタを指定されたリトライ数に達するまで読み込み直します。
  4. オプションで今後の実行のためにログファイルを書き出します。
GNU ddrescue Manual

よく読むと決して並列I/Oするとは言っていなかった。とはいえとっとと読めるところを読むというのはうれしい限りだ。ではUbuntuでインストールしよう。

$ sudo apt-get install gddrescue

実に簡単である。しかし問題があって、これまでddで一ヶ月近くコピーしてきた部分が既に存在することである。ただ、幸いGNU ddrescueは「今どこまでなにをしたか」と言う状態を簡単なテキストファイルとして保存するようだ。そのため、適当に実行してみると簡単に「既にNバイトコピーし終わった」状態を再現できた。

$ sudo ddrescue /dev/sdX /tmp/hoge rescue.log
...# C-cで止める
$ cat rescue.log
# Rescue Logfile. Created by GNU ddrescue version 1.16
# Command line: ddrescue /dev/sdb /tmp/hoo.img rescue.log
# current_pos  current_status
0x02490000     ?
#      pos        size  status
0x00000000  0x02490000  +
0x02490000  0x76DF0000  ?

どうやら0x02490000までがコピー済み、0x76DF0000が総サイズ残りのサイズである。詳しくはGNU ddrescue Manualを参考にしてほしい。ちなみに、statusの意味は以下の通り。

文字 意味
'?' non-tried ブロック
'*' non-trimmed ブロック
'/' non-split ブロック
'-' bad-sector ブロック
'+' コピー済みのブロック
GNU ddrescue Manual

いよいよ後はこのrescue.logのサイズを修正して走らせるだけである。

$ vim rescue.log # 編集
$ sudo ddrescue /dev/sdX /path/to/image rescue.log

ちなみにGNU ddrescueはCtrl-cで中断し、また後で同じログファイルを指定して実行することでresumeすることが出来る。つくづくありがたい。

実行中に分かったこと

  • ddrescueの実行フェーズのうち、tryとtrimは1セクタでもポシャったらやめてしまうので一番時間がかかるのはsplitということになる。で、そのsplitなんだけどなるべくサイズの大きなブロックからやり直してほしいものである。例えば100KBの領域とかだとこのボロボロのディスクだとまるっと読み込めないこともあり得るのに対して、サイズの大きなブロックは端っこだけ駄目なところがあったという可能性も大いにある。そこで、一度trimフェーズまで終了した時点でCtrl-cで終了し、「これはやり直してほしい」と思うブロックのstatusを'/'から'?'に書き直してしまう。これでもう一度実行すればそのブロックを先にコピーしてくれるというわけだ。
  • -a/--min-read-rateというのを指定すると最低これだけの速度が出ていないと後回しになるというオプションなのだけど、Ubuntu 12.04にはバージョン1.14が入っていてこれにはこのオプションがなかった。Ubuntu 12.10ならあるみたい。rescueに1年とかかかるわけにも行かないから本当のところこれを付けたいんだけど。

即興でThis script visualize the logfile of GNU ddrescue. · GitHubというddrescueのログファイルから進行状況のグラフを書くスクリプトを組んだ。だが、ラスタ画像なので最小でも1pxの幅を持たせないと読めないわけで、実質的にほとんど大きさに意味がないというか…大きいブロックは確実にでかいブロックだけど、小さいブロックはある程度以下は全部一緒くたっていう。まあ、512Bを500GBの中で考えるのが無茶というもの。

そしてエントリを書いてから一週間、コピーは遅々として完了しません。途中で次のようなものができました。

  • https://gist.github.com/4645399はnonzeroなセクタだけddするプログラムです。何がしたかったかというとマウントし忘れた状態でddrescueしてしまったので、元のディスクイメージと新しいディスクイメージをマージしたかった感じです。
  • https://gist.github.com/4652937は全く同じ内容の画像をハードリンクで処理するものです。今画像のフォーマットが違う時にヤバいんじゃとか思いつきましたが面倒なので放置します。

まだコピーは終わっていないが幾つかの知見を得た。

  • -dオプションはdirect I/Oなので付けるといいとか。本当かはしりません。
  • -r 3で3回リトライとか出来る。そしてリトライすると思ったより読める読める!最後の切り札である。