目次を表示する

RDB内部構造完全ガイド

WAL と障害復旧 ── データを絶対に失わない仕組み

WAL と障害復旧 ── データを絶対に失わない仕組み

このレイヤーの役割:クラッシュしてもコミット済みデータを失わない。レプリケーションの基盤。RDB の「保険」。


WALと障害復旧 — 先行書き込みログ・チェックポイント・PITR

この章で何ができるようになるか:「WAL がなぜ必要か」「チェックポイントとは何か」「WAL の書き込みがボトルネックになるケース」を説明でき、関連パラメータを適切に設定できるようになる。


なぜ WAL が必要か

WAL なしの場合:
  1. メモリ上のバッファを変更
  2. バッファをディスクに書き出す(ランダムI/O)
  3. → 書き出し途中でクラッシュ → データが中途半端な状態
  4. → 復旧不可能

WAL ありの場合:
  1. 「何を変更するか」を WAL に書く(シーケンシャルI/O)
  2. WAL を fsync(ディスクに確実に書き出す)
  3. メモリ上のバッファを変更
  4. → クラッシュしても WAL から変更を再適用(redo)できる

WAL の書き込みはシーケンシャルI/O → ランダムI/O より遥かに速い

WAL レコードの構造

WAL レコードの内容:
  - LSN(Log Sequence Number): WAL 内の位置(単調増加)
  - Transaction ID
  - 操作の種類(INSERT / UPDATE / DELETE / COMMIT / ...)
  - 対象のテーブルとページ
  - 変更前と変更後のデータ(Full Page Image の場合はページ全体)
-- 現在の WAL 位置(LSN)を確認
SELECT pg_current_wal_lsn();
-- → 0/1A3E5F00

-- WAL の書き込み速度を確認
SELECT
  pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0') / (1024*1024) AS wal_written_mb;

WAL の書き込みフロー

1. トランザクション内の各操作で WAL レコードを生成
2. WAL レコードを WAL バッファ(メモリ)に書き込む
3. COMMIT 時:
   a. WAL バッファをディスクに書き出す(write)
   b. fsync を実行(ディスクの書き込みバッファをフラッシュ)
   c. → ここで初めてコミットが「確定」する
4. クライアントにコミット成功を返す

synchronous_commit の設定

-- デフォルト: on(WAL の fsync を待つ)
SET synchronous_commit = on;
-- → 最も安全。クラッシュしてもデータを失わない。
-- → fsync の待ち時間がレイテンシに加算される。

SET synchronous_commit = off;
-- → fsync を待たずにコミット成功を返す
-- → レイテンシが大幅に短縮(数ms → 数百μs)
-- → クラッシュ時に直近のコミット(最大 wal_writer_delay 分)を失う可能性
-- → 用途: ログ書き込みなど、多少のデータロスが許容される場合

チェックポイント:WAL と実データの同期点

WAL だけが増え続けると、障害復旧時に大量の WAL を再適用する必要がある。チェックポイントで定期的にメモリの変更をディスクに反映し、古い WAL を不要にする。

チェックポイントの動作:
  1. 全ての Dirty Page(メモリ上で変更されたページ)をディスクに書き出す
  2. チェックポイントの LSN を記録
  3. この LSN より前の WAL はもう不要
     → 復旧時はチェックポイントの LSN から再生すればよい

チェックポイントのトリガー:
  - checkpoint_timeout(デフォルト 5分)ごと
  - max_wal_size(デフォルト 1GB)を超えたとき
  - 手動: CHECKPOINT コマンド
  - pg_start_backup() 実行時
-- チェックポイント関連のパラメータ
SHOW checkpoint_timeout;     -- 5min(チェックポイントの最大間隔)
SHOW max_wal_size;           -- 1GB(この量の WAL が生成されたらチェックポイント)
SHOW checkpoint_completion_target;  -- 0.9(チェックポイントの書き出しを timeout の 90% に分散)

checkpoint_completion_target の意味

checkpoint_completion_target = 0.9, checkpoint_timeout = 5min の場合:
  チェックポイントの書き出しを 5min × 0.9 = 4.5分 かけて行う
  → I/O を分散してスパイクを防ぐ

checkpoint_completion_target = 0.1 の場合:
  30秒で一気に書き出す
  → I/O スパイクが発生し、通常のクエリが遅くなる

Full Page Writes

チェックポイント後の最初のページ変更時に、ページ全体を WAL に書き込む。

なぜ必要か:
  OS は 4KB 単位で書き込む。PostgreSQL は 8KB ページ。
  ページ書き出しの途中でクラッシュすると、ページの前半は新データ、後半は旧データ
  という「Torn Page」が発生する。

  Full Page Write があれば、WAL からページ全体を復元できる。

コスト:
  チェックポイント直後は WAL の書き込み量が一時的に増大
  (ページ全体 8KB × 変更されたページ数)
SHOW full_page_writes;  -- on(デフォルト。変更すべきではない)

障害復旧(Crash Recovery)

PostgreSQL 起動時の復旧フロー:
  1. pg_control ファイルから最後のチェックポイントの LSN を読む
  2. そのチェックポイント以降の WAL レコードを順番に再適用(redo)
  3. コミットされていないトランザクションの変更を取り消す(undo は不要※)
  4. 復旧完了

※ PostgreSQL は未コミットのデータを可視性判定で除外する
   (t_xmin のトランザクションがコミットされていなければ見えない)
   → 明示的な undo 処理は不要

WAL のチューニングポイント

-- WAL バッファサイズ(メモリ上の WAL バッファ)
SHOW wal_buffers;  -- -1(自動: shared_buffers の 1/32, max 64MB)

-- WAL セグメントサイズ(1ファイルのサイズ)
-- デフォルト 16MB。大量書き込みなら 64MB〜256MB に増やす

-- WAL の圧縮
SET wal_compression = on;  -- WAL レコードを圧縮(CPU ↑, I/O ↓)
-- Full Page Writes が多い場合に特に効果的

-- WAL の書き込み先を別ディスクに分離
-- → WAL とデータの I/O が競合しない
-- → シンボリックリンクで pg_wal を別マウントポイントに向ける

Point-in-Time Recovery(PITR)

WAL を保存しておけば、任意の時点にデータベースを復元できる。

1. ベースバックアップ(pg_basebackup)を定期的に取得
2. WAL を連続的にアーカイブ(archive_command)

復元:
  1. ベースバックアップをリストア
  2. アーカイブされた WAL を再適用
  3. recovery_target_time で「2026-04-08 15:30:00 まで」と指定
  → その時点のデータベース状態に復元
-- postgresql.conf
archive_mode = on
archive_command = 'cp %p /backup/wal_archive/%f'

-- recovery.conf(復元時)
restore_command = 'cp /backup/wal_archive/%f %p'
recovery_target_time = '2026-04-08 15:30:00'

まとめ

概念役割設定ポイント
WAL変更を先にログに書く → クラッシュ耐性wal_buffers, wal_compression
fsyncWAL をディスクに確実に書き出すsynchronous_commit
チェックポイントDirty Page をディスクに反映 → 古い WAL を不要にするcheckpoint_timeout, max_wal_size
Full Page WritesTorn Page を防ぐfull_page_writes = on(変更不可)
PITR任意の時点に復元archive_mode, archive_command