2015年8月16日日曜日

SD Cardが壊れた話

HP Stream 11ネタ。話題が増えてきたのでラベルHP Stream 11を追加してみた。

SD Card故障

以前セットアップしてSD Card上で動かしていたUbuntu 14.04.3なんだけど、VirtualBoxの中でWindows 7にアップデートをガシガシあててたら、なんかディスク不調に。しばらくするとrootfsがread onlyで再マウントされてしまう。当然VM内のWindows 7はディスクに書けずにアップデート異常終了。しばらく原因を調べていると、どうもSD Cardの書き込みができなくなった模様。うっかりLockの爪が……みたいな話なら良かったんだけど、その場合には書き込みはエラーするはずで、今回は成功するものの書けてない。以下、この状況で発生したオモシロ現象。当たり前なんだけど、妙に感心した。

  • SD Card上のswapにデータが入ると、予想不能の発狂モードに突入。色々なものが次々にSEGV。
  • 書き込み量が増えてくると、どこかのタイミングでジャーナリングと実データの矛盾に気づいてEXT4が悲鳴を上げる。こうなるとrootfsはerrors=remount-roでマウントされてるのでread onlyモードに落ちてしまう。
  • read onlyモードではfsckが走るので、簡単な論理故障ならfsckかけてrebootで生き返る。けど、今回は修復が走った後にジャーナリングに異常が残ってcleanにできなかった的なエラーが出ました。
  • ファイルシステム故障かと思い色々手当するも、再起動後には必ず同じ場所まで巻き戻って、同じように自動でfsckがかかり、同じinodeが修復され、同じような故障状態でbootする。
で、本当にSD Cardの故障か調べるためにやった事、いくつか。

まずは該当ディスクに対してbadblocksを-n付きで走らせてみた。見事、全ブロックが書き込み失敗。128GBのディスクだったので完走するには一晩かかりました。まぁ、走らせて寝て起きて確認しただけ。

次にやったのはファイルシステムの確認。読み出しは問題ないので別のLinux上でdd使ってSD Cardのイメージを丸ごとファイルに書き出し。これも時間かかるので注意。dd if=/dev/sda of=sda.imgみたいな感じで。細かいオプションはお気に召すままに。で、ディスクイメージはそのままマウントできないので、以前のストレージ復旧でお世話になったkpartxを使ってパーティションを/dev/mapperに配置。
% kpartx -a sda.img
ってやれば/dev/loopNにsda.imgをマウントして、中のパーティションが/dev/mappers/loopNpMって形で見えるようになる。使い終わったら-d付きでイメージファイルを渡せば開放される。今回は/dev/mappers/loop0p1がswap、/dev/mappers/loop0p2がext4のrootfs。続けてファイルシステム修復。
% fsck.ext4 -f /dev/mappers/loop0p2
で確認したところ、実デバイスで走っていた時と同じような修復が走った。けど、最終的にcleanになった点が故障デバイス上とは違った。実際、二度目を走らせたらエラーは検出されず。

復旧

この時点で一番簡単な復旧は同じメディアを用意してddでコピーする事。新しいメディアに差し替えて起動すれば今までどおりEFI経由で起動するはず。ただ128GBのメディアも安くはないので手元にあった32GBに移植する事にした。128GBでも中身はパンパンだったんだけど、KVM用のイメージだったりChromiumとHaikuのソースコードだったりが大半。こいつらを退避したら32GB以下に収まった。いつも思うけどChromeでかすぎ。で、サイズの違うメディアへのイメージ移植は多少の手間が必要なので忘れないように簡単なメモ。

  1. 小さくしたいファイルシステム内で、目標のサイズに収まるようにファイルを削除していく。パツンパツンでも運用できなくなるので、余裕をもって減らす。
  2. resize2fsを使ってファイルシステムのサイズを小さくする。
  3. パーティションテーブルを書き換えて、該当ファイルシステムのパーティションサイズをファイルシステムに一致させる(か、多少大きくても良い)。
  4. 別メディアを用意して適切なコピー先パーティションを用意する。
  5. ddで縮小した旧パーティションを別メディアの新パーティションにコピー。
  6. 新パーティション上でresize2fsを使って、確保したパーティションサイズ目一杯までファイルシステムを拡張(縮小時にピッタリ同じサイズに収めていれば、この手順は不要)。
各手順を少し補足していく。まずファイルの削減。ディスクのサイズってみんながみんなバラバラな数え方するので、とりあえずざっくり28GBを目標に削減してみた。ファイル削除自体は/dev/mappers/loop0p2をどこかにマウントして作業すれば簡単。適宜dfでサイズを確認。
% fsck.ext4 -f /dev/mappers/loop0p2
% resize2fs /dev/mappers/loop0p2 28GB
メッセージはメモとってないけど、ここで実際に何ブロックまで小さくなったか、とブロックサイズ(4KB)が表示されるはず。ブロック数×4096が実パーティションサイズになるので、次のパーティションサイズ変更の参考に。
% sudo parted /dev/loop0
(parted) unit b           # 計算のためバイト単位の表示に切り替え
(parted) p                # パーティション情報の表示
(parted) resizepart 2 XXX # 2はパーティション、XXXは下で説明
(parted) q                # 終了
pコマンドでパーティション一覧が表示される。今回は2つ目のパーティションが対象。Start、End、Sizeがバイト単位で表示されるので、このSizeがresize2fs時のブロック数×4096になるように調整すれば良い。具体的にはresizepartのXXXをStart+ブロック数×4096とする。第一引数はパーティション番号なので、適宜正しい値を使うのを忘れずに。

新しいディスクもEFIで運用するためにGPTで用意する必要がある。
% sudo parted -a optimal /dev/sdb
(parted) mklabel gpt
(parted) mkpart primary ext4 0 100%
(parted) q
optimal付けとくと、ブロックのアライメントをメディアの物理単位に合わせてくれる。性能に影響するし、手動で揃えるのは面倒なので指定しておくのが無難。これで手順4までが完了。ddはif=/dev/mappers/loop0p2 of=/dev/sdb1以下お好み。最後にresize2fs /dev/sdb1を走らせれば確保した新パーティションのサイズ目一杯までファイルシステムを拡張してくれる。あと、古いメディアではswap領域がloop0p1相当の場所にあってfstabにも相当のエントリが存在していたので、複製した/dev/sdb1をマウントしてfstabから不要になったswapエントリを削除した。

EFIで運用していれば、ddでパーティションを複製した際にUUIDもそのまま複製されるので、ブートストラップ周りのディスク名の修正対応に追われないので楽ちん。blkidコマンドはどこかにキャッシュがあるのか、ddによる複製後も古いUUIDを返し続けるが、実際には正しく書き換わっているので混乱しない事。自分はこれに気づかずgrub.confのrootfs指定のUUIDを書き換えてしまい起動時にrootfsが見つからずにinitramfsの中で起動停止。initramfs上でgrub.confを書き換える苦行をするはめに。この中ってviどころかedすら使えないのね。適当な場所にrootfsを手動マウントして/mnt/bin/ed使うくらいしか思いつかなかった。/mnt/usr/bin/vim.basicとかは共有ライブラリ周りをうまく手当してやる必要があるので少し面倒なはず。自分はed使ったほうが早いのでviはさっさと諦めた。

おまけ:吸いだしたイメージをKVMで起動

復旧には直接必要なかったんだけど、吸いだしたディスクイメージが起動するかKVM上で試してみたのでメモ。こちらはEFI対応とかブートストラップの手当とか、わりと面倒だった。記憶で書いてるので不正確な部分もあるかも。

まずはEFI対応の方法。参考にしたのはtire.retireさんのKVMでUEFI環境のゲストを作る。CentOS向けの説明なのでOVMF.fdの置き場所が違うんだけど、他はそのまま。UbuntuではOVMF.fdの置き場所は確か/usr/share/qemuで通ったはず。

次に起動向けのEFI環境の準備。HP Stream 11ではSD CardはEFIから見えないため、例のSD Card環境ではEFIシステムパーティション、/bootは内蔵ディスクにありました。この辺を内蔵ディスクに頼らないように手当してやる必要がある。まずEFIシステムパーティションは新規ディスクイメージを作成してマウントさせる事で対処。
% head -c `expr 8 \* 1024 \* 1024` /dev/zero > efi.img
% parted efi.img
(parted) mklabel gpt
(parted) mkpart primary fat32
(parted) q
% sudo kpart -a efi.img
% sudo mkfs.vfat /dev/mappers/loop1p1
% sudo mount /dev/mappers/loop1p1 target
% sudo mount /dev/mmcblk0p1 original
% sudo mkdir target/EFI
% sudo cp -rfp original/EFI/ubuntu target/EFI
まず、grubの設定を正しく読み込むために、target/EFI/ubuntu/grub.cfgの最初の行、search.fs_uuidに書かれているUUIDを書き換える必要がある。これはblkid /dev/mappers/loop0p2(吸いだしたイメージのrootfsパーティション)の値を使えば良い。

で、次は/bootの対処。/dev/mappers/loop0p2の/bootは空になっているので、内蔵ディスクの該当パーティションからコピー。
% sudo mount /dev/mmcblk0p5 boot
% sudo cp -rfp boot/* target/boot/
target/boot/grub.cfgには最低限の修正を適用。Ubuntuの起動エントリでlinuxとinitrdの項目が/vmlinuz-...と/initrd.imd-...というルート直下のパスになっており、linuxにはroot=UUID=でmmcblk0p5を指定している箇所があるはず。このroot=UUID=を初段で指定したUUIDと同じ/dev/mappers/loop0p2のIDに書き換え、/vmlinuz-...、/initrd-...を/boot/vmlinuz-...等に書き換えれば準備完了。

KVMから起動するとEFI Shellに落ちる。作成したEFIのディスクがFS0として見えたので、
Shell> fs0:
fs0:\> cd EFI\ubuntu
fs0:\EFI\ubuntu\> shimx64.efi
でgrubのメニューが出るはず。まぁ、何か間違っててメニュー読み込み失敗したらgrubの対話モードからconfig file (hd0,gpt2)/boot/grub/grub.cnf とかで読めるはずなので、tabキーと友達になりながらgrub.cnfを探して読んでみてください。僕はこの段階でbootが空になってる事に気づいて、bootディレクトリのコピーとかUUIDの書き換えが必要なのを思い出しました。一度起動しちゃえばupdate-grub、grub-installを走らせれば、この辺の修正は綺麗になるので。まぁ、適当に。

0 件のコメント: