Write-Ahead Logging:それがどのようにあなたのデータを保護するか
IBM の ARIES から今日まで。先に順序ログを書いてからデータページを書き込む方式が停電に耐えられる理由、checkpoint と group commit がそれぞれ何を解決するのか、物理 WAL と論理 WAL の違い、よくある 2 つの誤用(長いトランザクションを閉じない、速度のために fsync を切る)を扱う。 commit
日本語
コピー

先行書き込みログ:あなたのデータをどう守るか
概要
- PRAGMA journal_mode=WAL; の一行で SQLite の先行書き込みログモードが有効になる。
- すべての書き込みはまずログに記録され、その後でデータファイルに適用される。
- PostgreSQL の pg_current_wal_lsn() はログの正確な位置をいつでも明らかにする。
- チェックポイントのチューニングを誤ると .wal ファイルが増え続け、データベース本体より多くの容量を占めるまでになる。
- PostgreSQL のストリーミングレプリケーションとは、要するに WAL を各レプリカに送ることにほかならない。
- SQLite の WAL モードでは、単一の書き手がデータを変更する間も並行して読み取りができる。
- fsync=off は速度と引き換えに、突然の電源断のたびにデータ破損を招く。
- SHOW wal_level; と PRAGMA wal_checkpoint(TRUNCATE); でログの実際の状態を確認できる。
先行書き込みログとは何か、なぜ存在するのか
船の航海日誌を思い浮かべてほしい。船長は正式な海図に最終位置を更新する前に、その時々の操作をすべて書き留める。時刻、針路、風向。航海の途中で問題が起きても、この日誌があれば何が起きたか、船が最終的にどこにいたかを正確に再現できる。先行書き込みログを備えたデータベースも、あらゆる変更に対して同じことをする。まず変更を順序どおりのログに記録し、それからようやく最終的なデータページに手を付ける。 解決すべき問題は口で言うのは簡単だが、実際にやるのは難しい。ひとつのトランザクションがメモリ上の複数のページを変更し、それらのページがディスクに書き込まれる前に OS が再起動するかもしれない。変更を事前に記録していなければ、ディスク上に半分だけ書かれたページが、コミット済みトランザクションのものなのか、中断されたトランザクションのものなのか、それとも両者が混ざった破損の結果なのか、判断する手立てがない。 IBM の研究者が発表した ARIES アルゴリズムは 1992 年、この方式を定式化した。変更はまずシーケンス番号(LSN、ログシーケンス番号)付きのログレコードとして記述され、トランザクションがクライアントに確認を返す前に、このレコードは fsync によってディスクに強制書き込みされなければならない。その後でエンジンは、変更されたページをデータファイルへゆっくりと適用する。プロセスが途中でクラッシュしても、ログにはコミット済みトランザクションをやり直し、中途半端なトランザクションを取り消すのに十分な情報が残っている。 素朴な代替案は、変更されたページを直接書き込み、都合の悪い瞬間に故障しないことを祈るというものだ。1980 年代にそうしていたエンジンは、タイミングの悪い電源断のたびにデータを失った。だからこそ先行書き込みログは、PostgreSQL、MySQL、SQLite、そしてほぼすべての現代のリレーショナルデータベースで事実上の標準になった。
内部の仕組み
この流れには三つの登場人物がいる。書き込みを要求するアプリケーション、ログ(追記のみで成長する一連のファイル)、そして最終的なデータファイルだ。トランザクションをコミットすると、エンジンは正確な変更を記述したレコードを組み立て、増加する LSN を割り当て、現在のログの末尾に追記する。このレコードが永続的にディスクへ書き込まれて初めて、エンジンはアプリケーションにコミット成功を返す。
変更されたデータページはまずメモリ上のバッファプールに留まり、後でディスクへ書き戻される。この処理を checkpoint と呼ぶ。checkpoint はメモリ上のダーティページをたどってデータファイルへ書き出し、どの LSN まで適用済みかをログに記録する。これが再起動時に再生すべきログの量を決める。最初のレコードからすべての履歴をやり直す必要はなく、復旧は最後に確認された checkpoint から始まる。
次の図は、アプリケーションが要求を発行してから checkpoint が最終ファイルへ書き出すまでの、1 回の書き込みの流れをまとめたものだ。
sequenceDiagram
participant App as Application
participant Log as Write-Ahead Log
participant Disk as Data file
App->>Log: requests to write a change
Log-->>App: confirms after fsync
Note over Log,Disk: the checkpoint runs in the background
Log->>Disk: applies the modified page
この方式が速いのは、fsync がログにしか触れず、ログは順次書き込みなのでコストが低いからだ。データページはファイル内のどこに散らばっていてもよく、後回しに更新できる。確認を待つクライアントを待たせる必要はない。

ログは常にファイル末尾に追記され、途中を書き換えることはない。画像は Ales Krivec による Unsplash 掲載のもの
実際の例
SQLite は先行書き込みログを、デフォルトのジャーナルモードに代わる任意のモードとして実装している。有効にするのは一文だけだ。
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
最初の行はジャーナルモードを変える。SQLite は元のページをロールバックファイルにコピーしてから変更するのではなく、変更を別の name.db-wal ファイルに書き込み、メインデータベースは次のチェックポイントまで変わらないままになる。二行目は fsync のレベルを緩める。WAL モードでは synchronous=FULL が要求する厳しい保証はもう必要ないからだ。SQLite のマニュアル自身が、この組み合わせをほとんどのアプリケーションにとって推奨の構成として挙げている。
もう少し現実に即した例を挙げよう。あるアプリケーションが行を挿入している最中に、別のプロセスが同じファイルを読んでいる。従来のジャーナルモードなら、これは読み手をブロックする。
import sqlite3
con = sqlite3.connect("inventario.db")
con.execute("PRAGMA journal_mode=WAL;")
con.execute(
"INSERT INTO productos (nombre, stock) VALUES (?, ?)",
("mechanical keyboard", 120),
)
con.commit()
con.close()
そのプロセスが書き込んでいる間、別のプロセスは同じデータベースを開いて SELECT を実行でき、待たされることはない。WAL モードでは、読み手には書き込みが始まる前のファイルの一貫したスナップショットが見えており、書き込みによってブロックされない。
PostgreSQL では WAL は任意ではなく常に有効だが、記録の詳しさは設定できる。ストリーミングレプリケーションを行うには、wal_level を最低レベルより引き上げる必要がある。
-- in postgresql.conf
wal_level = replica
max_wal_senders = 5
wal_keep_size = 512MB
wal_level を replica に設定すると、各ログレコードは、レプリカサーバーが元のデータファイルにアクセスせずに同じ状態を再構築するのに十分な情報を含むようになる。
はじめかた:WAL を段階的に有効化して検証する
SQLite の手順は単純だ。接続ごとに PRAGMA を一度実行し、結果を確認する:
sqlite3 inventario.db "PRAGMA journal_mode=WAL;"
-- expected response: wal
sqlite3 inventario.db "PRAGMA journal_mode;"
-- confirms it's still wal after reconnecting
データベースの隣に新しいファイルが 2 つ現れる。inventario.db-wal は書き込み待ちの変更を保持し、inventario.db-shm は読み手と書き手を調整する共有メモリ領域だ。
PostgreSQL では、postgresql.conf を編集してサービスを再起動したあと、現在有効なレベルとログの位置を確認する:
SHOW wal_level;
-- expected response: replica
SELECT pg_current_wal_lsn();
-- returns something like 0/3000060, the current log position
レプリケーションを有効にするなら、次は専用ユーザーを作成し、レプリカサーバー側で primary_conninfo をそのユーザーに向ける。細部はバージョンによって異なる。PostgreSQL の WAL パラメータのドキュメントが、各リリースに必要な詳細を個別に示している。
実際の利用場面
PostgreSQL のストリーミングレプリケーションの核心は、WAL をネットワーク越しに転送することにある。プライマリはレプリケーションのために何かを余分に計算しているわけではない。クラッシュリカバリ用に元々生成していたログレコードを、そのまま送り出しているだけだ。各レプリカは同じ順序でレコードを適用し、最終的に同じ状態に到達する。遅延はミリ秒から秒まで幅があり、ネットワークの遅延次第だ。
SQLite は同じ仕組みで別の問題を解く。モバイルやデスクトップのアプリケーションでは、複数のスレッドがデータベースを読みつつ、バックグラウンドのプロセスがデータを同期する必要がある。多くの組み込みアプリケーションが、この「複数のノンブロッキングな読み手 + 単一の書き手」という組み合わせに依存している。
同じ発想を借りているが write-ahead logging とは呼ばないシステムもある。Apache Kafka は本質的に分散した追記型ログであり、同じ原理をクラスタ規模に持ち上げたものだ。リレーショナルデータベースの WAL との類似は偶然ではない。どちらも同じ低レベルのデータ構造を使い、順序と永続性という同じ問題を解いている。

PostgreSQL では複数のレプリカが同じ WAL ストリームを同時に読める。Foto de Radowan Nakif Rehan en Unsplash
よくある間違いとベストプラクティス
SQLite で最もよくある間違いは、WAL モードのデータベースで長いトランザクションを開いたまま閉じないことだ。この読み取りトランザクションが生きている限り、自動チェックポイントはそれが開始した位置を越えて進めない。.wal ファイルはトランザクションが終わるまで増え続ける。
⚠️ 注意: .wal ファイルが増え続けるのは、ほぼ常に、誰も閉じていない読み取り接続が開いたままになっていることを意味する。
PostgreSQL でこれに対応する状況は、あるレプリカが落ちるか切断されたのに、wal_keep_size やレプリケーションスロットがそのレプリカのためにログセグメントを保持し続けている場合だ。そのレプリカがなかなか戻ってこなければ、プライマリには誰かが消費するのを待つ数 GB の保持 WAL が溜まる。
もう一つの古典的な間違いは、書き込み負荷が高いときに速度を上げようとして fsync を切ることだ。PostgreSQL の fsync=off や SQLite の PRAGMA synchronous=OFF は、完全な永続性の保証を無効にする。OS は物理ディスクが実際にデータを受け取る前に書き込み完了を報告でき、ちょうどその瞬間に電源が落ちればデータベースは壊れる。
ベストプラクティスとしては、まず計測する。PostgreSQL の pg_stat_bgwriter ビューは、時間によって強制されたチェックポイントがいくつあり、WAL の蓄積によって強制されたものがいくつあるかを示す。これは max_wal_size を上げる価値があるか、あるいは書き込み負荷を見直すべきかを直接教えてくれる。
他の方式との比較
write-ahead logging は永続性を保証する唯一の技術ではないが、現代のリレーショナルエンジンで最も広く使われているものだ。
| 技術 | 適する場面 | 利点 | 制約 |
|---|---|---|---|
| 書き込み前ログ | 汎用のトランザクションエンジン(PostgreSQL、SQLite、InnoDB) | リカバリが速く、同じログからそのままネイティブなレプリケーションができる | ログの増加を管理し、チェックポイントを調整する必要がある |
| シャドウページング | 同時書き込みがほとんどない最小限の組み込みエンジン | redo フェーズが不要で、リカバリがほぼ瞬時に終わる | ファイルが断片化し、同時トランザクションが複雑になる |
| ログなしの非同期書き込み | 最後の数秒のデータ損失を許容できるキャッシュやメトリクスシステム | 書き込み速度を最大まで引き出せる | 電源断で必ずデータを失う |
もう少し深く:グループコミットと論理レプリケーション
トランザクションごとに個別に fsync すると、高負荷時には遅くなる。そこで現代のエンジンは、コミット待ちのトランザクションをいくつかまとめ、1 回の fsync で全部をディスクに落とす。これをグループコミットと呼ぶ。PostgreSQL では commit_delay と commit_siblings で有効にし、数マイクロ秒だけ待ってより多くのトランザクションを集めてから fsync のコストを払う。
物理 WAL と論理 WAL の違いもある。物理 WAL は変更をディスクページ単位で記述し、リカバリや、まったく同じエンジンバージョンを動かすレプリカには十分で効率的だ。論理 WAL は変更を操作として記述する(この行を挿入する、この列を更新する)。そのためバージョン間でレプリケーションしたり、変更をイベントキューなどの外部システムに流し込んだりできる。
💡 ヒント: PostgreSQL から Kafka やその他の外部システムへ変更を送る必要があるなら、ドキュメントで論理デコードを調べるとよい。使うソースは同じ WAL で、それをエンジンの外でも読める形式に変換するだけだ。
PostgreSQL のレプリケーションスロットは、どの WAL セグメントを保持すべきかという問題を解決する。特定のレプリカが消費し終えるまで何も削除しないようプライマリに伝える仕組みで、代償として、あるレプリカが永久に落ちてそのスロットを誰も片付けなければ、プライマリのディスクが埋まる。
flowchart TD
A["Primary: generates WAL"] --> B["Replication slot retains segments"]
B --> C["Replica: consumes the WAL"]
C --> D[("Replica database")]
subgraph PostgreSQL Cluster
A
B
D
end
次のステップ:テスト用の SQLite データベースで PRAGMA journal_mode=WAL; を有効にし、2 つの接続を同時に開く。片方で書き込みをループさせ、もう片方で読み取りを行い、読み取りが決してブロックされないことを自分の目で確かめてほしい。
よくある質問
預写日誌はバックアップの代わりになるか?
ならない。WAL が守るのはプロセスや OS のクラッシュであって、WAL 自体が壊れたりディスクが完全に故障したりすれば、ログもデータも一緒に失われる。バックアップは依然として必要だ。実際 PostgreSQL は WAL そのものを使って継続的なバックアップを実現しているが、それは追加の用途であって代替ではない。
SQLite で .wal ファイルが大きくなりすぎたら?
デフォルトでは 1000 ページごとに自動 checkpoint が走るが、長時間実行中の読み取りがそれを妨げていると、その読み取りが終わるまでファイルは増え続ける。PRAGMA wal_checkpoint(TRUNCATE); で手動チェックポイントを強制すれば、ログを空にしてファイルを最小まで縮められる。
PostgreSQL の WAL を切って高速化できるか?
完全には切れない。PostgreSQL は常に WAL を書く。クラッシュリカバリに必要だからで、単機のデプロイでも同じだ。調整できるのは wal_level が決める記録の詳細度と、fsync の積極性であり、その代償は耐久性のトレードオフになる。
物理 WAL と論理 WAL の違いは?
物理 WAL はディスクページの低レベルな変更を記録する。論理 WAL は同じ変更を insert や update といった読める操作に翻訳したもので、バージョン間のレプリケーションや外部システムへのデータ出力を目的とする。
SQLite と PostgreSQL の WAL の実装は同じか?
原理は同じだが実装は異なる。SQLite はデータベースごとに .wal ファイルを 1 つだけ持ち、checkpoint のたびに全体をディスクに書き出す。PostgreSQL はログを 16 MB のセグメントに分割して循環利用し、複数のレプリカが同じストリームを同時に消費できる。
参考資料
- PostgreSQL ドキュメント:預写式ログとその耐久性保証についての公式解説。
- PostgreSQL ドキュメント:WAL 設定パラメータの完全なリファレンス。
- SQLite.org:預写日誌モードの仕様、関連する PRAGMA とその制限。
- Wikipedia:ARIES アルゴリズムと預写式ログの概念の歴史。