1. WAL簡介
WAL(Write-Ahead Logging)是PostgreSQL的核心機制之一。其基本理念是:在修改數據庫數據頁之前,必須先將這次修改操作寫入到WAL日志中。
這確保了即使發生崩潰,數據庫也可以根據WAL日志進行恢復。
恢復的核心流程就是不斷**Replay(重做)**WAL記錄,把數據庫恢復到一致狀態。
2. 為什么需要冪等性?
故障恢復可能發生在任意時刻,且可能反復進行。為了確保數據一致性,必須保證:
- WAL重放(replay)是冪等的:即多次重做同樣的WAL記錄,最終結果與只做一次是一樣的。
- 防止重復執行邏輯修改,避免出現臟數據(如重復插入、更新錯誤等)。
沒有冪等性,PostgreSQL的可靠性和數據完整性就無從談起。
3. PostgreSQL 如何實現 WAL 的冪等性?
分情況討論:
3.1 物理日志(Full Page Write, FPW)
- 當首次修改某個page時,如果遇到檢查點之后第一次修改,或者出現了部分寫(partial write)風險時,PostgreSQL會將整個page的物理鏡像寫入WAL。
- 恢復時直接拷貝覆蓋,無需考慮page上的狀態。
- 因為是整頁覆蓋,所以天然是冪等的。重復應用多次,結果不會變。
注:FPW是恢復的"大殺器",確保即使發生中間頁損壞,也能恢復。
3.2 邏輯日志(Insert/Update/Delete操作記錄)
- 日志并不是直接記錄"修改了page的哪一塊物理位置",而是記錄在什么page上執行了什么邏輯操作。
- 例如:在某個page上插入一條tuple "X",而不是"在page偏移offset=32的地方插入"。
- 如果簡單地無腦重做,恢復兩遍就會出現重復記錄。
為避免此問題,PostgreSQL每個page在物理結構中維護了一個字段:pd_lsn,即Page LSN。
- 當一個page被修改時,它的pd_lsn被更新為當前WAL record的LSN。
- 恢復時,每次要重放一個WAL record之前,PostgreSQL會先檢查:
只有當 WAL record 的 LSN > page 的 pd_lsn 時,才進行重做,否則跳過。
3.3 流程示意圖
恢復 -> 讀取WAL record -> 找到要修改的Page ->
比較 (WAL record LSN vs Page pd_lsn)
?? -> 如果 WAL LSN <= Page LSN, 說明已經做過,跳過
?? -> 如果 WAL LSN > Page LSN, 執行修改,更新Page LSN
這種機制可以保證:即使崩潰后在同一個地方反復重做日志,也不會對數據造成破壞。
4. 實驗驗證 PostgreSQL WAL 冪等性
下面用一個小實驗驗證pg的WAL冪等性:
4.1 實驗環境準備
initdb -D /tmp/pgwaltest
pg_ctl -D /tmp/pgwaltest -l logfile start
psql
4.2 創建表并插入數據
CREATE TABLE test_wal (id serial PRIMARY KEY, val text);
INSERT INTO test_wal(val) VALUES('first insert');
CHECKPOINT;? -- 強制生成檢查點,防止混入無關WAL
這時test_wal表中有一條數據,系統生成了新的檢查點。
4.3 查看Page LSN
PostgreSQL提供了擴展 pageinspect 來查看物理Page的信息。
安裝并使用:
CREATE EXTENSION pageinspect;
-- 找出表的物理文件
SELECT relfilenode FROM pg_class WHERE relname = 'test_wal';
-- 假設relfilenode是 16384
-- 查看page的LSN(第一頁)
SELECT * FROM page_header(get_raw_page('test_wal', 0));
會返回:
lsn? | checksum | flags | lower | upper | special | pagesize | version | prune_xid
------+----------+-------+-------+-------+---------+----------+---------+-----------
0/1500720 |??? ...?? |?????? |?? ... |?? ... |??? ...?? |??? 8192? |??? 4??? |??? ...
記錄下當前Page的LSN,比如是0/1500720。
4.4 模擬崩潰后重復恢復
手動重放WAL當然很復雜,但可以模擬:
- 手動修改page LSN,使其低于WAL日志的LSN;
- 重啟數據庫,觸發recovery。
不過這樣太麻煩,我們可以簡單理解為:
- 如果page LSN已經大于WAL record的LSN,那么即使WAL被Replay,也不會改動頁面。
4.5 直接實驗重復寫入一條相同數據:
-- 模擬意外重復插入
INSERT INTO test_wal(val) VALUES('first insert');
-- 應該報錯,因為違反唯一約束(id列),而不是"無腦"插兩遍
說明PostgreSQL在邏輯層也有冪等保護,比如:
- 主鍵/唯一約束
- page LSN校驗
- XID(事務ID)檢測
5. 注意事項和補充
- FPW(Full Page Writes)必須打開,否則遇到partial write會導致崩潰后無法恢復。
- 極端情況下,日志丟失(比如WAL損壞)仍然會導致恢復失敗,因此建議使用流復制+歸檔備份雙保險。
- WAL日志管理工具(如pg_waldump)可以輔助查看日志內容,幫助理解。
示例:
pg_waldump /tmp/pgwaltest/pg_wal/
可以看到諸如:
rmgr: Heap??????? len (rec/tot):? 54/ 54, tx: 490, lsn: 0/01500280, desc: INSERT off 2
顯示了一個Heap表上的insert動作及其LSN。
總結
- PostgreSQL WAL重放過程是嚴格冪等的。
- 物理日志天然冪等,邏輯日志依賴于Page LSN機制保證冪等。
- 實現細節涵蓋了page級別和事務級別的雙重校驗。
- 實驗驗證了冪等機制的有效性。
PostgreSQL通過這些設計,確保即使在最壞情況下崩潰恢復,也能保證數據一致性和正確性。