PostgreSQL WAL の内部構造を徹底解説:LSN、WAL レコード、書き込みパイプラインから Checkpoint まで

はじめに

この記事では、PostgreSQLのWAL(Write-Ahead Log)の内部実装を深掘りします。

前回の記事(InnoDB REDO Log の内部構造を徹底解説)ではMySQLのREDO Logを解説しました。今回はPostgreSQL 17のソースコード(src/backend/access/transam/xlog*.c)を読みながら、同じテーマをPostgreSQLの視点で解説します。InnoDBとの違いも随所で比較していきます。

この記事で扱う内容:

  • WALの役割とFull Page Write(InnoDBのDouble Write Bufferとの違い)
  • WALレコードの構造(Resource Manager方式)
  • WALの物理構造(ページ・セグメント・ファイル)
  • 書き込みパイプライン(WAL Buffer → ディスク)
  • Checkpoint(InnoDBとの設計思想の違い)

そもそもWALって何?

InnoDBのREDO Logと同じ役割です。PostgreSQLもメモリ(Shared Buffers)上でデータを変更し、ディスクへの反映は後回しにします。クラッシュ時にデータを失わないために、変更内容を先にログとしてディスクに書く。これがWAL(Write-Ahead Log)です。

InnoDBでは「REDO Log」と呼びますが、PostgreSQLでは「WAL」と呼びます。概念は同じです。

PostgreSQL用語        InnoDB用語
─────────────────────────────────
WAL                   REDO Log
LSN                   LSN
WAL Buffer            Log Buffer
Checkpointer          log_checkpointer
Full Page Write       Double Write Buffer(目的は同じ、手段が違う)

Partial Write問題とFull Page Write

InnoDBの記事で「Physiological Loggingはページの正しい状態が前提」と説明しました。PostgreSQLも同じ問題を抱えています。PostgreSQLのページサイズは8KB、ファイルシステムの原子書き込みは通常4KB。クラッシュ時にページの半分だけ書かれる(Partial Write / Torn Page)可能性があります。

InnoDBはDouble Write Bufferでこれを解決しました。ページを書く前に別領域に二重書きしておき、クラッシュ時に正しいページを復元します。

PostgreSQLは別のアプローチを取ります:Full Page Write(FPW)です。

Full Page Writeの仕組み

Checkpoint後に初めてページを変更するとき、WALレコードにページ全体のコピー(8KB)を含めます:

Checkpoint
    ↓
Page Xを初めて変更 → WALに「Page X全体(8KB)+ 変更内容」を記録
Page Xを2回目に変更 → WALに「変更内容だけ」を記録(FPWなし)
Page Xを3回目に変更 → WALに「変更内容だけ」を記録(FPWなし)
    ↓
次のCheckpoint
    ↓
Page Xを初めて変更 → またFPW付きで記録

リカバリ時にPartial Writeが検出されたら、FPW付きのWALレコードからページ全体を復元し、そこから後続のWALをリプレイします。

InnoDBとの比較

InnoDB PostgreSQL
手段 Double Write Buffer Full Page Write
追加I/O ページ書き込み時に二重書き Checkpoint直後のWALが肥大化
WALサイズ 影響なし FPWでWALが大きくなる
設定 innodb_doublewrite full_page_writes(デフォルトON)

PostgreSQLのFPWはWALサイズを増大させるデメリットがありますが、実装がシンプルで、WALだけで完結する(別の領域が不要)というメリットがあります。wal_compressionを有効にすればFPW部分を圧縮してサイズを軽減できます。

WALレコードの構造

InnoDBでは65種類のREDOタイプがありました。PostgreSQLはResource Manager(RM)という仕組みでWALレコードを管理します。

Resource Managerとは

PostgreSQLの各サブシステム(Heap、B-tree、Transaction、Sequenceなど)がそれぞれResource Managerとして登録されています。WALレコードの生成とリプレイは各RMが担当します。

/* src/include/access/rmgrlist.h より抜粋 */
PG_RMGR(RM_XLOG_ID,      "XLOG",      ...)   // WAL制御用
PG_RMGR(RM_XACT_ID,      "Transaction", ...)  // トランザクション
PG_RMGR(RM_HEAP_ID,      "Heap",       ...)   // テーブル操作
PG_RMGR(RM_BTREE_ID,     "Btree",      ...)   // B-treeインデックス
PG_RMGR(RM_HASH_ID,      "Hash",       ...)   // Hashインデックス
PG_RMGR(RM_GIN_ID,       "Gin",        ...)   // GINインデックス
PG_RMGR(RM_GIST_ID,      "Gist",       ...)   // GiSTインデックス
// ... 他にも多数

InnoDBが1つの巨大なswitch文で全REDOタイプを処理するのに対し、PostgreSQLは各RMに処理を委譲するプラグイン的な設計です。カスタムRMの追加も可能です。

WALレコードのヘッダ

各WALレコードには共通ヘッダXLogRecordが付きます:

typedef struct XLogRecord {
    uint32      xl_tot_len;   // レコード全体の長さ
    TransactionId xl_xid;     // トランザクションID
    XLogRecPtr  xl_prev;      // 前のWALレコードのLSN
    uint8       xl_info;      // RMごとのフラグ(操作種別)
    RmgrId      xl_rmid;      // Resource Manager ID
    pg_crc32c   xl_crc;       // CRC
} XLogRecord;

InnoDBとの違い: – xl_prev:前のレコードへのポインタ。InnoDBにはない。WALを逆方向にたどれる – xl_xid:トランザクションIDが入る。InnoDBのREDOにはトランザクション情報がない(UNDOログ側で管理) – xl_rmid + xl_info:RMの種類と操作種別の組み合わせで、何が起きたかを表現

WALレコードのボディ

ヘッダの後に、Block Reference(どのページを変更したか)とデータ本体が続きます:

  • XLogRecord 共通ヘッダ(24バイト)
  • Block Ref 0 対象ページ情報(RelFileNode + BlockNumber)
    • FPW data ← Full Page Writeの場合、ページ全体がここに入る
  • Block Ref 1 2つ目のページ(ある場合)
  • Main Data RM固有のデータ

1つのWALレコードが複数ページを参照できるのがInnoDBとの大きな違いです。InnoDBでは1 REDOレコード = 1ページでしたが、PostgreSQLではB-treeのノード分割のように複数ページにまたがる操作を1つのWALレコードで表現できます。

pg_waldumpで実際に見てみる

pg_waldumpコマンドでWALの中身を確認できます:

$ pg_waldump /path/to/pg_wal/000000010000000000000001

rmgr: Heap        len (rec/tot):     54/   150, tx:        737, lsn: 0/01234568, prev 0/01234520, desc: INSERT off: 2, blkref #0: rel 1663/13067/16384 blk 0 FPW
rmgr: Btree       len (rec/tot):     64/    64, tx:        737, lsn: 0/012345F8, prev 0/01234568, desc: INSERT_LEAF off: 2, blkref #0: rel 1663/13067/16387 blk 1
rmgr: Transaction len (rec/tot):     46/    46, tx:        737, lsn: 0/01234638, prev 0/012345F8, desc: COMMIT 2026-04-02 14:00:00 BST

この例では: 1. Heapテーブルにレコード挿入(FPW付き = Checkpoint後の初回変更) 2. B-treeインデックスにエントリ挿入 3. トランザクションコミット

WALの物理構造

InnoDBは「論理層→物理層(Block 512B)→ファイル層」の3層でした。PostgreSQLも似た構造ですが、単位が異なります。

ページ(8KB)

PostgreSQLのWALはディスク上で8KBページ単位に分割されます(InnoDBの512B Blockに相当)。各ページにはヘッダXLogPageHeaderDataが付きます:

typedef struct XLogPageHeaderData {
    uint16      xlp_magic;        // マジックナンバー
    uint16      xlp_info;         // フラグ(ロングヘッダかどうか等)
    TimeLineID  xlp_tli;          // タイムラインID(PITRやレプリケーション用)
    XLogRecPtr  xlp_pageaddr;     // このページの先頭LSN
    uint32      xlp_rem_len;      // 前ページから続くレコードの残りバイト数
} XLogPageHeaderData;

InnoDBのBlock Headerとの違い: – xlp_tli(タイムラインID):PostgreSQLのPITR(Point-in-Time Recovery)やレプリケーションで使う概念。InnoDBにはない – xlp_rem_len:前ページからまたがるレコードの残りバイト数。InnoDBのFirst Record Offsetと似た役割

WALレコードがページ境界をまたぐ場合、InnoDBと同様に分割されます:

Page N                         Page N+1
| Header | Record A |  | Header | Record B(後半) |
|---|---|---|---|---|
|  | Record B(前半) |  | (rem_len) | Record C |

セグメントファイル(デフォルト16MB)

WALページはセグメントファイルにまとめられます。デフォルトで1ファイル16MB(--wal-segsizeで変更可能)。

ファイル名は24桁の16進数です:

pg_wal/
├── 000000010000000000000001   ← タイムライン1、セグメント1
├── 000000010000000000000002   ← タイムライン1、セグメント2
└── ...
ファイル名: TTTTTTTTSSSSSSSSSSSSSSSS
            ^^^^^^^^                 タイムラインID(8桁)
                    ^^^^^^^^^^^^^^^^ セグメント番号(16桁)

InnoDBのib_logfile0/1が固定数のファイルを循環利用するのに対し、PostgreSQLは新しいセグメントファイルを次々作成し、不要になったら削除(またはリサイクル)します。

LSNとファイル位置の対応

PostgreSQLのLSNは64ビットの値で、ファイル内の物理位置に直接対応します:

/* LSNからセグメントファイル番号を求める */
segment = lsn / wal_segment_size;

/* LSNからファイル内オフセットを求める */
offset = lsn % wal_segment_size;

InnoDBのLSNはBlock Header/Tailerのオーバーヘッドを含むため、snとの変換が必要でした。PostgreSQLのLSNはページヘッダ分を含んだ値なので、変換はよりシンプルです。

書き込みパイプライン

InnoDBはMySQL 8.0で専用スレッド(log_writer、log_flusher)によるLock-free設計を導入しました。PostgreSQLのアプローチは異なります。

WAL Buffer

WAL Bufferは共有メモリ上の循環バッファです。サイズはwal_buffersで設定します(デフォルトはshared_buffersの1/32、最大64KB〜最小32KB)。

書き込みの流れ

PostgreSQLでは、WALの書き込みは各バックエンドプロセス自身が行います(InnoDBのような専用スレッドではない):

1. WAL Bufferへの挿入
   ├─ WALInsertLockを取得(軽量ロック、複数スロット)
   ├─ 予約位置を確保
   ├─ WALレコードをバッファにコピー
   └─ ロック解放

2. ディスクへの書き込み(必要な場合)
   ├─ WALWriteLockを取得(1つだけ)
   ├─ write() でPage Cacheに書く
   └─ ロック解放

3. fsync(コミット時)
   ├─ issue_xlog_fsync() を呼ぶ
   └─ 他のバックエンドのWALもまとめてフラッシュされる(group commit効果)

InnoDBとの設計思想の違い

InnoDB (8.0) PostgreSQL
書き込み主体 専用スレッド(log_writer) 各バックエンド自身
並列挿入 fetch_add + link_buf(Lock-free) WALInsertLock(複数スロットのLWLock)
fsync 専用スレッド(log_flusher) コミットするバックエンド自身
Group Commit log_flush_notifierで起床 fsync中に他のバックエンドが待機→まとめてフラッシュ

PostgreSQLのWALInsertLocknum_xloginsert_locks個(デフォルト8)のスロットに分かれており、異なるスロットを取得したバックエンドは並列にWAL Bufferに書き込めます。InnoDBのLock-freeほど洗練されていませんが、実用上は十分な並列性を確保しています。

synchronous_commit

InnoDBのinnodb_flush_log_at_trx_commitに相当するのがsynchronous_commitです:

挙動 InnoDBの対応
on(デフォルト) fsyncまで待つ = 1
off 待たない(最大600msのデータロス) = 0
remote_write レプリカのwrite()まで待つ
remote_apply レプリカの適用まで待つ

PostgreSQLはレプリケーションとの統合が深く、remote_writeremote_applyのようなレプリカ側の永続化レベルも選べます。

Checkpoint

InnoDBと同じく、PostgreSQLもCheckpointで「ここより前のWALは不要」という位置を記録します。ただし仕組みが異なります。

Checkpointの流れ

PostgreSQLのCheckpointerプロセスが定期的に実行します:

1. Checkpointの開始をWALに記録(XLOG_CHECKPOINT_ONLINE)
2. 全ダーティページをディスクにフラッシュ
3. Checkpoint完了をpg_control(制御ファイル)に記録
4. 古いWALセグメントを削除/リサイクル

InnoDBとの違い

InnoDBのCheckpointは「現在のダーティページの最小LSNを記録する」だけで、ダーティページのフラッシュは別の仕組み(Page Cleaner)が非同期に行います。つまりInnoDBのCheckpointは軽い操作です。

一方、PostgreSQLのCheckpointは全ダーティページをフラッシュする重い操作です。そのためcheckpoint_completion_target(デフォルト0.9)で、次のCheckpointまでの時間の90%をかけてゆっくりフラッシュする仕組みがあります。

InnoDB PostgreSQL
Checkpoint時の動作 LSNを記録するだけ 全ダーティページをフラッシュ
ダーティページのフラッシュ Page Cleanerが常時非同期で実行 Checkpointer + BGWriter
Checkpoint間隔 自動(REDOの消費量に応じて) checkpoint_timeout(デフォルト5分)
記録先 ib_logfile0のCheckpoint Block pg_controlファイル

pg_controlの確認

pg_controldataコマンドでCheckpoint情報を確認できます:

$ pg_controldata /path/to/data

Latest checkpoint location:           0/1234568
Latest checkpoint's REDO location:    0/1234520
Latest checkpoint's TimeLineID:       1

まとめ

トピック InnoDB PostgreSQL
名称 REDO Log WAL
Partial Write対策 Double Write Buffer Full Page Write
レコード管理 65種類のType Resource Manager方式
1レコードの対象 1ページのみ 複数ページ可
物理単位 Block(512B) ページ(8KB)
ファイル管理 固定数ファイルを循環利用 セグメントファイルを作成/削除
書き込み方式 専用スレッド + Lock-free 各バックエンド + LWLock
Checkpoint 軽い(LSN記録のみ) 重い(全ダーティページフラッシュ)

設計思想の違いが面白いですね。InnoDBはMySQL 8.0で専用スレッドとLock-freeデータ構造による高度な並列化を追求しています。PostgreSQLはよりシンプルな設計で、レプリケーションやPITRとの統合を重視しています。

次回はOracle REDO Logを同じ切り口で解説します。

参考文献

コメントする