WAL 預寫式日誌:PostgreSQL 資料安全的守護者 | PostgreSQL

2026/06/10
WAL 預寫式日誌:PostgreSQL 資料安全的守護者 | PostgreSQL

WAL(Write-Ahead Logging)PostgreSQL 實現資料持久性(Durability)的核心機制。其根本原則是:在修改資料頁之前,必須先將變更記錄寫入 WAL。只要 WAL 記錄持久化到磁碟,即使發生 Crash,已提交的 Transaction 也能在重啟後完整恢復。WAL 同時也是 PostgreSQL 複寫與備份的技術基石。

核心原則:隨機 I/O 變順序 I/O

傳統做法是每次 COMMIT 後立刻將所有 dirty page 寫回磁碟。問題在於資料頁分散在磁碟各處,每次 COMMIT 都要進行大量隨機 I/O(Random I/O),效能極差。

WAL 的解決方案是將隨機 I/O 轉換為順序 I/O:

傳統做法(Force Policy):
  COMMIT → 寫多個散落的資料頁 → 大量 Random I/O → 效能瓶頸
           Page A (Offset: 4MB)
           Page B (Offset: 156MB)
           Page C (Offset: 892MB)

WAL 方式:
  COMMIT → 只需順序追加 WAL 記錄 → Sequential I/O → 高效能
           WAL segment: 追加到末尾
           資料頁: 非同步延遲寫回(由 bgwriter/checkpointer 負責)

WAL 的兩大保證:

  1. Durability(持久性):COMMIT 時只需要 WAL flush 到磁碟,不需要資料頁立即寫回
  2. Atomicity(原子性):Crash Recovery 時重放 WAL,確保 Transaction 要麼完整重放、要麼完全不重放

LSN(Log Sequence Number)

LSN 是 WAL 記錄的全域唯一位置標識,是一個 64-bit 整數,表示在 WAL 串流中的位元組偏移量。

LSN 格式:高 32 位 / 低 32 位(以 16 進位顯示)
  例如:0/1A2B3C4D

我們可以查詢當前的 LSN 狀態:

-- 查看當前 WAL 已寫入的 LSN
SELECT pg_current_wal_lsn();
-- 輸出:0/1A2B3C4D

-- 查看當前 WAL 已 flush 到磁碟的 LSN
SELECT pg_current_wal_flush_lsn();

-- 查看當前 WAL 檔案名稱
SELECT pg_walfile_name(pg_current_wal_lsn());
-- 輸出:000000010000000000000001

-- 計算兩個 LSN 之間的 WAL 大小(bytes)
SELECT pg_wal_lsn_diff('0/2000000', '0/1A2B3C4D');

在備援伺服器上,還有兩個重要的 LSN 函數:

-- 備援伺服器接收到的最新 LSN
SELECT pg_last_wal_receive_lsn();

-- 備援伺服器重放到的最新 LSN
SELECT pg_last_wal_replay_lsn();

WAL Segment 檔案結構

WAL 以固定大小的 Segment 檔案儲存,預設每個 Segment 為 16MB,位於 $PGDATA/pg_wal/ 目錄:

pg_wal/ 目錄結構:
  ├── 000000010000000000000001   (Segment 1, Timeline 1)
  ├── 000000010000000000000002   (Segment 2, Timeline 1)
  ├── 000000010000000000000003   (Segment 3, Timeline 1)
  └── archive_status/
      ├── 000000010000000000000001.done  (已歸檔)
      └── 000000010000000000000002.ready (等待歸檔)

檔名格式(24 個 16 進位字元):
  ┌────────┬────────────────┬────────────────┐
  │ 8 位   │     8 位       │     8 位       │
  │Timeline│  Log Segment   │  Segment Num   │
  └────────┴────────────────┴────────────────┘

每個 Segment 內部由 8KB 的 Page 組成,每個 WAL Record 包含:

WAL Record 結構:
  ┌────────────┬────────────┬───────────────┬─────────────┐
  │ xl_tot_len │ xl_xid     │ xl_prev       │ xl_info     │
  │ (記錄總長) │ (TX ID)    │ (前一條 LSN)  │ (操作類型)  │
  ├────────────┴────────────┴───────────────┴─────────────┤
  │ data (變更內容,可能包含 Full Page Image)               │
  └────────────────────────────────────────────────────────┘

Checkpoint 機制

Checkpoint 是 WAL 與資料頁同步的關鍵機制——它確保在某個時間點之前的所有變更已寫入磁碟。

Checkpoint 執行流程:

  1. 記錄 checkpoint_lsn:獲取當前 LSN 作為 Checkpoint 點
  2. 寫回所有 dirty buffers:Checkpointer 掃描 Shared Buffer Pool,將所有標記為 dirty 的 Page 寫回磁碟(最耗時的步驟)
  3. 寫入 CHECKPOINT record 到 WAL:記錄 checkpoint_lsn 等資訊
  4. 更新 pg_control 檔案:記錄最新 Checkpoint 的位置,此 LSN 之前的 WAL 理論上可以刪除

Checkpoint 觸發條件

條件說明
checkpoint_timeout 到期預設 5 分鐘
WAL 大小超過 max_wal_size預設 1GB
手動執行 CHECKPOINT 命令需要超級使用者權限
PostgreSQL 正常關閉smart/fast shutdown

Checkpoint 與 Crash Recovery 的關係

時間軸:

Checkpoint A    Checkpoint B    Crash 點
    │                │              │
────●────────────────●──────────────✗

Crash Recovery 只需從最近的 Checkpoint(B)重放 WAL 到 Crash 點。
→ Checkpoint 越頻繁,Recovery 時間越短
→ 但正常執行期間 I/O 壓力越大

full_page_writes:防止 Torn Page

磁碟通常以 512B 或 4096B 為單位寫入,但 PostgreSQL 頁面為 8KB。如果寫入頁面時發生 Crash(只寫了一半),頁面會損壞(Torn Page)。

full_page_writes 的解決方案:

Checkpoint 後第一次修改某個 Page 時,WAL 記錄包含該 Page 的完整映像(FPI)。
後續對同一 Page 的修改,在下一個 Checkpoint 之前只記錄增量變更。

  Checkpoint    修改 P1   修改 P2   修改 P1   Checkpoint
      │            │        │        │          │
──────●────────────●────────●────────●──────────●
                   │                 │
                   └─ WAL 包含 P1   └─ 只記錄增量
                      完整映像(FPI)     (已有 FPI 保護)

代價是 WAL 體積增大,但生產環境務必保持 full_page_writes = on

WAL Level

WAL 記錄的詳細程度由 wal_level 參數控制:

Level功能用途
minimal最基本的恢復資訊單機、不需備份
replica(預設)支援 Streaming Replication 與 PITR大多數生產環境
logical支援 Logical Replication 與 logical decoding跨版本複寫、CDC
-- 查看當前 WAL Level
SHOW wal_level;
-- 輸出:replica

Crash Recovery 流程

PostgreSQL 啟動時發現上次並非正常關閉,會進入 Recovery 模式:

讀取 pg_control 檔案
    │
    ▼
判斷 state 欄位
    │
    ├─ 正常關閉 → 直接啟動
    │
    └─ 非正常關閉 → 進入 Recovery 模式
                         │
                         ▼
                 找到最近的 Checkpoint
                 (從 pg_control 讀取 checkpoint_lsn)
                         │
                         ▼
                 從 checkpoint_lsn 開始順序重放 WAL 記錄
                         │
                         ▼
                 重放到 WAL 結尾
                         │
                         ▼
                 建立新 Checkpoint,更新 pg_control
                         │
                         ▼
                 開放連線,正常運作

監控 WAL 統計

-- PG14+ WAL 全域統計
SELECT
    wal_records,           -- 產生的 WAL 記錄總數
    wal_fpi,               -- Full Page Image 的數量
    pg_size_pretty(wal_bytes) AS wal_size, -- WAL 總大小
    wal_buffers_full,      -- 因 buffer 滿而等待的次數
    wal_write,             -- WAL 寫入次數
    wal_sync              -- WAL fsync 次數
FROM pg_stat_wal;

-- 監控 Checkpoint 頻率
SELECT
    checkpoints_timed,     -- 因 timeout 觸發的 checkpoint 數
    checkpoints_req,       -- 因 WAL 大小觸發的 checkpoint 數
    buffers_checkpoint,    -- checkpoint 寫回的 buffer 數
    buffers_clean,         -- bgwriter 寫回的 buffer 數
    buffers_backend        -- backend 直接寫回的 buffer 數(應盡量少)
FROM pg_stat_bgwriter;

使用 pg_waldump 解析 WAL

pg_waldump 是分析 WAL 內容的強大工具:

# 解析指定 WAL segment 的記錄
pg_waldump $PGDATA/pg_wal/000000010000000000000001

# 只顯示特定 resource manager 的記錄
pg_waldump --rmgr=Heap $PGDATA/pg_wal/000000010000000000000001

# 顯示統計摘要
pg_waldump --stats $PGDATA/pg_wal/000000010000000000000001

生產環境建議配置

# postgresql.conf

# WAL 等級:至少 replica
wal_level = replica

# WAL 大小範圍
min_wal_size = 512MB
max_wal_size = 2GB

# Checkpoint 頻率(Recovery 時間 vs I/O 壓力的平衡)
checkpoint_timeout = 15min
checkpoint_completion_target = 0.9

# WAL 寫入效能
wal_compression = lz4        # PG15+ 壓縮 Full Page Image
synchronous_commit = on      # 生產環境保持 on
wal_buffers = 16MB

儲存配置建議:

  • pg_wal/ 目錄放在獨立的快速儲存設備(NVMe SSD)
  • 確保有足夠空間(至少 max_wal_size 的 2 倍)
  • 啟用 WAL 歸檔以支援 PITR

常見陷阱

Replication Slot 導致 WAL 積累

-- 排查:找出 inactive 的 replication slot
SELECT slot_name, active, restart_lsn
FROM pg_replication_slots
WHERE active = false;

-- 解決:刪除不再使用的 slot
SELECT pg_drop_replication_slot('unused_slot');

checkpoint_completion_target 設太低

設定過低(如 0.5)會導致 Checkpoint I/O 集中在短時間內,產生 I/O 突刺。建議設為 0.9,讓 Checkpoint 的 I/O 均勻分散在整個 Checkpoint 週期中。

WAL 與複寫的關係

WAL 是 PostgreSQL 所有複寫機制的基礎:

  • Streaming Replication:Primary 持續串流 WAL 給 Standby
  • WAL Archiving:WAL Segment 完成後複製到歸檔位置
  • PITR(Point-In-Time Recovery):從備份 + WAL 恢復到任意時間點
  • Logical Replication:從 WAL 解碼行級別變更,複製到訂閱者

PG17 新功能:Incremental Backup

PG17 引入 WAL Summarizer,記錄每個 WAL Segment 修改了哪些資料頁,用於實現增量備份:

# 啟用 WAL Summarizer(PG17+)
summarize_wal = on

增量備份只需備份自上次備份以來修改過的資料頁,大幅降低備份時間和空間。

總結

WAL 是 PostgreSQL 資料安全的基石:

  • 將隨機 I/O 轉換為順序 I/O,在保障資料安全的同時提升效能
  • LSN 是 WAL 的全域座標系,所有複寫與備份操作都以 LSN 為基準
  • Checkpoint 是 WAL 與資料頁的同步點,決定了 Crash Recovery 的起點
  • full_page_writes 防止 Torn Page,生產環境務必開啟
  • WAL 是 Streaming Replication、Logical Replication、PITR 的技術基礎

下一篇,我們將探討 Buffer Management——PostgreSQL 如何用 Clock-Sweep 演算法管理 Shared Buffer Pool。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。