ページキャッシュとライトバックの仕組み
ファイルI/Oが速い理由はカーネルがメモリに溜め込んでいるから。ページキャッシュとライトバックの原理を押さえれば、fsyncが守る範囲とデータが消える条件まで腑に落ちます。
- 1.ページキャッシュは、ファイルの中身をページ単位でメモリに保持する仕組み。readは多くがメモリヒットで済み、writeはまずキャッシュに溜めて即座に返る(ライトバック)。
- 2.書き換えられたダーティページは、flusherスレッドがバックグラウンドで遅延書き戻しする。dirty_ratioなどの閾値とタイマで、メモリ量と書き戻しタイミングが決まる。
- 3.ライトバックは速いがクラッシュでデータを失う窓がある。fsyncはそのファイルのダーティデータとメタデータをストレージへ確実に届け、永続性を保証する。
ページキャッシュとは:ファイルとメモリの間のバッファ
ファイルを read したとき、毎回ディスクまで取りに行っているわけではありません。カーネルは一度読んだファイルの中身を ページ単位(典型的には 4KB)でメモリに保持 しておき、次に同じ箇所が要求されたらメモリから返します。この透過的なキャッシュ層が ページキャッシュ(page cache) です。
ページキャッシュは「使われていない物理メモリを遊ばせない」という思想で動きます。空きメモリはディスクの内容で埋められ、必要になれば即座に解放できる 回収可能な(reclaimable) メモリとして扱われます。free コマンドで buff/cache が大きいのはこのためで、これは無駄遣いではなく、I/O を肩代わりしている有用なメモリです。
キャッシュに使われたメモリは、アプリが要求すれば破棄して譲られます。つまりページキャッシュが大きいこと自体は問題ではありません。free の available 列が、回収可能なキャッシュを差し引いた「実際にアプリが使える量」を示します。
ファイル全体の論理アドレス空間(ファイルオフセット)は、カーネル内部では address_space という構造で表現され、各ページがファイルのどのオフセットに対応するかが管理されます。mmap でファイルを貼り付けたときに触れるのも、まさにこのページキャッシュ上のページです。仕組みは メモリマップトファイル(mmap) を参照してください。
読みと書きのデータ経路
ページキャッシュを挟むことで、読みと書きの経路は次のようになります。
read(fd, buf, n):
該当オフセットのページが page cache に在る?
在る → メモリからユーザバッファへコピー(キャッシュヒット、I/Oなし)
無い → ディスクから1ページ読み込み→cacheに載せ→コピー(キャッシュミス、I/O)
write(fd, buf, n):
該当ページを page cache 上に用意し、ユーザのデータで書き換える
そのページに「ダーティ(dirty)」の印を付ける
→ ここで write は返る(まだディスクには書いていない!)
読みのミスは デマンドページングとページフォルト処理 のメジャーフォルトと同じ構造で、足りないページを先読み(readahead)でまとめて取り込む最適化も共通です。
注目すべきは書きの経路です。write が成功して返った時点では、データは メモリ上のページキャッシュに書かれただけ で、ストレージにはまだ届いていません。書き換えられたページは ダーティページ(dirty page) として印が付き、後でまとめてディスクへ書き戻されます。この「書きを遅延させてメモリに溜める」方式が ライトバック(write-back) です。
| 方式 | writeが返る時点 | 性能 | クラッシュ耐性 |
|---|---|---|---|
| ライトバック(既定) | キャッシュに書いた直後 | 高い(I/Oをまとめ遅延できる) | 書き戻し前ならデータ消失の窓あり |
| ライトスルー | ストレージへ書き終えた後 | 低い(毎回I/O待ち) | 返った時点で永続化済み |
なぜライトバックが速いのか
ライトバックの利得は、単に「待たない」ことだけではありません。書きを遅延してまとめることで、 I/O 回数そのものを減らせます。
- 書き込みの併合(write coalescing):同じページを短時間に何度も書き換えても、ディスクへの書き戻しは最終状態の1回で済む。ログのように同じ箇所を更新し続けるワークロードで効く。
- 隣接I/Oの結合:連続したオフセットのダーティページを1回の大きな I/O にまとめると、シークやコマンド発行の固定コストを償却できる。
- 無駄な書き込みの消去:書いた直後に削除・上書きされたデータは、ディスクへ一度も書かれずに消えることがある。
これらは、書きを 即座に永続化しない からこそ可能です。裏を返せば、ライトバックは性能と引き換えに クラッシュ時にデータを失う窓 を抱えています。電源断やカーネルパニックで、まだ書き戻されていないダーティページはメモリと一緒に消えます。この窓を制御するのが、次に述べる書き戻しのタイミングと fsync です。
ダーティページの書き戻し:flusher の役割
ダーティページを実際にストレージへ書き出す処理が ライトバック(書き戻し) で、Linux では専用のカーネルスレッド群が担います。歴史的に pdflush、bdflush と呼ばれ、現在は flusher スレッド(バッキングデバイスごとに動く writeback ワーカー)が受け持ちます。アプリの write とは非同期に、バックグラウンドで動くのが要点です。
flusher が書き戻しを始めるトリガは大きく二系統あります。
- 量による契機:ダーティページが一定割合まで溜まったら書き戻す。溜め込みすぎを防ぐ。
- 時間による契機:古くなったダーティページを期限切れとして書き戻す。溜めたまま放置しない。
この二つを制御するのが vm.* の sysctl パラメータです。代表的なものを押さえます。
| パラメータ | 意味 | 契機の種類 |
|---|---|---|
| vm.dirty_background_ratio | この割合を超えると flusher がバックグラウンド書き戻しを開始(アプリは止めない) | 量(ソフト閾値) |
| vm.dirty_ratio | この割合に達すると、書こうとするプロセス自身が書き戻しに参加させられ write がブロックする | 量(ハード閾値) |
| vm.dirty_expire_centisecs | ダーティになってからこの時間が経ったページを「古い」とみなし書き戻し対象に | 時間(期限) |
| vm.dirty_writeback_centisecs | flusher が起きて古いページを探す周期 | 時間(周期) |
*_ratio はシステムの利用可能メモリに対する ダーティページの割合(パーセント)です(バイト指定版の *_bytes もあり、そちらを設定すると ratio は無効になります)。
dirty_ratio が決める書き戻しの圧力
dirty_background_ratio と dirty_ratio は 二段階のブレーキ として働きます。挙動を順に追います。
ダーティ割合の上昇に伴う挙動:
0% ───────────────────────────────────────────────► dirty_ratio (例: 20%)
▲ ▲
dirty_background_ratio (例: 10%) │
│ │
ここまでは溜める │ flusherが背後で書き戻し開始 │ アプリ自身が
(writeは即返る) │ (writeはまだ止まらない) │ 同期的に書き戻し
│ → writeがブロック
dirty_background_ratio未満:ダーティページは溜まる一方で、writeは即座に返る。最も速い領域。dirty_background_ratio到達:flusher がバックグラウンドで書き戻しを始める。書き戻しが書き込みに追いつけば、ダーティ割合はこのあたりで均衡する。アプリのwriteはまだブロックしない。dirty_ratio到達:溜まりすぎ。これ以上メモリを汚させないため、書こうとしたプロセス 自身が書き戻しに従事させられ、writeがブロックする。これを スロットリング(throttling) と呼ぶ。
つまり dirty_ratio は ダーティページがメモリを食い尽くすのを防ぐ上限 であり、ここに張り付くと書き込みレイテンシが跳ね上がります。一方 dirty_background_ratio は その手前で背後の書き戻しを起動するソフト閾値 です。書き戻し帯域がストレージの実力で頭打ちになっている状態でバースト書き込みが続くと、dirty_ratio に到達してアプリが周期的にストール(IO 待ちで詰まる)するのが典型的な症状です。
write が成功して返っても、データはまだメモリ上です。dirty_expire_centisecs(既定はおおむね30秒相当)が経つか、量の閾値に達するまで、書き戻しは始まらないことすらあります。この間に電源が落ちれば、書いたはずのデータは失われます。永続化が必要なら、後述の fsync で明示的にストレージへ届けなければなりません。
fsync の保証範囲:どこまで永続化されるのか
ライトバックが抱える「データ消失の窓」を、アプリ側で閉じる手段が fsync です。fsync(fd) は、そのファイルディスクリプタが指すファイルの ダーティなページキャッシュと関連メタデータを、ストレージデバイスへ確実に書き出す ことを要求し、完了するまでブロックします。
ここで「確実に」の中身を正確に押さえる必要があります。fsync が保証する範囲には段階があります。
| 呼び出し | データ本体 | メタデータ(サイズ/タイムスタンプ等) | 用途 |
|---|---|---|---|
| fsync(fd) | 書き戻す | 書き戻す | データもメタデータも確実に永続化したい標準手段 |
| fdatasync(fd) | 書き戻す | データ整合に必須な分のみ(更新時刻などは省略可) | メタデータ更新を省きI/Oを軽くしたい場合 |
| sync() | 全ファイルのダーティを書き戻す(完了待ちは実装依存) | 同左 | システム全体を一括フラッシュ |
fsync の重要な性質を整理します。
- 対象は1ファイル:
fsync(fd)はそのファイルに紐づくダーティデータとメタデータだけを書き戻します。他のファイルやシステム全体ではありません。 - メタデータも含む:ファイルを新規作成して書き込んだ場合、データを書き戻すだけでは不十分です。ディレクトリエントリ(ファイルがそこに存在するという事実)が永続化されていなければ、クラッシュ後にファイル自体が見えないことがあります。新規ファイルでは、そのファイルの
fsyncに加えて親ディレクトリのfsyncが必要になるのが正確な作法です。メタデータの一貫性の土台は ファイルシステム を参照してください。 - デバイスキャッシュまで:
fsyncはストレージデバイスの揮発キャッシュ(ディスク側の書き込みキャッシュ)に対するフラッシュ(FLUSH/FUA)まで含めて、媒体へ到達させることを意図します。これが効かなければ「fsyncは返ったがデバイスキャッシュ内で消えた」事故が起こり得ます。
クラッシュセーフな新規ファイル作成の定石:
fd = open("data.tmp", O_CREAT|O_WRONLY)
write(fd, ...) # ページキャッシュに書くだけ
fsync(fd) # data.tmp のデータ+メタデータを永続化
close(fd)
rename("data.tmp","data") # 原子的に差し替え
dfd = open(親ディレクトリ)
fsync(dfd) # rename(ディレクトリ更新)を永続化
よくある誤りが3つあります。(1)データだけ書き戻して親ディレクトリを fsync せず、クラッシュ後にファイルが消える。(2)write の戻り値だけ見て永続化されたと誤認する(実際はキャッシュにあるだけ)。(3)一部のストレージやファイルシステム設定で書き込みキャッシュのフラッシュが無効化され、fsync の保証が崩れる。永続性を語るときは「どこまで届いたか」を常に意識します。
ページキャッシュの回収との関係
ダーティページは、メモリが逼迫したときの回収(reclaim)とも絡みます。クリーンな(ディスクと一致した)キャッシュページは、いつでも捨てて再利用できます。一方 ダーティページは捨てる前に書き戻しが必須 です。書き戻していないページを破棄すれば、その変更が失われるからです。
そのため、メモリ回収の際にダーティページが多いと、回収のために書き戻し I/O が発生し、メモリ確保のレイテンシが悪化します。dirty_ratio を低めに保つと、回収時に書き戻すべきダーティページが少なく、回収がスムーズになる傾向があります。回収そのものの選別ロジックは ページ置換アルゴリズム(LRU・Clock・WSClock) を参照してください。
(1)write が返った時点でデータはページキャッシュにあるだけで、まだ永続化されていないこと。(2)ライトバックが速い理由(併合・結合・無駄書きの消去)と、その代償(消失の窓)。(3)dirty_background_ratio=背後書き戻し開始のソフト閾値、dirty_ratio=writeをブロックさせるハード閾値、という二段ブレーキ。(4)fsync は対象ファイルのデータ+メタデータを書き戻し、新規ファイルでは親ディレクトリの fsync も要ること。この4点が頻出です。
まとめ
- ページキャッシュ は、ファイルの中身をページ単位でメモリに保持する透過的なバッファ。
readの多くはメモリヒットで済み、空きメモリは回収可能なキャッシュとして活用される。 writeは ライトバック 方式で、まずキャッシュに書いて即座に返り、書き換えたページは ダーティ として印が付く。I/O の併合・結合・消去で速いが、書き戻し前のデータはクラッシュで失われる。- ダーティページは flusher スレッド が非同期に書き戻す。
dirty_background_ratio(背後書き戻し開始)とdirty_ratio(writeをブロック)が量の二段ブレーキ、dirty_expire_centisecs/dirty_writeback_centisecsが時間の契機を決める。 fsyncは対象ファイルのダーティデータとメタデータをストレージへ確実に届けて永続性を保証する。新規ファイルでは親ディレクトリのfsyncも必要で、デバイスキャッシュまでフラッシュして初めて媒体に届く。
土台は メモリマップトファイル(mmap) と デマンドページングとページフォルト処理、永続性の前提は ファイルシステム、回収側の論理は ページ置換アルゴリズム(LRU・Clock・WSClock) も合わせて読むと、ファイルI/Oの全体像が繋がります。
OS Article
ページキャッシュとライトバックの仕組みを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ページキャッシュ
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
書き換えられたダーティページは、flusherスレッドがバックグラウンドで遅延書き戻しする。dirty_ratioなどの閾値とタイマで、メモリ量と書き戻しタイミングが決まる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ページキャッシュ / ライトバック」に近いか確認する。
- 強みである「ページキャッシュは、ファイルの中身をページ単位でメモリに保持する仕組み。readは多くがメモリヒットで済み、writeはまずキャッシュに溜めて即座に返る(ライトバック)。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。