ファイルシステムが正常にマウントできるからといって、クラッシュ前の2回の業務用書き込みが同時に保持されていることを意味するわけではありません。以下では、ブロックレイアウト、構造復旧、データチェックサム、履歴復元 をそれぞれ詳しく解説し、1つの特徴ですべての保証を概括しないようにします。
説明すべき現象 参照箇所
ファイルオフセットはどのようにデバイスブロックにマッピングされるか? 第1節:extent の手動計算
ジャーナルと COW はそれぞれどのように復旧するか? 第2~4節:ext4、XFS、Btrfs
デバイスの階層化にはどのような調整が必要か? 第5節:bcachefs
同じ条件下で比較するには? 第6節:能力の境界と計算実験
1. extent は連続範囲のマッピングであり、定数時間の魔法ではない
ブロックサイズを 4 KiB とし、extent が「論理ブロック 100 からの 8 ブロックが、物理ブロック 900 からの 8 ブロックに対応する」と記述しているとします。102 × 4096 + 17 にアクセスする場合:
ステップ 計算
extent 内の相対ブロック位置を特定 102 − 100 = 2
物理ブロックへのマッピング 900 + 2 = 902
ブロック内オフセットを保持 物理ブロック 902 内の 17 バイト目
物理ブロック番号は依然としてファイルシステム層に属しており、その下にはボリュームマネージャーやデバイスマッピングが存在する可能性があります。
extent が節約するのは、連続領域のマッピング記録です。8つのブロックは、ブロックごとにポインタを持つ代わりに、1つの記録で記述できます。間接ブロックの設計は、N番目のブロックを検索する際に N 個のポインタを線形スキャンするわけではありません。インデックスを通じて段階的に位置を特定できます。extent の木検索のコストも、木の高さと断片化の数に依存しており、「間接ブロックは O(N)、extent は O(1)」と概括することはできません。
ext4 の inode には、extent 木のルートを受け入れる 60 バイトの領域があります。12 バイトのヘッダーの後に、4つの 12 バイトの記録を格納できます。木の高さが 0 の場合、これらはリーフの extent です。木が大きくなると、ルートにはインデックスが格納され、「最初の4セグメントのデータは常に inode の中に直接残る」とは言えなくなります。ee_len には、初期化済み/未初期化(unwritten)のエンコーディングルールがあり、単なる解釈不能な通常の長さではありません。ext4 inode とブロックマッピング を参照してください。
3つの「まだ書き込まれていない」状態、占有関係はそれぞれ異なる
状態 物理位置と読み取りのセマンティクス
ホール(Hole) 通常、データブロックが存在しない
unwritten extent ブロックが予約されており、読み取り時はゼロが表示され、書き込み後に有効な状態に変換される
遅延割り当て キャッシュ段階で物理位置の決定を遅らせる
遅延割り当ては集約に有利ですが、その後の書き戻しで割り当てに失敗する可能性があります。プリアルLOCATION も、将来のメタデータ更新や COW におけるスペース不足を絶対に回避するものではありません。
以下では、「読み取るとゼロになる」と「物理ブロックがすでに割り当てられている」を分けて図解します。ホール、unwritten、通常、共有範囲それぞれに書き込みを行い、位置や他のファイルの内容が同時に変わるかどうかを観察します。
図を準備しています 再読み込み
読み取り結果は同じゼロでも、占有関係は異なる ホールにはデータブロックがなく、unwritten は予約済みだが読み取り時はゼロとなる。共有 extent への書き込みはその範囲を分離し、もう一方のファイルは旧内容を参照し続ける。
{"id":"extent-states-and-cow","title":"読み取り結果は同じゼロでも、占有関係は異なる","summary":"ホールにはデータブロックがなく、unwritten は予約済みだが読み取り時はゼロとなる。共有 extent への書き込みはその範囲を分離し、もう一方のファイルは旧内容を参照し続ける。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><p class=\"caption\" data-i18n=\"logical\"></p><div class=\"choices\"><button data-block=\"0\">0</button><button data-block=\"1\">1</button><button data-block=\"2\">2</button><button data-block=\"3\">3</button><button data-block=\"4\">4</button><button data-block=\"5\">5</button></div><div id=\"state\" class=\"readout\"></div><div class=\"actions\"><button id=\"read\" data-i18n=\"read\"></button><button id=\"write\" data-i18n=\"write\"></button></div><div class=\"track-row\"><span data-i18n=\"mapping\"></span><div id=\"mapping\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"other\"></span><div id=\"other\" class=\"track\"></div></div><p class=\"caption\" data-i18n=\"result\"></p><div id=\"result\" class=\"value\">—</div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.choices{grid-template-columns:repeat(6,minmax(0,1fr))}.track{display:grid;grid-template-columns:repeat(3,minmax(0,1fr))}.cell{min-height:65px}.readout{min-height:4.8em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let chosen=0,state,pfn,data,serial,result='—';function init(){state=['hole','unwritten','written','shared','shared','hole'];pfn=[null,100,101,102,103,null];data=['0','0','A','B','C','0'];serial=200;result='—';}function draw(){document.querySelectorAll('[data-block]').forEach(e=>e.setAttribute('aria-current',String(+e.dataset.block===chosen)));set('state','L'+chosen+' · '+t(state[chosen])+'\\n'+(pfn[chosen]===null?'—':'P'+pfn[chosen]));cells('mapping',pfn.map((p,i)=>({text:'L'+i+' → '+(p===null?'—':'P'+p)+' ['+data[i]+']',cls:i===chosen?'on':state[i]==='shared'?'gold':p===null?'empty':''})));cells('other',Array.from({length:6},(_,i)=>({text:'L'+i+' → '+(i===3?'P102 [B]':i===4?'P103 [C]':'—'),cls:i===3||i===4?'gold':'empty'})));set('result',result);set('explanation',t('note'));}document.querySelectorAll('[data-block]').forEach(e=>e.onclick=()=>{chosen=+e.dataset.block;draw();});$('#read').onclick=()=>{result='L'+chosen+' → '+data[chosen];draw();};$('#write').onclick=()=>{if(state[chosen]==='hole'||state[chosen]==='shared')pfn[chosen]=serial++;state[chosen]='written';data[chosen]='N';draw();};$('#reset').onclick=()=>{init();draw();};init();draw();","audio":false,"strings":{"heading":"読み取り結果は同じゼロでも、占有関係は異なる","hole":"ホール","intro":"論理ブロックを1つ選び、読み取りまたは N の書き込みを行う。ホール、予約済み未書き込みブロック、共有ブロックの比較が重要:書き込み後、どのマッピングが変化し、もう一方のファイルも変化するかどうか?","logical":"現在のファイル:論理ブロック番号","mapping":"現在のファイル → 物理ブロック","note":"矢印の終点と旧内容を観察すること。ホールを読み取っても、このモデルではデータブロックは割り当てられない。共有ブロックへの書き込みで、他のファイルが依然として参照しているブロックを直接上書きすることはできない。","other":"もう一方のファイルが保持する共有内容","read":"選択したブロックを読み取る","reset":"リセット","result":"最新の読み取り結果","scope":"6つの 4 KiB 論理ブロック;1回の操作でブロック全体をカバーし、キャッシュ、遅延割り当て、圧縮、永続性は含まない。下段は各論理ブロックのマッピング説明であり、物理的な距離の比率で描画していない。共有ブロックへの書き込みは新しいブロックを使用し、範囲単位の COW の教育粒度である。","scopeLabel":"モデルの範囲と規約","shared":"共有有効ブロック","unwritten":"予約済み、読み取りはゼロ","write":"ブロック全体に N を書き込む","written":"排他的有効ブロック"}}
2. ext4:ジャーナルは何を保護し、ordered は何を追加するか
JBD2 は一連のメタデータ変更をジャーナルに書き込み、有効なコミットレコードを確立した後、後で最終位置へのチェックポイントを実行できます。クラッシュ復旧では有効なトランザクションを再生し、ディレクトリ、割り当てビットマップ、inode などの関連する変更が、一貫性のない中間状態で停止しないようにします。コミットはジャーナルトランザクションが復元可能な境界であり、チェックポイントは最終位置への進行です。これらは同じ瞬間ではありません。
データモード ユーザーデータとジャーナルの関係 アプリケーションが依然として処理すべきこと
data=ordered通常、メタデータのみを記録し、関連するデータとメタデータのコミットに順序制約を課す 書き換えや複数回の write が自動的にアプリケーショントランザクションになるわけではない
data=writebackordered モードのようなデータ先行制約を提供しない 復旧後の内容がアプリケーションが期待する新データではない可能性がある
data=journalデータもジャーナルを経由する アプリケーションのコミットポイントを依然として定義し、同期結果を確認する必要がある
例えば、アプリケーションが同じファイルの2つの領域を連続して上書きする場合、ファイルシステムメタデータが完全に合法であっても、クラッシュ後に data=ordered だけで両方が同時に新バージョンであると見なすべきではありません。ジャーナル整合性はファイルシステムが復元可能な構造に関心があり、アプリケーションの1つの業務トランザクションには、独自の原子性更新プロトコルが必要です。ext4 ジャーナルの説明 を参照してください。
遅延割り当てとバックグラウンド書き戻しは、メモリ内に永続化されていないデータの集合を拡大できますが、「クラッシュ時にダーティページが消失する」現象は遅延割り当て固有のものではありません。ここで変更を保持する必要があるかどうかは、fsync、同期オープンなどのインターフェースで表現すべきであり、バックグラウンドで数秒後にフラッシュされるかどうかを推測すべきではありません。
3. XFS:割り当ての並列性を AG に分散させる
AG は Allocation Group(割り当てグループ) です。各グループはローカルな空きスペースと inode 割り当て情報を管理し、すべての割り当てリクエストが1つのグローバル構造に殺到する圧力を軽減します。ファイルのデータは複数の AG に分散可能であり、AG は各 CPU が固定で独占するパーティションではありません。ジャーナル、同一 inode の並列変更、デバイススループットは依然として共有ボトルネックとなる可能性があります。
XFS は異なるインデックスで異なる質問に答えます。アドレスとサイズで整理された空き空間インデックスは適切な領域を見つけるために使用されます。inode 割り当て木は inode チャンクとその使用状況を記述します。ファイル自身の extent マッピングは論理オフセットを物理領域にマッピングします。inode 割り当て木を単純に「各リーフが完全な inode を格納している」と記述することはできません。小規模な記録は埋め込み形式で存在する可能性があり、必ずしも外部の B+tree を毎回トラバースするわけではありません。
共有 extent:独立したファイルでも内容は共有可能
reflink をサポートするファイルシステムでは、2つの異なる inode がデータ extent を共有でき、共有範囲の修正時に COW が実行されます。コピーはハードリンクではありません。両方の名前とメタデータは独立しており、その後で片方を修正しても、ハードリンクのセマンティクスに従ってもう片方が変更されることはありません。XFS reflink と Btrfs はどちらも範囲単位で共有および分裂をサポートしており、「extent 単位とファイル全体単位の COW」で二者を区別することはできません。
最新の XFS の自己記述型メタデータには、チェックサム、所有オブジェクト、位置情報などが含まれており、破損や誤った位置への書き込みを検出できます。これは、通常のユーザーファイルデータにも同様のエンドツーエンドのチェックサムがあることを意味するものではありません。XFS メタデータ完全性設計 を参照してください。
4. Btrfs:参照パスの更新
COW とスナップショット:書き換えパス、参照されている旧ブロックを保持
スナップショットルート R₀
旧バージョンを保持
現在のルート R₁
コミット後の新バージョン
旧メタデータ M₀
新メタデータ M₁
旧データ A
変更されていないデータ B
新データ A′
A の修正でファイル全体をコピーする必要はありません。スナップショットの参照が残っている限り、旧 A を回収することはできません。
デフォルトの COW データパスでは、変更を新しい位置に書き込み、関連するメタデータ参照を更新します。トランザクションのコミットには、書き込み順序とルートの有効性要件を満たす必要があります。「各ポインタが個別に原子書き込みされるため、どの中間状態でも安全である」というわけではありません。旧 extent は、現在の木、スナップショット、その他の参照がそれを必要としなくなった場合にのみ回収できます。
スナップショットは通常、既存の構造とデータを共有して作成され、サブボリューム全体の内容をコピーする必要はありませんが、すべての規模と負荷で厳密に O(1) であるわけではありません。その後、旧バージョンが保持されるにつれてスペースが徐々に消費されます。ファイルを削除した後でも、スナップショットはそれが占有していたブロックを保持し続ける可能性があります。サブボリュームには独自のファイルツリーがあり、個別にマウントできますが、独立した物理ボリュームではなく、自動的に mount namespace 分離を形成するわけではありません。ネストされたサブボリュームの内容は、親/子ボリュームのスナップショットに再帰的に含まれません。
COW は、小さな修正でも常に元の完全な大 extent をコピーする必要はありません。実際には、実装や圧縮ユニットなどのルールに従って分割/書き換えが行われます。圧縮、断片化、共有の程度は書き込み増幅に影響します。NOCOW などの設定を有効にすると、データチェックサムと共有書き込みの制限が異なります。Btrfs には fsync を加速するための tree log があるため、「COW ファイルシステムにはジャーナルが不要である」という主張も成立しません。Btrfs 機能の説明 および サブボリュームのセマンティクス を参照してください。
検出、修復、履歴復元は3つの異なる能力
必要な機能 そのために必要な条件 それだけでは保証できないこと
破損の検出 コンテンツとチェックサム記録を比較可能であること 検出が必ずしも再構築可能であることを意味しない
破損の修復 検証可能な正しいコピーがもう1つ存在すること ミラーが自動的に削除前の履歴を保持するわけではない
履歴の復元 スナップショットまたは履歴バージョンがまだ存在すること 同じプール内のスナップショットでも故障ドメインを共有する
プール全体の損失への対応 独立した故障ドメイン内のバックアップ 復旧プロセスの検証は依然として必要
したがって、「2枚のディスク」「チェックサムあり」「スナップショットあり」は異なる条件であり、1つで他の条件を代替することはできません。
Btrfs のデータチェックサムアルゴリズムは設定可能であり、すべてのファイルを固定 CRC32C に書き込むべきではありません。データとメタデータのプロファイルも異なる場合があります。「2枚のディスクがある」だけでは、実際のプロファイル、デバイスのエラー、残りの利用可能なコピー数をチェックする代替にはなりません。
5. bcachefs:階層配置と回収も設計の重点
bcachefs は、extent マッピング、B-tree、ジャーナル、チェックサム、圧縮、マルチデバイス管理を組み合わせます。高速デバイスは書き込みまたはキャッシュを受け持ち、低速デバイスはバックグラウンドでのデータ移行データを保持します。これには、配置ターゲット、コピー数、バックグラウンド移行、スペース回収の間の調整が含まれます。copygc は、まだ有効なデータを回収対象領域から移動させるため、ユーザーの書き込み量とデバイスの総書き込み量は異なる可能性があります。
ジャーナルは削除できる装飾的なオプションではなく、「COW が主でジャーナルはオプション」という記述は、そのリカバリプロトコルを説明するものではありません。比較する際は、永続コミットの条件、データとメタデータのコピーポリシー、デバイス故障後の復旧パスを見るべきであり、プロジェクトの宣伝目標を証明済みのパフォーマンス結論として記述すべきではありません。
バージョンとデプロイメントの境界
バージョン確認:2026-09-08。 bcachefs は Linux 6.7 でメインラインに入りましたが、現在公式には 6.18 以降メインラインカーネルから配布されなくなり、ツリー外 DKMS モジュールに切り替わりました。デプロイメントとアップグレード時には、モジュール、ツール、対象カーネル、ディスクフォーマットの互換性を同時に確認する必要があり、「6.7 以降のカーネルに付属」とだけ記述することはできません。bcachefs 公式説明 および 公式ソースリポジトリ を参照してください。
6. 能力の境界で比較し、固定の優勝者を出さない
メカニズム ext4 XFS Btrfs
典型的なメタデータ復旧方法 JBD2 ジャーナル メタデータジャーナル COW トランザクションと tree log などのメカニズム
ファイルデータの共有 通常のネイティブ reflink を提供しない reflink をサポートするフォーマットで使用可能 reflink とサブボリュームスナップショット
通常データのチェックサム 一般的にコンテンツチェックサムを提供しない 一般的にコンテンツチェックサムを提供しない 通常提供されるが、設定に依存する
スペース組織の注目点 ブロックグループ、extent、遅延割り当て AG、extent、複数の割り当てインデックス ブロックグループ、プロファイル、共有参照
容量の上限は、ブロックサイズ、フォーマット特性、カーネル実装、ツールバージョンに依存し、フォーマットの理論的なビット数をそのままユーザー利用可能な上限として記述すべきではありません。パフォーマンス比較では、データセット、並列性、同期頻度、空き容量、スナップショット状態を固定すべきです。短時間のバッファ書き込みはメモリへの吸収速度しか測定できないため、デバイスの持続能力を説明することはできません。
以下の純粋な計算例は、デバイスを修正せずに論理範囲のマッピングをチェックします:
extents = [ ( 100 , 8 , 900 ) , ( 108 , 4 , 3000 ) ]
def locate ( byte_offset , block_size = 4096 ) :
if byte_offset < 0 :
raise ValueError ( " negative offset" )
logical , within = divmod ( byte_offset , block_size )
for start , count , physical in extents :
if start <= logical < start + count :
return physical + logical - start , within
return None
assert locate ( 102 * 4096 + 17 ) == ( 902 , 17 )
assert locate ( 108 * 4096 ) == ( 3000 , 0 )
assert locate ( 112 * 4096 ) is None
print ( " extent 範囲と境界:通過" )
永続更新のアプリケーションプロトコルは VFS 応用 を参照、ストレージプール、TXG、ZIL は ZFS を参照してください。