トランザクションログとWAL
トランザクションログとWALとは
[[トランザクションとACID]] の D(永続性)は「コミットした結果は障害でも失われない」と説明されます。では、COMMIT が返った 0.1 秒後に電源ケーブルが抜けても、なぜその変更は残っているのでしょうか。
答えがトランザクションログです。データベースはテーブル本体を書き換える前に、変更内容をログファイルへ追記してディスクへ確実に書き終えてからコミット完了を返します。この順序を守る方式を WAL(Write-Ahead Logging、ログ先行書き込み)と呼びます。
COMMIT が返るまでに必ず済ませる → 変更内容がログとしてディスク上にある
COMMIT の後にゆっくりやる → テーブル本体・インデックスへの反映
つまり「本体にはまだ書かれていないが、ログにはある」状態が常時存在します。電源断が起きてもログを読み直せば復元できる — これが永続性の正体です。
なぜ本体より先にログなのか
「本体を直接書けばいいのでは」と思えるところですが、理由は書き込みの速さにあります。
1 件の UPDATE で書き換わるのはデータ本体だけではありません。インデックスも同時に更新され、ディスク上のバラバラの位置に書き込みが発生します。これはランダム書き込みで、HDD ならヘッド移動の待ちが毎回入ります([[ストレージの種類]])。一方ログは1 本のファイルの末尾に、変更差分だけを追記する順次書き込みで、ディスクが最も得意な形です。
| データ本体への書き込み | ログへの書き込み | |
|---|---|---|
| 書く場所 | ディスク上のあちこち(ランダム) | ファイル末尾に追記(順次) |
| タイミング | コミット後でよい | コミット前に完了必須 |
「ディスクへ書いた」の意味にも注意が要ります。通常のファイル書き込みは OS のメモリ上のキャッシュに置かれるだけで、電源断で消えます。そのためコミット時には fsync(キャッシュを物理媒体へ強制的に書き出す指示)を呼びます。コミットが遅い原因がこの fsync 待ちであることは多く、製品側は複数トランザクションの fsync をまとめるグループコミットで緩和しています。
チェックポイントとクラッシュリカバリ
ログだけが頼りだと、起動のたびに全ログを最初から再生する羽目になります。そこで定期的にチェックポイントを打ちます。メモリ上の変更済みページをまとめて本体へ書き出し、「ここまで反映済み」という印をログに記す処理です。
障害後の起動時(クラッシュリカバリ)は、この印より後ろのログを見るだけで済みます。
- REDO(再実行)— ログにあるが本体に未反映の変更を適用し直す
- UNDO(取り消し)— コミットされていないのに本体へ書かれた変更を戻す
UNDO が必要なのは、メモリが足りなくなると未コミットの変更を含むページでもディスクへ追い出されることがあるためです。復旧には進めることと戻すことの両方が要ります。
ログが溜まるとディスクが埋まる
運用でよく遭遇するトラブルです。役目を終えたログは削除・再利用されますが、次の場合は消せずに溜まり続けます。
- PITR 用にアーカイブしている — 保持期間の設定を誤ると増え続ける
- レプリカが遅れている・停止している — まだ受け取っていないログを送信元は捨てられない
- 長時間トランザクションが開きっぱなし — 終わるまで関連するログを解放できない
行き着く先は「ディスク使用率 100% で書き込み不能になり停止」です。ログ領域の空き容量は必ず監視します。3 は [[ロックとデッドロック]] の面でも避けるべきで、「トランザクションは短く」の原則がここでも効きます。
PITR とレプリケーションも同じログの上に乗る
このログは「いつ・何が変わったか」が時系列で並んだ完全な記録なので、他機能の土台にもなります。
- PITR — [[バックアップとリカバリ]] の特定時点復元は、フルバックアップを戻したあとログを目的の時刻まで再生する処理です。誤削除の 1 分前で止められるのはこのおかげです
- レプリケーション — [[レプリケーション]] で転送されているものの正体がこのログです。レプリカはそれを受け取って適用し続けており、レプリケーションラグとは「まだ適用しきれていないログの量」です
製品差も押さえましょう。PostgreSQL は WAL 1 本が三役を兼ねますが、MySQL(InnoDB)はクラッシュリカバリ用の redo log とレプリケーション・PITR 用の binlog(バイナリログ)が別物です。SQLite にも WAL モードがあり PRAGMA journal_mode = WAL; で切り替えます。
初学者向けポイント
- 「コミットしたのにディスクにはまだ書かれていない」は異常ではなく設計どおり。ログが書かれていれば永続性は満たされる
- fsync を無効化する設定は劇的に速くなるが、電源断でコミット済みデータを失う。作り直せる開発環境だけに留める
- [[リレーショナルデータベース]] が「絶対に壊れてはいけないデータ」に強いのは、この地味な追記処理を必ず先に通しているからです
関連技術とのつながり
- [[トランザクションとACID]] — D(永続性)を実際に成立させているのが WAL
- [[バックアップとリカバリ]] — PITR で再生するログの実体
- [[レプリケーション]] — レプリカへ転送されている中身もこのログ
- [[リレーショナルデータベース]] — ACID 保証を支える内部機構
- [[ロックとデッドロック]] — 長いトランザクションはロックとログ肥大の両面で問題
Q: WAL(ログ先行書き込み)でコミット完了を返す前に必ず終えているのはどれ?
- [x] 変更内容をログファイルへ書き、ディスクに確実に書き終えること
- [ ] テーブル本体とインデックスをディスクへ反映し終えること
- [ ] レプリカへの適用が完了すること
解説: WAL では本体への反映は後回しでよく、ログをディスクへ確実に書いた時点でコミットを返せます。
Q: 本体への書き込みより先にログへ書くほうが速い理由として本文が挙げたのはどれ?
- [ ] ログは圧縮されるのでサイズがゼロになるから
- [x] ログはファイル末尾への追記(順次書き込み)で済むが、本体はディスク上のあちこちを書き換えるから
- [ ] ログはメモリ上だけに置かれ、ディスクを一切使わないから
解説: 本体とインデックスの更新はランダム書き込みになる一方、ログは1本のファイルへの追記だけで済みます。
Q: トランザクションログが消せずに溜まり続ける原因として本文が挙げていないのはどれ?
- [ ] レプリカが遅れている・停止している
- [ ] 長時間トランザクションが開きっぱなしになっている
- [x] インデックスが断片化している
解説: 本文が挙げた原因はPITR用アーカイブ・レプリカの遅延・長時間トランザクションの3つです。放置するとディスクが埋まり停止します。