ファイルシステムのクラッシュ整合性とfsync保証
電源断でデータが壊れない仕組みと、fsyncが本当に守る範囲がわかります。ジャーナリング・CoW・ログ構造に共通する原理と、書き込み順序とバリアの正しい使い方まで整理します。
- 1.クラッシュ整合性は、複数ブロックへの非原子的な更新を、ログやCoWで「全部反映済みか、全く反映していないか」のどちらかに収束させて担保する。
- 2.fsyncはデータ+メタデータ、fdatasyncはデータと最小限のメタデータ、O_SYNCは各writeを同期化する。新規ファイルでは親ディレクトリのfsyncも要る。
- 3.すべての保証はデバイスキャッシュへのFLUSH/FUAと書き込み順序(バリア)が土台であり、これが崩れるとログの原子性ごと破綻する。
クラッシュ整合性とは何を守ることか
ファイルシステムへの1回の論理的な操作(追記、リネーム、ファイル作成)は、ディスク上では 複数の独立したブロック更新 に分解されます。ストレージはこれらをまとめて原子的に書く手段を持たないため、途中で電源が落ちれば一部だけが永続化された 中間状態 が残ります。
ここで区別すべきは2種類の整合性です。メタデータ整合性(ファイルシステム構造が矛盾しない、たとえば「inode が指すブロックが空きビットマップ上でも使用中になっている」)と、データ整合性(アプリが書いた中身がそのまま残る)です。多くのファイルシステムが既定で保証するのは前者だけで、後者は fsync などでアプリが明示的に要求しない限り守られません。「FS は壊れていないが、直前に書いた内容は消えた」は仕様どおりの挙動です。
クラッシュ整合性の核心は 原子性の擬似的な実現 です。本来非原子的な複数更新を、復旧後に「全部反映済み」か「全く反映していない」のどちらかにだけ収束させ、半端な中間状態を観測させない。これを達成する代表的な手法が、ジャーナリング・CoW・ログ構造の3つです。
手法1:ジャーナリング(先行書き込みログ)
ジャーナリングは「これからこう書く」という更新計画を専用のログ領域に 先に 記録し、本番領域へはその後で反映します。これは Write-Ahead Logging(WAL、先行書き込みログ)そのもので、DB のトランザクションログと原理を共有します。
1. 更新するブロック群をジャーナル領域へ書く
2. バリア(FLUSH/FUA)で 1 の永続化を待つ
3. コミットレコードを書き、トランザクション完了を確定する
4. 後で本番位置へ反映する(checkpoint)
整合性の鍵は 手順2と3の順序 です。コミットレコードより前にすべての更新が永続化済みであることをバリアで保証してからコミットを書くため、復旧時には二者択一だけが起こります。コミットレコードがある → 完全なので本番へ 再適用(redo) する。ない → 不完全なので 丸ごと破棄 する。半端な状態が原理的に発生しないため、fsck の全走査は不要になり、ログ再生だけで一貫性が戻ります。詳しい内部は ジャーナリングファイルシステムの内部(ext4/XFS) を参照してください。
ログにも本番にも書くため、最悪で書き込み量が2倍になります。ext4 の既定 data=ordered がメタデータだけをジャーナルし、データは本番へ先に書く順序制約で済ませるのは、この二重書き込みをデータ本体については避けるための妥協です。守る範囲と性能のトレードオフが本質です。
手法2:コピーオンライト(CoW)
CoW は既存ブロックを 決して上書きせず、変更を常に別の空き領域へ書きます。書き換えたブロックを指していた親ブロックも新しい位置へ書き直され、これがルートまで連鎖します。最後にスーパーブロック内のルートポインタを 1つだけ原子的に差し替える ことで、新旧どちらかの完全な木だけが見える状態を保ちます。
更新前: root → A → B(旧)
更新中: B'(新) を空き領域へ書く → A'(B'を指す) を書く
確定: root のポインタを A' へ差し替え(この1書き込みが原子的境界)
ジャーナリングと違い、CoW は「古い状態」が上書きされずに残るため、ログ再生という別工程が要りません。クラッシュ時はルート差し替えが起きたか否かだけで結果が決まります。同じ仕組みからスナップショット(古いルートを保持するだけ)が自然に導かれます。詳細は コピーオンライト型ファイルシステム(ZFS/Btrfs) を、原理としての CoW は コピーオンライト を参照してください。
手法3:ログ構造とWALの共通原理
ログ構造化ファイルシステム(LFS)は、すべての書き込みをディスク上の 一本の連番ログ に追記し続け、その場上書きを一切しません。最新のデータがログのどこにあるかは inode マップで引きます。書き込みが常に末尾追記になるため、ランダム書き込みがシーケンシャルに変換され、フラッシュメモリの FTL や一部の組み込み FS で採用されています。
ここまでの3手法は、表面上は別物に見えて 同じ不変条件 を共有しています。
| 手法 | 原子的に切り替わる境界 | 整合性の担保方法 |
|---|---|---|
| ジャーナリング | コミットレコードの永続化 | ログを先に書き、本番へはコミット後に反映(redo) |
| CoW | ルートポインタの差し替え | 旧ブロックを上書きせず、新しい木を別領域に構築 |
| ログ構造/WAL | 新レコードの追記完了 | 末尾追記のみ。最新位置はインデックスで管理 |
共通原理は3つです。第一に その場上書きを避ける(ログへ追記、または別領域へ書く)。第二に 単一の原子的書き込みで新旧を切り替える(コミットレコード、ルートポインタ)。第三に その原子的書き込みより前に依存物の永続化を強制する(バリア)。WAL がトランザクション境界をログレコードで確定するのと、CoW がルート差し替えで確定するのは、抽象的には同じ「一点の原子的コミットへ全更新の見え方を集約する」設計です。
fsync / fdatasync / O_SYNC の保証範囲
通常の write は ページキャッシュ に書いて即座に返るだけで、ストレージには届いていません。永続化を要求する手段が同期系のシステムコールです。それぞれ守る範囲が異なります。
| 呼び出し | データ本体 | メタデータ | 粒度・特徴 |
|---|---|---|---|
| fsync(fd) | 永続化する | サイズ・時刻・ブロックマップ等すべて | そのファイル1つ。標準的な永続化手段 |
| fdatasync(fd) | 永続化する | データ取得に必須な分のみ(更新時刻等は省略可) | メタデータI/Oを削り、追記サイズ変化があれば結局書く |
| O_SYNC(open時) | 各writeで永続化 | 各writeでメタデータも | writeごとに同期。fsync相当を毎回実行 |
| O_DSYNC(open時) | 各writeで永続化 | 必須分のみ | fdatasync相当を毎回実行 |
fdatasync の境界は誤解されやすい点です。「メタデータを書かない」のではなく、「書いたデータを後で正しく読み出すために必須のメタデータは書く」が正確です。たとえばファイルを末尾に追記してサイズが伸びた場合、新しいサイズを永続化しないとそのデータは読めないため、fdatasync でもサイズは書きます。省けるのは更新時刻(mtime)のように、データの読み出しに無関係なメタデータだけです。
ファイルを新規作成して fsync しても、永続化されるのはそのファイルのデータと inode だけです。「そのディレクトリにこのファイルが存在する」というディレクトリエントリは別のメタデータで、これが永続化されていないとクラッシュ後にファイル自体が見えません。新規作成や rename の後は、親ディレクトリを開いて fsync する のが正確な作法です。
クラッシュセーフな原子的差し替えの定石:
fd = open("data.tmp", O_CREAT|O_WRONLY)
write(fd, ...) # ページキャッシュに書くだけ
fsync(fd) # data.tmp のデータ+メタデータを永続化
close(fd)
rename("data.tmp","data") # 同一FS内なら原子的に差し替え
dfd = open(親ディレクトリ)
fsync(dfd) # rename(ディレクトリ更新)を永続化
この「一時ファイルへ書く → fsync → 原子的 rename → ディレクトリ fsync」は、アプリ側で WAL と同じ原理を再現しています。rename の原子性が「コミットレコード」に相当し、それ以前の状態は不完全として無視できます。
書き込み順序とバリア:保証の土台
これらすべての保証は、指定した順序でストレージに永続化されること を前提にしています。ところが現代のディスクや SSD は内部に揮発キャッシュを持ち、OS が出した順とは違う順で媒体へ書くことがあります(リオーダリング)。これを放置すると、コミットレコードがデータ本体より先に永続化され、ジャーナルの原子性が崩れます。
そこで FS は FLUSH(デバイスキャッシュ全体を媒体へ追い出す)と FUA(Force Unit Access、その書き込みだけ媒体へ直接) をコミットの前後に発行し、ハードウェア層に順序を強制します。fsync が「確実に永続化」と言えるのは、最終的にこの FLUSH/FUA まで到達させるからです。これが効かなければ「fsync は返ったがデバイスキャッシュ内に留まり、電源断で消えた」事故が起こります。
性能目的でバリアを無効化したり、電源保護(バッテリ・キャパシタ)のないストレージでライトキャッシュを有効にしたままだと、順序保証が崩れ、クラッシュ時にFSが破損し得ます。ジャーナリングもCoWも「バリアが正しく効いている」前提の上に成り立つ保証です。さらに fsync のエラー(特に書き戻し失敗時のエラー報告とダーティページの扱い)には実装差があり、エラーを無視して再試行すると黙ってデータを失う「fsync問題」も知られています。
(1)クラッシュ整合性は非原子的な複数更新を「全部か無か」に収束させること。(2)ジャーナリング=WAL、CoW=ルート差し替え、ログ構造=追記、はいずれも単一の原子的コミットへ見え方を集約する同一原理。(3)fsyncはデータ+メタデータ、fdatasyncは読み出しに必須なメタデータのみ、O_SYNCは各writeを同期化。(4)新規ファイルは親ディレクトリのfsyncが必要。(5)すべてはFLUSH/FUAによる順序保証が土台。この5点が頻出です。
まとめ
- クラッシュ整合性 は、本来非原子的な複数ブロック更新を、復旧後に「全部反映済み」か「全く反映していない」のどちらかへ収束させ、半端な中間状態を観測させない仕組み。
- ジャーナリング(WAL)・CoW・ログ構造 は表面上別物だが、その場上書きを避け、単一の原子的書き込みで新旧を切り替え、それ以前に依存物の永続化を強制する、という共通原理を持つ。
- fsync はデータ+メタデータ、fdatasync は読み出しに必須なメタデータのみ、O_SYNC/O_DSYNC は各 write を同期化する。新規ファイルでは親ディレクトリの
fsyncも必要。 - すべての保証は FLUSH/FUA によるデバイスキャッシュのフラッシュと書き込み順序(バリア) が土台であり、これが崩れると上層のログやCoWの原子性ごと破綻する。基礎は ファイルシステム も参照してください。
OS Article
ファイルシステムのクラッシュ整合性とfsync保証を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ファイルシステム
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
fsyncはデータ+メタデータ、fdatasyncはデータと最小限のメタデータ、O_SYNCは各writeを同期化する。新規ファイルでは親ディレクトリのfsyncも要る。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ファイルシステム / fsync」に近いか確認する。
- 強みである「クラッシュ整合性は、複数ブロックへの非原子的な更新を、ログやCoWで「全部反映済みか、全く反映していないか」のどちらかに収束させて担保する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。