二重書き込みバッファとトーンページ問題
電源断でページが半分だけ書かれても DB が壊れないのはなぜか。InnoDB の二重書き込みバッファと PostgreSQL の full-page write の原理と性能コストを、torn page の根本から解き明かします。
- 1.DB のページ(8〜16KB)はストレージのセクタ(512B/4KB)より大きく、書き込み途中の電源断でページが新旧混在の torn page になりうる。WAL の redo は「ページの一部が壊れていない」前提なので、torn page はリカバリを破綻させる。
- 2.InnoDB は doublewrite buffer で同じページを連続領域へ一度書いてから本来位置へ書き、PostgreSQL はチェックポイント後に各ページが最初に汚れたとき full-page write で WAL へページ全体を載せ、torn page を検知・上書き再生する。
- 3.どちらも「同じデータを二度書く」ため書き込み量と WAL 量が増えるコストを払う。アトミック書き込み対応ストレージなら doublewrite を、ブロックサイズ整合なら一部の保護を省ける。
torn page はなぜ起きるのか
DBMS が読み書きする最小単位は ページで、InnoDB は既定 16KB、PostgreSQL は 8KB です。一方、ストレージがアトミックに(不可分に)書ける単位は伝統的な HDD で 512B、いまどきの SSD でも物理セクタ 4KB 程度にすぎません。つまり 1 ページの書き戻しは、内部的には複数セクタへの書き込みに分解されます。
ここに電源断が刺さると問題が起きます。ページの前半セクタは新しい内容、後半セクタは古い内容、という新旧が混ざった状態でディスクに残ることがあるのです。これが torn page(破れたページ、部分書き込み) です。
torn page はストレージの読み書き自体は正常で、「複数セクタの書き込みが途中で止まった」結果生じる整合性の崩れです。媒体劣化や宇宙線によるビット反転(これは バックアップとリカバリ で扱うチェックサム検知の領域)とは原因が異なります。torn page は正しく動くハードウェアでも、電源断と「ページ > アトミック単位」という構造だけで必然的に発生します。
なぜ WAL だけでは防げないのか
「WAL(先行書き込みログ) があるなら、クラッシュ後に redo すれば直るのでは」と思うかもしれません。しかし WAL の redo は多くの場合物理論理(physiological)ログ、すなわち「このページのこのオフセットをこう書き換える」という差分を記録します。差分 redo が成立する前提は、再生の起点となるページが矛盾のない既知の状態であることです。
torn page はこの前提を破壊します。ページの一部が新・一部が旧という中途半端な状態に、さらに差分を当てても正しいページには戻りません。ページヘッダの pageLSN(最後に適用された WAL の LSN)すら、torn によって信用できなくなります。
正常: [旧ページ] --(redo差分を適用)--> [新ページ] ← 起点が既知なので正しく直る
torn: [前半=新 / 後半=旧] --(redo差分)--> [?壊れたページ] ← 起点が不定なので直らない
要するに redo は「ページ全体が壊れていないこと」を暗黙に仮定しており、torn page 対策はその仮定を物理層で保証するための別レイヤーです。解決の方針は一つ、「ページ全体を、torn の影響を受けない形でどこかにもう一部持っておく」ことです。
InnoDB: doublewrite buffer
InnoDB の答えが doublewrite buffer(二重書き込みバッファ) です。汚れたページをデータファイルの本来位置へ書き戻す前に、まずディスク上の連続した専用領域(doublewrite 領域)へそのページ群をまとめて書き、fsync します。そのうえで初めて、各ページを本来のランダムな位置へ書き、再度 fsync します。
書き込みは必ず「doublewrite 領域 → 本来位置」の順なので、クラッシュ時に壊れうるのはどちらか一方だけです。リカバリ時は各ページのチェックサムを検証し、本来位置のページが torn(チェックサム不一致)なら、doublewrite 領域にある無事なコピーで上書きします。
| タイミング | doublewrite 領域 | 本来位置 | リカバリの判断 |
|---|---|---|---|
| doublewrite 書き込み中に断 | torn の可能性 | 旧データのまま無事 | 本来位置が無事なので何もしない |
| 本来位置への書き込み中に断 | 正しいコピーが存在 | torn の可能性 | doublewrite のコピーで本来位置を修復 |
| 両方完了後に断 | 正しいコピー | 正しいデータ | 整合済み、修復不要 |
ポイントは、doublewrite 領域は連続領域へのシーケンシャル書き込みなので、ランダムな本来位置への書き込みに比べてコストが相対的に小さいことです。それでも全ページを 2 回書く以上、書き込み I/O 量はおおむね倍増します。
ストレージやファイルシステムがページサイズ単位のアトミック書き込みを保証する場合(一部 SSD の Atomic Write、Fusion-io 由来の機能など)、torn page は原理的に起きないため doublewrite は不要になり、無効化して I/O を半減できます。MySQL 8.0 系では doublewrite のファイル分離や並列化も入り、コストの軽減が図られています。
PostgreSQL: full-page write
PostgreSQL は別アプローチの full-page write(FPW、全ページ書き込み) を採ります。チェックポイント直後にそのページが最初に変更されるとき、通常の差分 WAL レコードの代わりに、変更後のページイメージ全体を WAL に書き込みます(同一チェックポイント区間で 2 回目以降の変更は通常の差分でよい)。
狙いは redo の起点を WAL 内に確保することです。クラッシュリカバリの redo 中に torn page を踏んでも、その区間の最初の FPW レコードがページ全体の正本を持っているため、ディスク上の torn なページをまるごと上書きして整合状態に戻し、そこから差分 redo を続けられます。「torn を検知して直す」のではなく、「torn かどうかに関わらず WAL の全ページで一旦塗り直す」のが本質です。
FPW はチェックポイントごとにリセットされるため、チェックポイント直後はほぼすべての更新がページ全体を WAL へ載せ、WAL 生成量が一時的に跳ね上がります。チェックポイント間隔を広げると FPW の発生頻度は下がり WAL 量は減りますが、その分リカバリ再生範囲が伸びます(WAL のチェックポイント・トレードオフがそのまま効きます)。full_page_writes は torn 耐性が確証できる環境以外で無効化してはいけません。
二つの設計の比較と性能コスト
両者は「ページ全体の正本を別の場所に持つ」点で同じですが、正本の置き場所が決定的に違います。
| 観点 | InnoDB doublewrite | PostgreSQL full-page write |
|---|---|---|
| 正本の保存先 | 専用の doublewrite 領域(固定サイズ・上書き循環) | WAL ストリーム内(FPW レコードとして) |
| 書く契機 | 汚れたページを本来位置へ書き戻すたび | チェックポイント後に各ページが最初に汚れたとき |
| 主なコスト | データファイルへの書き込み量がほぼ倍増 | WAL 生成量の増加(特にチェックポイント直後) |
| 副次効果 | WAL は差分のままで軽い | WAL が膨らみアーカイブ/レプリカ転送量も増える |
どちらも本質的に「同じデータを二度書く」ため、RUM 予想 でいう write amplification(書き込み増幅) を払って読み取り時の整合性(torn 耐性)を買っている、と整理できます。InnoDB はその重さをデータファイル側に、PostgreSQL は WAL 側に寄せています。
「なぜ WAL があるのに torn page 対策が別途要るのか」――WAL の差分 redo は再生起点ページが無傷であることを仮定するから。「InnoDB と PostgreSQL の違いは」――前者は本来位置への書き戻し前に専用領域へ二度書きし torn を検知・修復、後者はチェックポイント後の初回更新で WAL にページ全体を載せ無条件に上書き再生。どちらも write amplification がコスト、と答えられれば十分です。
まとめ
torn page は「DB のページがストレージのアトミック書き込み単位より大きい」という構造から、正常なハードウェアでも電源断だけで生じます。WAL の差分 redo は無傷なページを起点に仮定するため、torn 対策は物理層の別レイヤーとして必要です。InnoDB は doublewrite buffer で本来位置の手前に二度書きして torn を修復し、PostgreSQL は full-page write でチェックポイント後の全ページを WAL に載せて塗り直します。いずれも書き込み増幅という対価を払って永続性(WAL が支える「D」)を本当に成立させる、最後の一歩です。ストレージのアトミック書き込みが保証できるなら、この対価の一部は安全に省けます。
データベース Article
二重書き込みバッファとトーンページ問題を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
InnoDB
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 6
導入後に効く点
InnoDB は doublewrite buffer で同じページを連続領域へ一度書いてから本来位置へ書き、PostgreSQL はチェックポイント後に各ページが最初に汚れたとき full-page write で WAL へページ全体を載せ、torn page を検知・上書き再生する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 6
判断チェックリスト
- 自社の用途が「InnoDB / PostgreSQL」に近いか確認する。
- 強みである「DB のページ(8〜16KB)はストレージのセクタ(512B/4KB)より大きく、書き込み途中の電源断でページが新旧混在の torn page になりうる。WAL の redo は「ページの一部が壊れていない」前提なので、torn page はリカバリを破綻させる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。