優雅な生活の設計と実装

The Design and Implementation of the Gracious Days


LAST MODIFIED: 2011/09/28 15:20:49 UTC

新しい秩序の確立は、他の何にも増して難しく、
成功する可能性が低く、危険な事業である。
改革者は旧秩序から利益を得ている
全ての者を敵にまわし、
新秩序から利益を受けるはずの者からは
及び腰の支持しか集められない。
--- Niccolo Machiavelli, The Prince

この種の「保護」は初心者を保護するかも知れないが、
熟練ユーザを窮地に追い込むことになる。
というのは、何が親切であり、何が適切でないかかという
オペレーティングシステムの考え方の裏をかくことばかりに
かなりの労力を費やさなければならないからである。
--- A.S.Tannenbaum, Modern Operating Systems


不定期更新の日記です。ディスクスペースの関係上、 あまりに古くなったものは順次消していきます。 この日記の更新は、今野さんの *BSD Diary Links から取得することが可能です。

September 2011

感想はこちらまで (内容は匿名のメールで送られます)

コメント:

注: お返事が必要な場合は直接メールください。 ただし、確実にお返事するかどうかはわかりませんのであしからず。

Wednesday, September 28

* ZFS のトラブル続き(3)

壊れた zpool のデータを個別に精査。 今までごまかしごまかし使っていたけれど、 メタデータにあり得ない値が入っているものがいくつか見つかったので、 やっぱり zpool を廃棄してつくりなおすことに決定。

同じマシンに、HDD を 3 台ほど追加。 同構成・同容量の zpool をつくることは、 接続できる HDD の数の関係で難しいので、 追加した HDD を冗長性なしの JBOD 構成にして新しい zpool を作成。 現在のデータ量は何とか入る大きさ。

元の zpool で snapshot をつくったり消したりするのはちょっと怖いので、 zfs send + recv は使わず、rsync で同期。 読みこみ時に panic したり、データが壊れている場所が分からないと後々めんどうなので、 データがおかしければログに残して、不整合を適当に補正するコードを追加。 ちょっと乱暴な方法ではあるけれど、大部分のデータは上流サイトからのミラーなので、 壊れているものはあとで選択的に同期すれば十分だと判断。

というわけで、allbsd.org の FTP ミラーは、データの退避が終るまで一時的に止めています。 CVS repository 等、それ以外のコンテンツはすでに退避が終っているので、 アクセスできるように戻しました。古い zpool を破棄して、 今度は逆に書き戻す時にダウンタイムが発生しますが、 その時の切り替えは短い時間で終ると思います。

* ZFS のトラブル続き(4)

というわけで、データの破損が zpool scrub で検出されない場合、 その zpool を使い続けるのは危険だよ、という当たり前の結論に到着。 そのデータ破壊の間接的な原因はカーネルのバグだったわけなのですが、 地道な検証とレポートの繰り返しで、だいぶバグを潰すことができました。

まだ lock order reversal がもう一個残っている感じがあるので調査中ですが、 修正後のシステムでは、新しくつくった v28 の zpool が(今のところ)元気に動いています。

RELENG_8 で v28 を使うひとは、次のパッチがあたっているか確認しましょう。 基本的には全部 commit されてますので、最新のソースを使っていれば大丈夫です。 特に r224608 があたっていない状態で使うと、かなり危険です。 ご注意。

* Errata

offset の説明で、論理セクタのオフセット値 (ATA8-ACS の IDENTIFY DEVICE で返ってくる word 209 の 15:14 が 01b の時の、13:0 部分の数値) がある場合、「加算しましょう」と書いていたのですが、減算の間違いでした。 (ご指摘くださった方、どうもありがとうございました)

512e の時、オフセットが 0 なら LBA=7 へのアクセスは PBA=0 へのアクセスとなるのですが、 この値が 1 だと、LBA=7 へのアクセスは +1 ずれて PBA=1 へのアクセスになります。 なので、8n - 1 にアラインメントを合わせる必要がある、というのが正しいです。

Friday, September 23

* ZFS のトラブル続き

8 月下旬から allbsd.org のメインファイルサーバの調子が悪く、 落ちることがしばしば。

原因は ZFS に絡んだトラブル。RELENG_8 で pool を v28 にしてからというもの、バグを踏みまくっています。 だいぶバグは潰しましたが、 8-STABLE で ZFS を使うのは、 もうちょっと待ったほうが良いかも知れません。 8.2-RELEASE は v15 ですが、そっちは安定してます。 8.2R を使っている人はご安心ください。

単にカーネルが panic するところから、pool を認識しなくなるレベルまで、いろいろな症状と戦った結果、 経験値がたまりました...

冗長性のある zpool を構成している物理デバイスが故障した場合の交換や対処方法は、 多くの文献に紹介されていますが、 zpool や zfs データセットが壊れてしまった場合の対処は、なかなか難しいです。 あんまりまとまっていませんが、ZFS の障害経験例はなかなか知る機会が少ないものでもあるので、 つらつらっと書いてみました。

基本的な切り分けは「zpool レベルの障害」と「zfs レベルの障害」からスタートします。 の記事、かなりでたらめなので信じないほうが良いと思います。 「ZFS は障害に強い」なんて事実はありません。

zpool の障害対応フロー

zpool は、論理ボリュームマネージャとしての機能を提供するものです。 そこで発生する可能性がある障害は、1) 物理デバイスとの対応関係情報が壊れる、 2) ブロックレベルのデータ完全性が失われる、の 2 点です。

1) が起こると、まず zpool が認識されなくなります。 対応関係は、物理デバイスそのものに記録されているですが、 それを起動時に毎回全部探すと時間がかかってしまうため、 /boot/zfs/zpool.cache にキャッシュデータが保存されています。 たとえばこのファイルが壊れると、存在するはずの zpool が見えなくなって、 かなり焦ることになるわけです。

zpool import を実行すると、 物理デバイスを再スキャンして対応関係の情報を取り出してくれます。 zpool が認識されなくなったら、まずは import を試してみましょう。 物理デバイスのスキャンは、/dev/dsk の下をデフォルトで見るようになっています。 FreeBSD の場合は /dev の直下にあるので、 -d /dev オプションをつけないと見つからないかも知れません。

ちゃんと見つかれば、その際に壊れたキャッシュデータを再構築してくれるので、 何もしなくとも問題は解決します。ここでひとつ、 「ZFS は、データ構造に矛盾があって、それが修復可能な時、勝手に修復する」 という原則を覚えましょう。 修復だけを明示的に行なうオペレーションは、基本的にありません。 障害の切り分けをしている時にも、アクセスする度に勝手に修復されていくので、 何が原因で何が修復のトリガになったかが分かりにくく、 個人的にはうっとうしいと感じることもあるのですが、 この原則を知っておくことは大切です。

さて、zpool import を実行した時に、正常に終了しない状況はあり得ます。 ひとつはデータが完全に壊れていて認識できないケース。 これはもう、どうしようもないのでバックアップから書き戻しましょう。

もうひとつは、zpool import を実行した時に panic するような状況です。 これは対応関係のデータに致命的な破壊が発生しています。 先ほどの原則を考えると、破壊をなおしてくれても良さそうなのですが、 あの原則は、ZFS の操作で panic するような場合、 それは修復不可能な破壊か、カーネルのバグという意味でもあります。 なので、なおすことはできないと考えるのが正しい認識です。

panic 等で操作できなくなってしまった時には

とは言うものの、大量のデータが入った zpool を管理していて、 バックアップも真面目にとってない場合だったら、 「とにかくマウントできる状態に戻したい」と考えたくなりますよね。

そこでとれる選択肢に、「panic を無効にする」という操作があります。 これは、矛盾するデータを無理矢理整合性のあるデータに置き換えて、 処理を先に進める、ということです。

具体的には、sysctl 変数 vfs.zfs.recover を 1 に設定することで、 致命的な panic 発生時に、panic の発生を抑制することができます。 Solaris で /etc/system に zfs:zfs_recover=1 を設定するのと同じです。

もしその panic が、intent log に起因するものである場合は、 ログを無視することで復旧できる場合があります。これもデータは失われてしまうのですが、 比較的被害は少なくて済みます。具体的には、 vfs.zfs.zil_replay_disable=1 を設定しましょう。 8.2R とそれ以前では、vfs.zfs.zil_disable=1 という設定になります。 両者はちょっと意味が違うのですが、ログを無視させる効果は同じです。 panic していた点を通過できたら、0 に戻して再起動しましょう。

致命的でない assert でも、panic が発生することがあります (assert とは、カーネルの内部で念のため行なっている、データに異常がないかどうかのチェックのことです)。 Solaris では aok=1 とすることで assert を強制的に無視させることができるのですが、 FreeBSD では無視させる操作がありません。これに引っかかった場合は、 カーネルを書き換えるか、zdb を使わないと回避できません(zdb については後述)。

これらの設定を入れて強制的に進めれば、とりあえず panic で再起動を繰り返すような、ドツボな状態から脱することはできます。 ただし、注意しなければならないのは、 データは修復不可能な破壊が発生している可能性があり、 かつ破壊されたデータは修復できない という点です。OS のバグで panic していることもあるので、 必ずしもデータが壊れているとは言えないのですが、 修復する方法だと考えるのは大きな間違いなので、正しく理解しましょう。

データ完全性のエラー

話はずっとさかのぼって、次に 2) のブロックレベルのデータ完全性が失われた場合の障害を考えてみましょう。 これは ZFS のチェックサム機能がかなり強力にチェックしてくれるので、 問題になることは稀です。冗長性のある zpool なら、勝手に修復されます。

チェックサムの整合性の検証を指示する zpool scrub というコマンドがあります。 これを fsck のように「ファイルシステムの不整合を修復するための操作だ」と勘違いしている説明をたまに見かけるのですが、 これはブロックレベルのチェックサムとデータの整合性を、zpool 全体に対して調べなおすだけの操作です。 ファイルシステムレベルの不整合はチェックしてくれません。

また、ブロックレベルの完全性チェックは、通常のアクセスでも常に行なわれるものなので、 エラーがあれば、scrub でなくとも、アクセスしたタイミングで勝手に修復されます。 ただもちろん、長時間アクセスしないデータがある場合、エラーがあるかどうかが報告されないので、 それを調べたい時には、scrub の実行が必要です。つまりこれは、「エラーがあるかどうか」 をチェックする目的では使えるのですが、エラーの修復に使えるわけではないのです。 実際、ブロックレベルで修復できないエラーが発生する可能性はあります。 もしそれが報告されたら、バックアップから書き戻す以外、修復の方法はありません。

ブロックレベルの完全性が失われるというのは、HDD への書き込み操作に問題がある、 HDD のデータが何らかの原因で化けてしまった等、 ハードウェアの信頼性に起因するものが多いです。冗長性を確保していれば、 修復できない状態に陥ることはほとんどありません。

zdb プログラム

上記の障害対応フローでは名前だけ書いたのですが、 ZFS には zpool, zfs のコマンドの他に、zdb というツールがあります。 このツールは、「ZFS のデータをユーザランドから読み書きするためのプログラム」です。 カーネルのソースファイルの一部をプログラムの中に取り込んでいるので、 カーネルが行なう操作と同じ内容を、ユーザランドから行なえます。

zdb は読み書きのツールだと書きましたが、 基本的に ZFS の情報を人間が読みやすいようにダンプする機能しかありません。 データ修復のためのツールではありませんので、誤解しないでください。 もともと開発者がデータ内容を調べるためにつくったツールなので、 ドキュメントもほとんどありません。

しかし、このツールはデータを読み出す際に使うソースファイルがカーネルと共通になっているため、 ZFS の原則が適用されます。つまり、不整合があるデータに遭遇すると、 勝手にそれを修復しようとするのです。 これをうまく使うと、深刻な状況になった zpool を、 認識するレベルまで持っていくことができます。 何度も繰り返しますが、データの修復はできません。 あくまでzpool として認識できない状況を解消できるだけなので、 肝に銘じてください。

zpool にアクセスすると panic してしまうとか、zpool import を実行してもちゃんと import してくれないような場合、 zdb でアクセスすると、同じようにエラーが出ます。カーネルと同じソースを使うので、まあ当たり前の話ですね。

カーネルの場合、前述した zfs_recover=1 を設定すれば、panic を回避できましたが、 zdb では -AA を指定すると panic が回避できます。 -A を指定すると、assert の不整合を回避します。 つまり、-AAA を指定すれば、panic と assert の両方を回避して zpool へアクセスすることができ、 そのアクセスの発生によって、発見された不整合は強制的に解消されます。

具体的には、次のように使います。zpool の名前が tank であれば、

# zdb -d tank

とすれば、ZFS データセットの一覧が表示されます。もし、tank が認識されていなければ、

# zdb -e -p /dev -d tank

のように、-e と -p /dev をつけてください。-e は認識されていない zpool を探すオプション、 -p /dev は探す場所を指定するオプションです。 -p /dev を付けなくても見つけられる場合がありますが、念のため付けておくことをおすすめします。

import できない要因はいろいろと考えられるのですが、 データセットの一覧を表示する zdb のコマンドがエラーなく終るのであれば、 import できる確率は高いです。もしエラーが出るようなら、 次のように -AAA をつけて回避させてみましょう。 2 回目からは出なくなっているはずです。

# zdb -e -p /dev -AAA -d tank

intent log が壊れていると、import した時に panic が発生したり、import できなくなることがあります。 zdb は、トランザクションログのエントリの一部を破棄させる操作があるので、 もし上記を操作してもダメなようなら、-F オプションを付けてみましょう。

# zdb -e -p /dev -F -d tank

また、-c オプションを使うと、ブロックレベルのチェックサムを調査することもできます。 内容的には scrub と同じです。scrub の実行中に panic することはほとんどないと思いますが、 zdb での操作は、カーネルで操作するよりも安全です。 -c 1個でメタデータのブロック、-cc のように 2個重ねるとすべてのブロックをチェックします。 もちろん、問題があればアクセス時に修復されます。

# zdb -e -p /dev -cc tank

繰り返しになりますが、zdb で可能な操作はカーネルと同じです。 ですので、カーネルで失敗する操作は、zdb でも同じように失敗します。 FreeBSD の場合、assert を回避する手段 (aok=1) がカーネルに入っていないので、 その場合は zdb の -A を使うのが一番簡単な方法です。その他、import できないなど、 カーネルに認識させることができなくなってしまった時には、zdb でアクセスしてみるのがひとつの手です。 内部構造を詳しく知らないと、表示される内容はちんぷんかんぷんだと思いますが、 アクセスすることに意味があります。

zdb でアクセスした場合、何が修復されるのか/修復されたのかは、把握するのが困難です。 たとえば、物理デバイスのパス名情報が壊れてしまった場合、 「zdb -e -d tank」と実行しても tank が見つからないことがあります。 明示的に検索パスを指定して、 「zdb -e -p /dev -d tank」とすると見つかりますが、 見つかった瞬間、tank の物理デバイスに記録されているパス名情報が修正されるため、その次からは 「zdb -e -d tank」のコマンドで tank が見つかるようになります。 内部構造を知っていれば、「ああ、情報が修正されたんだな」と理解できるのですが、 知らないと「さっきはエラーだったのに、なんで二回目はエラーなく動くのだろう」と混乱してしまうでしょう。

このように、zdb は使用者が意図しなくても、アクセスしたデータを書き換えてしまうツールです。 zdb がデータを壊してしまうようなアクセスを生むことは、ほとんどないのですが、 大事なデータが入っていて失敗が許されないのであれば、 まずは物理デバイスのデータのコピーをつくってから挑戦するなどの対策をとりましょう。

いずれにしても、このツールに頼らなければならない状況に陥っているのであれば、 もうすでにデータの完全修復はできない状況です。 問題が修正できるなんていうのは大嘘で、 カーネルからアクセスできなくなった zpool を、アクセスできるレベルに持っていくことができるかも知れない、 程度に理解してください。当該記事は「metaslab」のことを「メタラベル」と書いていたりするので、 おそらく zdb が何をしているのか、何が表示されているのかを理解しないで書かれているのだと思います。

カーネルが認識できる状態に持っていくことができたら、zdb に頼る必要はありません。

zfs の障害対応フロー

ZFS は、ファイルシステムレベルの不整合に対してほとんど何もできません。 そもそも、ブロックレベルでの完全性がチェックサムによって保証されていることに強く頼っているため、 ファイルシステムレベルでの不整合の発生に対する対策は、入っていないのです。 もっとも、この種の不整合は技術的に検出することが難しいので、 ZFS に限らず他のファイルシステムでも、状況はあまり変わらない話ではあります。

たとえば非常に稀ではあるのですが、消せないファイルや、 中身が壊れたディレクトリが作成されてしまうことがあります。 また、アクセスすると panic するようなファイルができてしまう可能性もゼロではありません。 そういう状況に陥った場合、どうすれば良いのでしょうか。

ブロックレベルの修復不可能なエラーが、zfs レベルのエラーを発生させることはあり得ます。 ZFS 管理ガイドの「データ破壊エラー」の項に書いてある状況が、それです。 しかし、必ずしもブロックレベルのエラーが原因となっているわけではないので、 scrub に頼ることはできません。scrub でまったくエラーが検出されなくても、 中身が壊れたディレクトリは存在できるからです。

何らかの原因でアクセスできないディレクトリエントリは、 どこか別のところに mv して退避させておきましょう。 アクセス権を 0 にしておけば、誤ってアクセスしてしまうこともありません。 削除できれば削除したほうが良いのですが、削除しようとしたら panic するような場合、簡単に修復する方法はありません。

* ZFS のトラブル続き (2)

もうひとつ、ZFS に関して「ZFS は fsck のようなチェックが必要ないので、 起動時に待たされることはない」という話を聞いたことがあります。 これも嘘です。fsck のようなチェックがないのは真実ですが、 起動時に待たされる状況は、ZFS でもちゃんと発生します。

ZFS は、space map や書き込み処理のデータ構造に、ログを使っています。 そのため、意図しない電源断が発生した場合でも、 処理が中途半端な状態になってしまうことはありません。

起動時、ZFS のデータセットをマウントする際には、 このログを調べて、未処理のトランザクションエントリを処理するのですが、 それが膨大な量になっている場合、その時点で待たされます。 ふつうの操作では高々数十秒のオーダで終りますし、 そもそも正常にシャットダウンできていればログはクリアされていることが多く 気づきにくいのですが、次のようなケースを想像してください。

# zfs destroy tank@20110920

このコマンドは、tank@20110920 というスナップショットを削除します。 このスナップショットの生成元である tank というデータセットが、 大量のファイルを抱えていたとしましょう。destroy の処理は、 ログ上の記録は大きくありませんが、 実際の処理に長い時間がかかるものの典型です。

このコマンドを実行した直後に電源を強制的に落すと、 再起動後にマウント直前のところで、処理が止まったように見えます。 Ctrl+T で処理を調べると、次のような状態で待たされているはずです。

load: 0.12  cmd: zfs 98 [tx->tx_sync_done_cv)] 355.56r 0.00u 0.03s 0% 2284k

あるいは、次のような表示になるかも知れません。

load: 0.31  cmd: zpool 336 [zio->io_cv)] 1.20r 0.00u 0.06s 0% 2140k

止まっているように見えるのですが、 これは destroy の処理を延々と続けています。完全に終るまでマウントしてくれません。 ファイルの数にもよりますが、おおきいものでは数時間単位の時間がかかります。

早く起動してほしい、と思っても、現時点でこの問題に対する有効な手段はなく、 ただ待つしかありません。この事例を、もうちょっと詳しく見てみましょう。

メモリが足りない?

障害復旧時に見落としがちなのが、メモリの消費です。 ZFS はデータ構造の多くをメモリ上に載せて処理を進めるため、 従来のファイルシステムと比較してメモリを多く使うのですが、 それとは別に、障害復旧時の処理に必要なマシンリソースを把握しておく必要があります。

話がちょっと脇道にそれますが、FFS の場合を復習しておきましょう。 ご存知のとおり、障害復旧時に使われるツールは fsck_ffs です。 このツール、どれくらいメモリを使うのかあまり知られていないような気がするのですが、 最大で 1GB のディスクあたり 1MB くらいのメモリを使います。 これは、 ディスク全体から必要なデータを集めて、 メモリに載せて処理するつくりになっているからです。

最近の HDD は容量が 2TB とか平気でありますから、2TB をまるまる 1 個のファイルシステムとして作成してしまうと、 fsck の実行には 2GB のメモリが必要、ということになります。 もちろん、1MB/1GB ルールは多少余裕を持って計算した理論最大値なので、 実際にはもっと少ない量でいけることが多いですが、 fsck は単に時間さえかければ良い、という話ではないわけです。

インストール時にパーティションに分けることをせず、 全部 / でつくることを勧めている解説もたまに見かけるのですが、 メモリが少ないシステムで 2TB の単一ファイルシステムをつくると、 障害が発生してから fsck が異常終了することに気づく、 という最悪のパターンになりかねません。知識として知っておくべきことだと思います。

さて、話を戻しましょう。 ZFS の場合も、実は障害から復旧する時に普段よりも多くのメモリを消費します。 どれくらい消費するのかは構成や状況に強く依存するので一概に言えないのですが、 急な電源断等が発生した後に再起動すると、 データセットをマウントしなくても、zpool を認識した時点で、 記録されていた未処理の内容を処理しようとします。 その際、処理に必要なデータの読み込みが発生してメモリを消費するわけです。 実際に、普段のオペレーションではメモリ不足にならないシステムの復旧をしていたのですが、 このメモリ消費の時点で、メモリ不足となるケースがありました。

きれいにシャットダウンしなかった場合の再起動後、 zpool を認識させた場合の処理の流れを、実例で追いかけてみます。 まず、この説明で使う zpool の構成です。

# zpool list
NAME   SIZE  ALLOC   FREE    CAP  DEDUP  HEALTH  ALTROOT
a     18.1T  6.52T  11.6T    35%  1.81x  ONLINE  -

この zpool は、FTP サーバ用のデータを保持する a/ftproot というデータセットがあるのですが、 そのデータセットのスナップショットをつくって、destroy した直後にシステムを強制リセットしました。 容量は次のとおりです。

# zfs list a/ftproot
NAME        USED  AVAIL  REFER  MOUNTPOINT
a/ftproot  3.93T  6.80T  3.93T  /a/ftproot

再起動後、zpool を認識した直後の zfskern カーネルプロセスの様子は、 次のようになっています。

# procstat -w 1 -kk 9
  PID    TID COMM             TDNAME           KSTACK
    9 100064 zfskern          arc_reclaim_thre mi_switch+0x16e sleepq_timedwait+0x42 _cv_timedwait+0x14f arc_reclaim_thread+0x29d fork_exit+0x11d fork_trampoline+0xe 
    9 100065 zfskern          l2arc_feed_threa mi_switch+0x16e sleepq_timedwait+0x42 _cv_timedwait+0x14f l2arc_feed_thread+0x1a8 fork_exit+0x11d fork_trampoline+0xe 
    9 100251 zfskern          txg_thread_enter mi_switch+0x16e sleepq_wait+0x44 _cv_wait+0x13c txg_thread_wait+0x79 txg_quiesce_thread+0xb5 fork_exit+0x11d fork_trampoline+0xe 
    9 100252 zfskern          txg_thread_enter mi_switch+0x16e sleepq_wait+0x44 _cv_wait+0x13c zio_wait+0x61 dsl_scan_sync+0x4d3 spa_sync+0x390 txg_sync_thread+0x139 fork_exit+0x11d fork_trampoline+0xe

4 番目にある 100252 のスレッドが、dsl_scan_sync() というのを呼んでいます。 これは scrub や resilvering と同じようなスキャンの処理です。 zpool iostat では 1MB/s, 500 IOPS くらいの read として見えています。

これが走っている間、wired なメモリの消費量は増え続けます。 時おり書き込みアクセス(destroy に直接関連する処理)が発生し、次のようになります。 同じ 100252 番のスレッドに注目してください。

# procstat -w 1 -kk 9
  PID    TID COMM             TDNAME           KSTACK
    9 100064 zfskern          arc_reclaim_thre mi_switch+0x16e sleepq_timedwait+0x42 _cv_timedwait+0x14f arc_reclaim_thread+0x29d fork_exit+0x11d fork_trampoline+0xe 
    9 100065 zfskern          l2arc_feed_threa mi_switch+0x16e sleepq_timedwait+0x42 _cv_timedwait+0x14f l2arc_feed_thread+0x1a8 fork_exit+0x11d fork_trampoline+0xe 
    9 100251 zfskern          txg_thread_enter mi_switch+0x16e sleepq_wait+0x44 _cv_wait+0x13c txg_thread_wait+0x79 txg_quiesce_thread+0xb5 fork_exit+0x11d fork_trampoline+0xe 
    9 100252 zfskern          txg_thread_enter mi_switch+0x16e sleepq_wait+0x44 _cv_wait+0x13c zio_wait+0x61 dbuf_read+0x5e5 dmu_buf_hold+0xe0 bpobj_iterate_impl+0x23d bpobj_iterate_impl+0x31b dsl_scan_sync+0x4c3 spa_sync+0x390 txg_sync_thread+0x139 fork_exit+0x11d fork_trampoline+0xe

また、ある程度進むと、次に dsl_pool_sync() という処理も現れてきます。 この処理では、読み込みよりも書き込みが頻繁に発生します。

# procstat -w 1 -kk 9
  PID    TID COMM             TDNAME           KSTACK
   9 100064 zfskern          arc_reclaim_thre mi_switch+0x16e sleepq_timedwait+0x42 _cv_timedwait+0x14f arc_reclaim_thread+0x29d fork_exit+0x11d fork_trampoline+0xe 
    9 100065 zfskern          l2arc_feed_threa <running>
    9 100251 zfskern          txg_thread_enter mi_switch+0x16e sleepq_wait+0x44 _cv_wait+0x13c txg_thread_wait+0x79 txg_quiesce_thread+0xb5 fork_exit+0x11d fork_trampoline+0xe 
    9 100252 zfskern          txg_thread_enter mi_switch+0x16e sleepq_wait+0x44 _cv_wait+0x13c zio_wait+0x61 dsl_pool_sync+0x2c3 spa_sync+0x336 txg_sync_thread+0x139 fork_exit+0x11d fork_trampoline+0xe

単にシングルユーザモードで再起動しただけで、 zfs mount は、まったくしていない状態です。 この dsl_scan_sync() や dsl_pool_sync() の処理の間は、 zfs destroy の処理が行なわれています。困ったことに、この処理が終るまで、 a/ftproot をマウントすることができません。 なかなか厳しいのですが、ずっと待たないといけないのです。

destroy してるわけですから、 zpool list で表示される ALLOC は、だんだんと減っていくのが観測できます。 ここでメモリの使用量に注目しましょう。

最初に試した時にはマシンの物理メモリ 8GB を一気に食い潰してしまったため、 次の設定を入れてメモリ消費を抑制させました。

vfs.zfs.arc_min="1G"
vfs.zfs.arc_max="2G"
vfs.zfs.arc_meta_limit="1G"
vfs.zfs.dedup.prefetch=0
vfs.zfs.prefetch_disable=1
vfs.zfs.l2arc_noprefetch=1
vfs.zfs.vdev.min_pending=1
vfs.zfs.vdev.max_pending=2

zpool を認識してから 26 分後の wired は 4370MB です。 40 分後は 5042MB になりました。短い時間で見ると増えたり減ったりしていますが、 確実に増加しています。

完了したのは 54 分後、6.52T から 5.95TB に ALLOC は減少しました。 583MB を 3240 秒で処理したことになりますから、 およそ 180KB/s くらいです。wired のメモリは最終的に 5683MB を示していました。今度は足りたわけですが、 この量のメモリがなければ、この復旧処理は途中で止まってしまうことになります。

また、前述のとおり、この処理が終らなくとも zfs mount できるデータセットはあります。 しかし、マウントはできるものの、処理が終るまで sync ができません。 この悩ましい状況は、長いダウンタイムを生むことがあります。 ZFS はスナップショットの作成が簡単にできますが、意外と落し穴は深いので、 取り扱いには十分注意しましょう。

8-STABLE のバグ

8.2R は問題ないのですが、r225100 より前の 8-STABLE には、 起動時にデッドロックが発生することがあります。見分けるコツは、 Ctrl+T で次のような表示になるかどうか、です。

load: 0.90  cmd: zpool 35 [spa_namespace_lock] 10.15r 0.00u 0.12s 0% 2116k

該当するバージョンで、これが出て止まっているとしたら、 そのバグを踏んでいる可能性が高いと考えて良いと思います。 zpool remove や、zpool add のタイミングで発生しやすく、 再起動してもこれに引っかかり続けるケースがあります。

r225100 の変更を入れてカーネルを再構築すれば、この症状は消えます。 slog や cache デバイスを入れていると、再現しやすいバグです。

Saturday, September 3

* 4KiB セクタの HDD と FreeBSD

Western Digital の物理セクタ長 4KiB の HDD (WDxxEARS など) が発売されてからもう 2 年ほど経過しました。 「FreeBSD で使えるの?」という話を、いろいろなところで尋ねられたり、 説明したりしているような気がするのですが、 まとまった資料がないので、まとめてみました。

4096B セクタ長の HDD

どうして WD は物理セクタ長を 512B から 4096B に変更したか、 という話は、いろいろなところで紹介されているので今更ですが、 これは WD だけが独自に考えたわけではなくて、 ストレージ機器の企業コンソーシアムである IDEMA (International Disk Drive Equipment and Materials Association) で議論された次世代規格 AF (Advanced Format) に基づくものです。

IDEMA では「HDD が今後記録密度を増大させていった時、 磁気媒体上の S/N 比の減少が問題となる。そこで、 それに対応する技術としてなにを採用すべきか」という問題提起が、 1998 年に起こりました。そこでは、 密度が上がると磁性材料の特性に起因するエラー発生確率が増えるため、 エラー訂正符号を強化する必要があるだろう、と予測されました。

エラー訂正符号を強化するというのは、アルゴリズムを変えることも含まれるのですが、 結局のところ、訂正用に余分に付加する情報を増やす、ということを意味します。 この情報は、物理セクタ単位で用意しなければなりません。つまり HDD の中での 512B の情報は、 「512B + エラー訂正用の付加情報」という、 ちょっと大きいデータとして格納されているわけです。

記録密度の増大によって、エラーの発生確率は増加します。 なので、そこに同じエラー訂正能力の手法を同じように使い続けていると、 訂正能力は変わらず、エラーの発生確率が増えるわけですから、 信頼性がどんどん落ちることになってしまいます。 「じゃあ訂正能力の高い手法を導入しようよ」という方向に話が進むのは、納得できる話です。

しかし、単純に訂正能力をあげると、せっかく増えた記憶容量がエラー訂正用の情報に食われてしまって、 嬉しくない状況になってしまいます。 実は、同じ訂正能力を持つ手法を 512B のデータと 4096B のデータに適用した場合、 データの長さは 8 倍違いますが、そこにくっつけるエラー訂正情報は 8 倍にならず、 もっと短い情報にすることが可能です。つまり、 「ひとかたまり」と考えるデータの量を大きくすれば、 (相対的にですが)少ないエラー訂正情報で、 同程度の訂正能力を実現できます。

なので、いろいろと「ひとかたまり」の大きさを変えて理論計算してみたところ、 4096B が、今までと同じ水準の信頼性を維持しつつ、エラー訂正情報に必要な記憶容量の増加が抑えられる値だろう、 ということが報告されました。その研究報告をスタート地点として、IDEMA では 4096B を物理セクタとする業界標準の策定に入り、具体的な製品が 2009 年から発表されはじめました。 今後増えるかどうかは分かりませんが、業界としての流れになっています。

注意しなければならないのは、物理セクタ長が 4096B になったからといって、 ユーザがアクセスのに使う論理セクタ長が 4096B になっているとは限らないことです。 Advanced Format では、次のようなパターンを想定しています。

512e の場合、HDD は OS に対して論理セクタ長を 512B と報告し、 4Kn の場合は 4096B と報告します。現在流通しているもののは、ほとんど 512e です。 どちらなのかは、ATA_CAM を有効にして camcontrol を使うとチェックできます。

# camcontrol identify ada1
pass2: <C300-CTFDDAC064MAG 0006> ATA-9 SATA 3.x device
pass2: 300.000MB/s transfers (SATA 2.x, UDMA5, PIO 8192bytes)

protocol              ATA/ATAPI-9 SATA 3.x
device model          C300-CTFDDAC064MAG
firmware revision     0006
serial number         00000000104702FF6594
WWN                   500a7512ff6594
cylinders             16383
heads                 16
sectors/track         63
sector size           logical 512, physical 512, offset 0
LBA supported         125045424 sectors
LBA48 supported       125045424 sectors
PIO supported         PIO4
DMA supported         WDMA2 UDMA5 
media RPM             non-rotating
:

このサンプル結果はセクタ長が 512B の SSD に対して行なったものですが、sector size という欄に、 logical と physical のセクタ長が出ていることが確認できます。 ここで physical が 4096 になっていれば、物理セクタ長が 4096B だ、という意味です。512e なら logical=512, physical=4096 に、 4Kn なら両方とも 4096 になります。

その後ろの、offset というのが曲者です。これは論理セクタ境界が、 物理セクタ境界に対してどれくらいずれているかを示す数値です。 後述するアラインメント問題で詳述します。

FreeBSD で使えるの?

結論から言うと、ちゃんと使えます。現行のもので「使えない」という報告はあがってきていません。 ただし、注意しなければならない点がいくつかあります。

LBA のアラインメント

アラインメントとは、論理セクタ番号を物理セクタ境界の数値に丸めることです。 512e の場合、ユーザからは 512B 単位でアクセスできるわけですが、 読み書きの最小単位は 4096B 単位です。 ですので、アクセスするセクタ番号が 8 の倍数になっている時が、 もっとも効率良くアクセスできます。

たとえば、論理セクタ番号 0 へのアクセスと、番号 3 へのアクセスを考えてみてください。 どちらも結果的に、物理セクタに対応する 8 個の論理セクタ (0,1,2,3,4,5,6,7) すべてへのアクセスになります。それぞれのアクセスが 4KiB のデータを読み込むアクセスだったとすると、 番号 0 は物理セクタ 1 個分だけで処理が終了しますが、 番号 3 は次の物理セクタまで読まなければならなくなります。 つまり、物理セクタのアクセス量が増えてしまいます。

このように、アクセス単位とアクセス先頭位置設定の細かさが異なる場合、 アクセス先頭位置設定をアクセス単位に合わせる操作を、「アラインメントを揃える」と表現します。 説明したとおり、ちょうど物理セクタの先頭位置からアクセスするのが、一番効率が良くなります。 もちろん読み込む量が 5KiB だとしたら、結局どちらも同じアクセス処理量になるのですが、 発生し得るいろいろなパターンを考えると、 先頭位置からずれているアクセスは、物理セクタを余分にアクセスする可能性が高くなり、 結果的にアクセス性能の低下につながります。

したがって、パーティション分割時の先頭セクタ番号のアラインメントに気をつけないと、 性能が低下することがあるわけです。4096B なら最低でも 8 の倍数にしましょう。 現状のインストーラや gpart 等のツールは、そのへんの面倒を見てくれません。

ただし、前述の offset が 0 でない場合は、8 の倍数に offset の数を加算して減算してください。 非常に分かりづらい仕様なのですが、offset が 1 の時、論理セクタの先頭と物理セクタの先頭は、 論理セクタ 1 個分だけずれています。この場合、 8 の倍数ではなく、8 の倍数 + 1 8 の倍数 - 1 に丸めないといけません。 (2011/9/28 誤情報修正)

ちなみに最近増えてきた SSD は、ものによって若干違いますが、 同じようなデバイスの特性上の理由で、MLC なら 128KiB, SLC なら 64KiB のアラインメントが、最低でも必要になります。 4096B は高々 8 倍ですが、SSD は非常に大きな単位を持っているため、 性能に与えるインパクトも大きいです。

最近の Windows や MacOS X は、初期設定時の開始セクタを 1MiB 境界に 設定してくるようになってきていますので、それにならって、 いつもそれに合わせると決めておくと、 ドライブの種類が変わっても悩まなくて良いでしょう。 先頭の 1MiB 分が無駄になりますが、最近の HDD なら微々たるものです。

物理セクタ長が 512B だと報告される?

すでに書いたとおり、HDD から OS に報告されるセクタ長は camcontrol ユーティリティでチェックすることができます。 512e の HDD は論理セクタ長を 512B と報告しますが、 FreeBSD は物理セクタ長に基づいた I/O アクセス単位の最適化を行ないます。

しかし、どうも「物理セクタが 512B だ」と嘘情報を返すものがあるらしく、 その場合は最適化が効きません。その場合でも動かなくなるような 深刻な影響はないのですが、手動で対応する必要があります。

対応方法は次の URL の記述が詳しいです。

http://ivoras.net/blog/tree/2011-01-01.freebsd-on-4k-sector-drives.html

簡単にまとめると、次のようになります。

UFS であれば、newfs 時にフラグメントサイズを大きくとる操作が有効です。 「-b 32768 -f 4096」というオプションを指定しましょう。

UFS のフラグメントサイズは、UFS の中でもっとも小さいデータブロックのサイズだと考えてください。 UFS 上のデータは、「ブロック」と「フラグメント」という 2 種類の領域を組み合わせた部分に格納されます。 ブロックサイズは newfs の -b オプションで指定可能で、現在のデフォルトは 16KiB です。 フラグメントはその 1/8, 2KiB になっています。 たとえば 36KiB のファイルは、ブロック 2 個とフラグメント 2 個の組み合わせで保存されるわけです。

4096B の物理セクタ長を持っている場合、 フラグメントが 2048B だとアラインメントの問題が生じてアクセス速度が低下する可能性があります。 そこで、ブロックを 32KiB, フラグメントを 4KiB にして newfs することで、 最小アクセス単位を 4KiB にします。パーティション先頭のアラインメントが揃っているのであれば、 ほぼすべてのアクセスが 4KiB 境界からスタートするようになり、効率的です。

デメリットは、容量利用効率が若干低下することです。最小単位が 4KiB になるので、 4KiB より小さいファイルを書き込んでも、最低 4KiB を消費します。 ほとんどの場合、これが大きく問題になることはないのですが、 2KiB フラグメントの UFS からファイルをコピーしたりした場合に、 空き容量の計算が違ってくるので注意が必要です。

ZFS は、ZFS データセットの特性の中で ashift というパラメータが 12 になっている状態が最適です。 (ashift はアラインメントのためのパラメータ。512B だと 2^9 なので 9 です)

# zdb
p:
    version: 28
    name: 'p'
    state: 0
    txg: 4
    pool_guid: 6065572820319548312
    hostid: 521902490
    hostname: 'test.allbsd.org'
    vdev_children: 1
    vdev_tree:
        type: 'root'
        id: 0
        guid: 6065572820319548312
        create_txg: 4
        children[0]:
            type: 'disk'
            id: 0
            guid: 16575765623681420282
            path: '/dev/ada0s1f'
            phys_path: '/dev/ada0s1f'
            whole_disk: 1
            metaslab_array: 30
            metaslab_shift: 27
            ashift: 9
            asize: 15994191872
            is_log: 0
            create_txg: 4

下から 4 番目にあります。ashift は、 zpool を構成する HDD それぞれに設定されるパラメータです。

物理セクタ長を「512B だ」と報告する HDD の場合、検出がうまくいかずに 上記のように ashift が 9 になってしまいます。これは明示的に指定することが できないパラメータなので、上記 URL の記事では、gnop を使って 4096B に 見せかけて ZFS データセットを作成する、という荒技を使っています。

# gnop create -S 4096 /dev/ada1
# gnop create -S 4096 /dev/ada2
# zpool create data mirror /dev/ada0.nop /dev/ada1.nop
# zpool export data
# gnop destroy /dev/ada0.nop /dev/ada1.nop
# zpool import data

「gnop -S 4096 /dev/ada1」は、/dev/ada1.nop というデバイスノードを生成します。 gnop で生成されるデバイスノードは、基本的にアクセスを素通しするだけなのですが、 セクタ長や総容量を見かけ上変えたり、疑似的なエラーを発生させたりという付加機能が付いています。 主に、開発者がテストのために使うものです。

-S 4096 を指定して生成した /dev/ada1.nop を使ってアクセスすると、 セクタ長が 4096B であるように見えます。それを使って ZFS データセットを作成し、 一回 export します。そして gnop デバイスを削除してから import すれば、 ashift が 12 になったデータセットが得られる、というわけです。

少々強引ですが、今のところはこのような方法しかありません。 何もしなくても使えるのですが、 アラインメントを合わせないと性能の低下が発生しますので、 面倒でも合わせておくのをおすすめします。

また、mirror や raidz pool に 512B と 4096B な HDD を混在させると、 性能が落ちることがあります。これは 4096B セクタな HDD 特有の話ではなく、 デバイスの非対称性によって発生する問題です。

感想はこちらまで (内容は匿名のメールで送られます)

コメント:

注: お返事が必要な場合は直接メールください。 ただし、確実にお返事するかどうかはわかりませんのであしからず。