WAL
Write-Ahead Logging ・ うぉる
変更をまずログへ追記し、データ本体への反映は後回しにして、停電後もログから復旧できるようにする仕組み。
✦ 30秒で試す — 航海本体への書き込みを後回しにするほうが、停電に強い再起動したとき、「成功」と応答したその変更は?予想してみる概要
WAL(Write-Ahead Logging=先行書き込みログ)は、データベースが変更を確定させるとき、データ本体のファイルを書き換える前に「何をどう変えるか」の記録を専用のログファイルへ追記する仕組みです。名前のとおり「本体より先に(ahead)ログを書く」という順序の規律がすべてで、この順序さえ守れば、書き込みの途中で電源が落ちてもログを読み返して変更をやり直せます。つまりトランザクションの永続性(ACID の D)を、クラッシュに対して保証できます。
WAL は PostgreSQL、MySQL(InnoDB。REDO ログと呼びます)、SQLite の WAL モードなど、ほぼすべての本格的なデータベースの心臓部にあります。同じ発想はファイルシステムのジャーナリング(ext4 や NTFS)や、Kafka のようなログ基盤にも現れていて、「まず追記、反映は後」という設計は永続化の技術の共通語になっています。
なぜ生まれたか
データベースの変更は、最終的にはディスク上のデータファイルのあちこちに散らばったページ(数 KB 単位の塊)の書き換えです。1つのトランザクションが複数のページを書き換える途中で落ちると、一部だけ反映された中途半端な状態が残ります。かといって「COMMIT のたびに関係する全ページをディスクへ書き切る」方式は、ランダムな位置への書き込みが大量に発生するので遅く、しかも1ページの書き込み自体が途中で切れる(torn write、ページの書きかけ)問題も残ります。
WAL はこのジレンマを「書き込みの性質を変える」ことで解きました。COMMIT のときに確実にディスクへ書くのはログへの追記だけにする。追記はディスクが最も得意なシーケンシャル書き込みで、1回の同期で複数のトランザクションをまとめて確定することもできます。データ本体への反映は急がず、メモリの上で書き換えておき、都合のよいときにまとめて書けばよい。遅いランダム書き込みを、速い追記と後回しの反映に変えたのです。
この原理は1970年代の IBM のデータベース研究の中で整理され(ジム・グレイの1978年の講義ノート「Notes on Data Base Operating Systems」に、先行書き込みの規則が書かれています)、その後の商用データベースに広がりました。1992年にはモハンらが ARIES と呼ばれる回復アルゴリズムを論文にまとめ、ログを使った REDO と UNDO の手順を体系化しました。現在の多くのデータベースの回復処理は、この ARIES の考え方を下敷きにしています。
詳細
書き込みの流れ — ログが常に先に行く
COMMIT までの流れを追ってみます。(1)トランザクションがデータを更新すると、変更はまずメモリ上のバッファプールに適用されます。バッファプールはデータページのキャッシュで、書き換えられてまだディスクに戻っていないページをダーティページと呼びます。(2)同時に「ページ X のこの位置を A から B に変更」というログレコードが WAL バッファに積まれます。(3)COMMIT のとき、WAL バッファの中身がログファイルへ追記され、fsync(OS のキャッシュに溜まった書き込みを物理ディスクまで押し出すよう求めるシステムコール)で永続化が確認されます。ここで初めてデータベースはクライアントに「COMMIT 成功」と応答します。(4)ダーティページは、後からバックグラウンドで少しずつデータファイルへ書き戻されます。
ここに WAL の規則がもう1つ隠れています。ダーティページをデータファイルへ書き戻せるのは、そのページへの変更を記したログレコードがディスクに届いた後だけです。この順序が守られていれば、データファイルにある変更は必ずログにもあるので、後から取り消すことも、やり直すこともできます。
図の要点は、COMMIT の必須経路(金の実線)がログの側にしかないことです。停電した瞬間にデータファイルがどれだけ古くても、ログさえ無事なら変更を再現できるので、遅いランダム書き込みを COMMIT の経路から追い出せます。
クラッシュリカバリ — ログを読み返してやり直す
停電の後に再起動したデータベースは、まず WAL を読みます。ARIES の手順では、ログを調べて「どのトランザクションが COMMIT まで届いていたか」を洗い出し(分析)、データファイルに届いていないかもしれない変更をログの順に当て直し(REDO)、最後に COMMIT の記録がないトランザクションの変更を後ろから順に取り消します(UNDO)。取り消しに使うのは、ログレコードに書かれた変更前の値です。PostgreSQL は MVCC の古い版を残しているので、明示的な UNDO をせず「COMMIT されなかった版は無効」と扱うだけで済ませる、という実装の違いはありますが、外から見える結果は同じです。
ここで効いてくるのが「ページの書きかけ」です。データファイルへの書き戻しの途中で電源が落ちると、1ページの前半だけが新しく、後半が古い、という壊れたページが残ることがあります。変更の差分だけを記したログでは、壊れたページの上に差分を当てても正しくなりません。PostgreSQL は、チェックポイントの後に初めて変更するページについて、ページ全体の写しを WAL に書いておきます(full_page_writes)。回復のときはその写しから丸ごと復元するので、書きかけのページも直ります。MySQL(InnoDB)は、同じ問題を doublewrite buffer という別の書き込み領域で防いでいます。
書き込みが流れている最中に、好きな瞬間で電源を抜いて確かめてください。メモリが消え、データファイルが古いまま(あるいは書きかけのまま)残り、再起動すると WAL が再生されて、COMMIT 済みの変更だけが戻ります。チェックポイントを押してから電源を抜くと、再生するログの長さが変わります。
3本のトランザクションが書き込みを続けています。変更はまずメモリに入り、COMMIT のときに WAL だけがディスクへ書かれます。好きな瞬間に「電源を抜く」を押してください。
4つのページと3本のトランザクションに単純化しています。実際の PostgreSQL は、チェックポイントの後に初めて変えるページの全体を WAL に書き(full_page_writes)、書きかけのページもそこから直します。
「ログを最初から再生すれば状態を復元できる」という性質は、イベントソーシングの発想とも深くつながっています。
fsync とチェックポイント — 性能を左右する2つのつまみ
WAL の性能と信頼性は、2つの設定でほぼ決まります。1つ目は fsync の頻度です。COMMIT ごとに毎回 fsync すれば最も安全ですが、fsync はディスクの物理書き込みを待つ高価な操作です。そこで、同じ瞬間に COMMIT した複数のトランザクションを1回の fsync に相乗りさせるグループコミットや、「直近の数百ミリ秒分の COMMIT を失う可能性を受け入れて、fsync を待たずに応答する」設定(PostgreSQL の synchronous_commit = off、MySQL の innodb_flush_log_at_trx_commit = 2 など)が用意されています。後者はデータベースが壊れることはありませんが、応答済みの COMMIT が停電で消えることがあります。永続性を何ミリ秒分まで賭けてよいかという、明示的な取引です。
なお fsync が本当にディスクまで届くかは、下の層しだいです。ディスク自体の書き込みキャッシュが電池で守られていないと、fsync が成功を返しても停電で失われることがあります。2018年には、fsync が失敗を返したあとに再試行すると成功に見えてしまう Linux の挙動が PostgreSQL の開発者によって問題にされ、PostgreSQL は fsync が失敗したら即座に停止する方針に改めました。
2つ目はチェックポイントです。ログは追記され続けるので、放っておくと伸び続け、回復のときに再生する量も増えます。そこで定期的に「この時点までのダーティページはすべてデータファイルへ書き戻した」という区切りを記録します。これより古いログは削除(または再利用)でき、回復はチェックポイント以降の再生だけで済みます。ただしチェックポイントのあいだは書き戻しの I/O が集中します。間隔を短くすれば回復は速いが平常時の負荷が増え、長くすればその逆です。チェックポイントのたびに性能が波打つのは、WAL を使うデータベースの典型的な運用課題で、PostgreSQL は書き戻しを時間をかけて散らす設定(checkpoint_completion_target)で波をならします。
レプリケーションへの応用 — ログは複製の言葉になる
WAL には「データベースへのすべての変更が、順序付きで1本の流れとして記録されている」という副産物があります。これはそのままレプリケーションの輸送手段になります。プライマリが WAL をレプリカへ流し、レプリカがそれを再生し続ければ、同じ状態の複製が保てます。PostgreSQL のストリーミングレプリケーションはまさにこの仕組みです(MySQL のレプリケーションは、REDO ログとは別の binlog という論理的な変更記録を流します)。WAL を外部のシステムが購読して変更イベントとして扱う CDC(Change Data Capture)は、検索インデックスやキャッシュ、ストリーム処理の基盤へデータを同期する定番の手法です。ベースバックアップに WAL を足し込み続けて任意の時点へ戻す PITR(ポイントインタイムリカバリ)も、ログがあればこそです。障害対策として生まれた1本のログが、複製・連携・バックアップの共通の土台になっています。
試問
画面では、3本のトランザクションがデータベースの4つのページを書き換え続けています。変更はまずメモリとログ(WAL)に入り、データファイルへの書き戻しは後回しです。「電源を抜く」で好きな瞬間に停電させ、「再起動する」で WAL から復旧させられます。「チェックポイント」(そこまでの変更をデータファイルへ書き戻す)と、COMMIT に応答するタイミングの設定も変えられ、いまは「fsync を待たずに(ディスクに届くのを待たずに)応答」になっています。
3本のトランザクションが書き込みを続けています。変更はまずメモリに入り、COMMIT のときに WAL だけがディスクへ書かれます。好きな瞬間に「電源を抜く」を押してください。
4つのページと3本のトランザクションに単純化しています。実際の PostgreSQL は、チェックポイントの後に初めて変えるページの全体を WAL に書き(full_page_writes)、書きかけのページもそこから直します。
