以下は、Linux カーネル文書、Documentation/filesystems/xfs-self-describing-metadata.txt の訳です。原文と同じ、GPL v2 で公開します。
XFS が直面している最も大きなスケーラビリティ問題は、アルゴリズムではなく、ファイルシステム構造の検証にあります。ディスク上の構造とインデックスのスケーラビリティおよび、それらを列挙するアルゴリズムは、数10億の inode, ペタバイト級のファイルシステムにとっても妥当なものです。しかし、かえってそのために、検証のスケーラビリティが問題となります。
XFS のほとんどすべてのメタデータは、ダイナミックに確保されます。位置が固定のメタデータは、アロケーショングループヘッダ(SB, AGF, AGFL and AGI)だけです。それ以外のメタデータ構造は、それぞれの方法で、ファイルシステム構造をたどって見つける必要があります。現在既に、ユーザー空間のツールがあって、ファイルシステム構造を検証し、修復するために使われています。しかし、検証できるものには限界があり、これが XFS ファイルシステムのサポート可能な大きさを決めることになります。
例えば、xfs_db と、いくらかのスクリプトを使って、100テラバイトのファイルシステムの構造を解析して、ファイルシステムがこわれた根本原因を、手作業で調査するのは、やればできます。しかし、シングルビットエラーや、間違った位置へのライトが、破壊の根本原因であるかを判断するのは、今のところ、手作業に頼る他はありません。そういうフォレンジック調査をするには、数時間から数日かかるかもしれません。これくらいのスケールであれば、根本原因の調査は、可能な範囲です。
しかし、ファイルシステムがペタバイト以上にスケールすると、メタデータは10倍以上になり、フォレンジック作業は何週間も何ヶ月もかかることになります。調査のほとんどは、低速で、面倒です。調査の規模が大きくなると原因が雑音にまぎれてわからなくなる可能性が高まります。なので、ペタバイトスケールのファイルシステムをサポートするときの主な課題は、ファイルシステム構造の基本的なフォレンジック調査に必要な時間と努力を最小にすることです。
現在のメタデータ形式の1つの問題は、メタデータブロックにあるマジック番号以外には、それがなんであるかを知る手段がないことです。また、それが正しい場所にあるのかもわかりません。簡単に言えば、あなたは切り離された1つのメタデータブロックを見て、「よし。これは正しいところにあって、内容も有効だ。」と言う事はできないのです。
フォレンジック調査のほとんどの時間は、メタデータの値の基本的検証に使われます。定められた範囲に収まっている(ため、自動のチェックにはかからない)けれど、正しくない値を探すのです。クロスリンクしたブロックリスト(つまり、Bツリーの兄弟ポインターで、ループになっているもの)のようなものを発見し、なぜそうなったかを理解するのは、問題が起きた理由を理解する鍵となります。しかし、事が起こってしまった後で、ブロックがリンクされた順序や、ディスクに書かれた順序を知るのは不可能です。
このため、メタデータにもっとたくさんの情報を記録して、そのメタデータが正常で、調査のためには無視しても良いことを素早く決定できるようにする必要があります。すべてのエラーのタイプに対処することはできませんが、一般的なエラーが簡単に検出できるようにすることはできます。自己記述型メタデータはこのためにあります。
何よりもまず、自己記述型メタデータの基本的要件は、メタデータオブジェクトが既知の場所に、なんらかの形の一意な識別子を持つことです。これにより、ブロックの期待される内容を識別することができ、メタデータオブジェクトを解読して検証することができます。あるオブジェクトに含まれるメタデータの型を個別に識別できないときは、メタデータは自己記述型ではないということです。
幸い、ほとんどすべての XFS メタデータは既にマジック番号を埋め込まれています。なお、AGFL、リモートシンボリックリンク、そしてリモート属性ブロックだけは、識別可能なマジック番号を持ちません。そこで、これらすべてのオブジェクトのディスク上形式を変え、より多くの識別情報を追加し、そしてメタデータオブジェクトにあるマジック番号を変えることで形式の違いを判定できるようにするのがよいでしょう。つまり、マジック番号が今と同じなら、メタデータは自己記述型ではありませn。新しいマジック番号なら、自己記述型なので、フォレンジック調査や修復の時に、ずっと広範な自動検証が可能です。
自己記述型メタデータは、なんらかの全体的な一貫性チェック、改ざん防止、が必要です。外部の要因のためにメタデータが変更されていないことを確認しない限り、それを信用できません。このために、メタデータブロックに CRC32c 検証コードを追加しました。あるブロックが期待通りのメタデータを含むことが検証できれば、膨大な手作業による検証をスキップすることができます。
XFS では、メタデータは64KB 以上にならないため、CRC32c が選ばれました。32ビット CRC があれば、メタデータブロックの複数ビットエラーを検出するのに十分すぎるくらいです。また、CRC32c は一般的な CPU でハードウエア補助があるため、高速です。CRC32c は可能な一貫性チェック手段のうち、最強ではないものの、私達の要求にとっては十分で、比較的オーバーヘッドも小さいです。より大きな一貫性チェックフィールドと多くのアルゴリズムをサポートするのは CRC32c に比べて何らかの価値を増すことができるでしょうが、複雑性も増すため、一貫性チェック機構を変更するしかけを提供する予定はありません。
自己記述型メタデータは、メタデータブロックが正しい場所にあることを、他のメタデータを見ることなく検証できるだけの十分な情報を含む必要があります。つまり、それは位置情報を持つ必要があるということです。メタデータにブロック番号を追加しただけでは、間違った位置へのライトへの対処として不十分です。ライトは、間違った LUN に向けられ、間違ったファイルシステムの、「正しいブロック」に書かれるかもしれません。なので、位置情報としては、ブロック番号とファイルシステム識別子が必要です。
フォレンジック調査で鍵となるもうひとつの情報は、あるメタデータブロックが誰に属するかです。タイプ、位置、それが有効かこわれているか、そしてそれが最後に更新された時刻は既にわかっています。ブロックの所有者を知ることによって、破壊のスコープを決めるのに役立つ、関連するそれ以外のメタデータを発見することができるため、それは重要なことです。例えば、エクステントBツリーオブジェクトがあるとします。それがどの inode に属するかはわからないため、そのブロックの所有者を探すためにファイルシステム全体をたどらなくてはいけません。さらに、こわれているために所有者がわからないこともありえます。(つまり、親のないブロックです。)そして、メタデータに所有者フィールドがないため、破壊のスコープがわかりません。もしそれがあれば、すぐさま、トップダウンの検証をすることで問題のスコープがわかるでしょう。
メタデータのタイプによって、所有者の識別子も変わります。例えば、ディレクトリ、属性、そしてエクステントツリーブロックはすべて inode によって所有されます。一方、空きスペースBツリーブロックはアロケーショングループによって所有されます。このため、所有者フィールドの長さと内容は、メタデータオブジェクトのタイプによって決まります。所有者情報は、また、間違った位置へのライトを発見することもできます。(つまり、空きスペースBツリーブロックが、間違った AG に書かれているとき)
さらに、自己記述型メタデータは、それがファイルシステムに書かれた時刻を示すものを持っている必要があります。フォレンジック調査で鍵となる情報に、そのブロックがどのくらい最近更新されたかがあります。複数のこわれたメタデータブロックの相関を、更新時刻を元に考えれば、以下のことがわかるために重要なことです。それらの破壊が関連づいているか、最終的な障害になる前に、複数の破壊があったかどうか、そして、実行時検証がまだ検出することができないでいる破壊が存在するか。
例えば、あるメタデータオブジェクトが解放されているのか、所有者によって参照され、確保されているのかは、以下のように決めることができます。まず、そのブロックを含む空きスペースBツリーブロックが書かれた時刻を見ます。次に、メタデータオブジェクト自身とその所有者が書かれた時刻を見ます。前者が新しければ、そのブロックは、所有者から削除されて解放されている可能性が高いです。
この「書き込みタイムスタンプ」を提供するため、それぞれのメタデータブロックは、それを更新した最も最近のトランザクションのログシーケンス番号 (LSN) が書かれています。この番号は、ファイルシステムがある間、ずっと単調増加します。リセットされるのは、 xfs_repair を実行した時だけです。さらに、LSN を使えば、こわれたメタデータがすべて同じログチェックポイントに属するかがわかります。また、こわれたメタデータの最初のものと最後のものとの間に、どれくらいの更新があったか、さらに、こわれたメタデータが書かれた時から、それが検出されるまでにどれくらいの更新があったかもわかります。
自己記述型メタデータの検証は、実行時に2箇所で行われます。
- ディスクからのリードが成功した直後
- ライト IO を発行する直前
検証は、完全にステートレスです。更新処理とは全く独立して行われ、メタデータがそれが示す通りのものであり、フィールド値は妥当な範囲で、内部的に一貫していることをチェックするだけです。このため、ブロックで起きる可能性のあるすべてのタイプの破壊を捕まえることはできません。実行中の状態であることからくる制限があったり、ブロックをまたがる破壊があったり(前述の兄弟ポインターリストの破壊など)するためです。このため、コードの本体ではステートフルなチェックが必要でしょうが、一般的には、ほとんどのフィールドごとの検証はここで述べるように行われます。
リードの検証では、コール元は期待されるメタデータタイプを指定します。そして IO 完了処理で、読まれたものが期待通りであるか調べます。検証が失敗したら、読んだオブジェクトは、 EFSCORRUPTED とマークされます。コール元は、IO エラーと同じようにそれをチェックする必要があり、検証失敗に応じた処理をします。もしもより高いレベルでエラーの種類を区別する必要があるなら、新しいエラー番号を作ってもよいです。
リードの検証の最初のステップは、マジック番号をチェックして、CRC チェックをするか決めることです。するなら、 CRC32c を計算して、オブジェクト自身に書いてある値と比較します。これが成功したら、位置情報を調べ、さらに、オブジェクト固有の検証に進みます。どれかが失敗したら、バッファは破壊されたものとして、 EFSCORRUPTED エラーが設定されます。
ライトの検証は、リードの逆です。最初にオブジェクトが完全に検証され、次にオブジェクトを最後に更新した LSN が設定されます。この後、CRC を計算して、オブジェクトに入れます。この後、ライト IO は続行されます。途中でどれかが失敗したら、バッファは EFSCORRUPTED とマークされ、上位層がわかるようにされます。
典型的なディスク上構造は、以下の情報を持つ必要があります。
struct xfs_ondisk_hdr {
__be32 magic; /* magic number */ __be32 crc; /* CRC, not logged */ uuid_t uuid; /* filesystem identifier */ __be64 owner; /* parent object */ __be64 blkno; /* location on disk */ __be64 lsn; /* last modification in log, not logged */ };
メタデータによって、この情報は、メタデータ内容とは別のヘッダー構造の一部となることもあります。あるいは、既存の構造内に分散して置かれることもあります。後者は、既に前記情報のいくらかを持っている、スーパーブロックや AG ヘッダーがそうなります。
他のメタデータは前記情報を異なる形式で持つこともあるでしょう。しかし、同じレベルの情報が一般的には提供されます。例えば、
- 短形式のBツリーブロックは32ビットの所有者(AG 番号)と、32ビットのブロック番号を位置情報として持ちます。この2つを組み合わせると、前述の構造体の @owner, @blkno と同じ情報になりますが、ディスク上で8バイト小さくて済みます。
- ディレクトリ/属性ノードブロックは、16ビットのマジック番号を持ち、それを含むヘッダーはそれ以外の情報も持ちます。このため、追加のメタデータヘッダーは、メタデータ全体の形式を変えます。
典型的なバッファリード検証は以下のように行われます。
#define XFS_FOO_CRC_OFF offsetof(struct xfs_ondisk_hdr, crc)
static void xfs_foo_read_verify( struct xfs_buf *bp) { struct xfs_mount *mp = bp->b_target->bt_mount; if ((xfs_sb_version_hascrc(&mp->m_sb) && !xfs_verify_cksum(bp->b_addr, BBTOB(bp->b_length), XFS_FOO_CRC_OFF)) || !xfs_foo_verify(bp)) { XFS_CORRUPTION_ERROR(__func__, XFS_ERRLEVEL_LOW, mp, bp->b_addr); xfs_buf_ioerror(bp, EFSCORRUPTED); } }
このコードでは、スーバーブロックのフィーチャービットを見て、ファイルシステムに CRC が有効なときだけ、CRC をチェックします。そして、CRC 検証が成功(あるいは不要)なときに、ブロックの実際の内容を調べます。
検証機能は、マジック番号をブロックの形式を決めるために使うことができない時には、以下のようになるでしょう。
static bool
xfs_foo_verify(
struct xfs_buf *bp)
{ struct xfs_mount *mp = bp->b_target->bt_mount; struct xfs_ondisk_hdr *hdr = bp->b_addr; if (hdr->magic != cpu_to_be32(XFS_FOO_MAGIC)) return false; if (!xfs_sb_version_hascrc(&mp->m_sb)) { if (!uuid_equal(&hdr->uuid, &mp->m_sb.sb_uuid)) return false; if (bp->b_bn != be64_to_cpu(hdr->blkno)) return false; if (hdr->owner == 0) return false; } /* object specific verification checks here */ return true; }
複数のマジック番号を使って、複数の形式を調べるときは、こうなります。
static bool
xfs_foo_verify(
struct xfs_buf *bp)
{ struct xfs_mount *mp = bp->b_target->bt_mount; struct xfs_ondisk_hdr *hdr = bp->b_addr; if (hdr->magic == cpu_to_be32(XFS_FOO_CRC_MAGIC)) { if (!uuid_equal(&hdr->uuid, &mp->m_sb.sb_uuid)) return false; if (bp->b_bn != be64_to_cpu(hdr->blkno)) return false; if (hdr->owner == 0) return false; } else if (hdr->magic != cpu_to_be32(XFS_FOO_MAGIC)) return false; /* object specific verification checks here */ return true; }
ライトの検証は、リードと似ていて、逆にするだけです。
static void
xfs_foo_write_verify(
struct xfs_buf *bp)
{ struct xfs_mount *mp = bp->b_target->bt_mount; struct xfs_buf_log_item *bip = bp->b_fspriv; if (!xfs_foo_verify(bp)) { XFS_CORRUPTION_ERROR(__func__, XFS_ERRLEVEL_LOW, mp, bp->b_addr); xfs_buf_ioerror(bp, EFSCORRUPTED); return; } if (!xfs_sb_version_hascrc(&mp->m_sb)) return; if (bip) { struct xfs_ondisk_hdr *hdr = bp->b_addr; hdr->lsn = cpu_to_be64(bip->bli_item.li_lsn); } xfs_update_cksum(bp->b_addr, BBTOB(bp->b_length), XFS_FOO_CRC_OFF); }
この場合、まず最初にメタデータの内部的構造を検証します。それにより、メタデータがメモリ上で更新された結果起きる破壊を検出します。それが成功し、CRC が有効ならば、LSN フィールド(最後にいつ更新されたか)を入れ、メタデータについて CRC を計算します。これが終わったら、IO を発行できます。
inode と dquot は特別な雪のかけらです。オブジェクトごとの CRC と自己記述子を持ちますが、1つのバッファに複数オブジェクトが入るように、パックされています。このため、オブジェクトごとの検証と CRC 計算をするためにバッファごとの検証関数を使うことができません。バッファごとの検証関数は、バッファに対しての基本的識別をするだけです。inode とディスククオータを含んでおり、あるべきところすべてにマジック番号があることを検証します。それ以上の CRC と検証はそれぞれの inode がバッファから読み書きされた時に行われます。
検証関数や、識別子のチェックは、前述のバッファコードととても似ています。呼ばれる場所だけが異なります。例えば、inode リードの検証は、 xfs_iread() において、inode がはじめてバッファから読まれて、 struct xfs_inode が作られる時に行われます。inode の検証は、今でも、 xfs_iflush_int において書き込む時に十分行われています。なので、追加すべきは、inode がバッファにコピーバックされるときに LSN と CRC を加えることだけです。
XXX:inode アンリンクトリストの更新は、inode CRC を再計算しません!リスト更新は、削除の時もログ回復の時も、CRC のチェックも更新もしません。なので、今まで誰も気が付かずにいました。これは当面は問題とならないでしょう。repair が気がついてなんとか言うでしょう。でも、なおす必要があります。
訳は2013年 kanda.motohiro@gmail.com による。