日誌記錄#
發生故障時(斷電、OS 錯誤、資料庫伺服器當機),RAM 中的所有內容都會遺失,只有已寫入磁碟的資料能存活。故障後要啟動伺服器,就必須還原資料一致性;若磁碟本身損毀,同樣的問題得靠備份還原來解決。
理論上你可以讓磁碟上的資料隨時保持一致。但實務上這意味著伺服器必須不斷把隨機頁面寫到磁碟(儘管循序寫入便宜得多),而且這些寫入的順序必須保證任何時刻一致性都不受損——這很難做到,尤其在處理複雜索引結構時。
與多數資料庫系統一樣,PostgreSQL 採取不同做法:
- 伺服器運行時,部分當前資料只存在於 RAM,寫入永久儲存被延後
- 因此運行期間磁碟上的資料總是不一致的,因為頁面從不會被一次全部刷出
- 但 RAM 中發生的每一項變更(例如在緩衝快取中執行的頁面更新)都會被記錄:PostgreSQL 建立一筆日誌條目,包含日後重做該操作所需的全部關鍵資訊
與頁面修改相關的日誌條目,必須在被修改的頁面本身之前寫入磁碟——日誌的名稱即由此而來:預寫式日誌(write-ahead log,WAL)。
這項要求保證了:故障發生時 PostgreSQL 能從磁碟讀取 WAL 條目並重播它們,重做那些「已完成但結果仍在 RAM、當機前未落盤」的操作。
維護預寫式日誌通常比把隨機頁面寫到磁碟更有效率:
- WAL 條目構成連續的資料流,連 HDD 都能勝任
- WAL 條目通常比頁面小
哪些操作被記錄#
必須記錄所有在故障時可能破壞資料一致性的操作:
- 緩衝快取中執行的頁面修改——因為寫入被延後
- 交易提交與回滾——因為狀態變更發生在 CLOG 緩衝區中,不會立刻落盤
- 檔案操作(表格新增或移除時的檔案與目錄建立、刪除)——因為這類操作必須與資料變更同步
不被記錄的操作:
- 對
UNLOGGED表格的操作 - 對暫存表格的操作——它們的生命週期無論如何都受限於產生它們的 session
PostgreSQL 10 之前,雜湊索引也不被記錄。它們當時的唯一用途是把雜湊函式對應到不同資料型別。
除了當機復原,WAL 也用於從備份進行時間點還原(PITR)與複寫(replication)。
WAL 的結構#
邏輯結構#
就邏輯結構而言,WAL 是一串長度可變的日誌條目。每筆條目由標準標頭加上特定操作的資料構成,標頭提供的資訊包括:
- 與該條目相關的交易 ID
- 負責解讀該條目的資源管理器(resource manager)
- 用來偵測資料損毀的校驗和
- 條目長度
- 指向前一筆 WAL 條目的參照
WAL 通常朝前方向讀取,但
pg_rewind之類的工具可能會反向掃描——這正是「指向前一筆條目」的參照存在的理由。
WAL 資料本身可能有不同格式與意義。例如它可以是一段頁面片段,用來取代頁面中指定偏移量處的某部分。對應的資源管理器必須知道如何解讀與重播特定條目;表格、各類索引、交易狀態與其他實體各有獨立的管理器。
WAL 快取#
WAL 檔案在伺服器共享記憶體中佔用專屬緩衝區,其大小由 wal_buffers 參數定義。預設值 -1 表示自動選擇為緩衝快取總大小的 1/32。
WAL 快取與緩衝快取相當類似,但它通常以環形緩衝模式運作:新條目加到頭部,較舊的條目從尾部開始存到磁碟。
若 WAL 快取太小,磁碟同步的執行頻率會高於必要。
低負載下,插入位置(緩衝區頭部)幾乎總是與已存到磁碟的條目位置(尾部)相同:
=> SELECT pg_current_wal_lsn(), pg_current_wal_insert_lsn();
pg_current_wal_lsn | pg_current_wal_insert_lsn
−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−
0/3DF56000 | 0/3DF57968
(1 row)LSN#
要指涉特定條目,PostgreSQL 使用特殊資料型別 pg_lsn(log sequence number,日誌序號)。它表示從 WAL 起點到該條目的 64 位元位元組偏移量,顯示為兩個以斜線分隔的 32 位元十六進位數字。
實驗:更新一列後,插入 LSN 前進了:
=> BEGIN;
=> SELECT pg_current_wal_insert_lsn(); -- 0/3DF708D8
=> UPDATE wal SET id = id + 1;
=> SELECT pg_current_wal_insert_lsn(); -- 0/3DF70920頁面修改在 RAM 的緩衝快取中執行,這項變更也記錄在同樣位於 RAM 的 WAL 頁面中。
為確保被修改的資料頁面嚴格在對應 WAL 條目之後才刷到磁碟,頁首存放了與該頁面相關的最新 WAL 條目的 LSN:
=> SELECT lsn FROM page_header(get_raw_page('wal',0)); lsn −−−−−−−−−−−− 0/3DF70920 (1 row)
整個資料庫叢集只有一個 WAL,新條目不斷被附加其後。因此頁面中存放的 LSN 可能比稍早 pg_current_wal_insert_lsn 回傳的值還小;但若系統中什麼都沒發生,這兩個數字會相同。
CLOG 頁面的處理#
提交操作同樣被記錄,插入 LSN 再次改變。提交會更新 CLOG 頁面中的交易狀態,這些頁面存放在自己的快取中(共享記憶體中通常佔 128 個頁面)。
為確保 CLOG 頁面不會在對應 WAL 條目之前落盤,CLOG 頁面也必須追蹤最新 WAL 條目的 LSN——但這項資訊存放在 RAM 中,而非頁面本身。
到了某個時點 WAL 條目會落盤,屆時 CLOG 與資料頁面才可能被淘汰出快取。若它們必須更早被淘汰,系統會發現這一點,並強制先把 WAL 條目寫到磁碟。
計算 WAL 條目的大小#
若知道兩個 LSN 位置,只要相減(並轉型為 pg_lsn)就能算出兩者之間 WAL 條目的位元組數:
=> SELECT '0/3DF70948'::pg_lsn - '0/3DF708D8'::pg_lsn;
?column?
−−−−−−−−−−
112
(1 row)本例中 UPDATE 與 COMMIT 相關的 WAL 條目大約佔一百個位元組。
同樣的方法可用來估算特定工作負載每單位時間產生的 WAL 量——這項資訊是設定檢查點時所必需的。
實體結構#
磁碟上,WAL 存放在 PGDATA/pg_wal 目錄中,分成獨立的檔案(區段,segment)。其大小由唯讀參數 wal_segment_size 顯示(預設 16 MB)。
對高負載系統而言,加大區段大小是合理的,可能減少開銷。但這項設定只能在叢集初始化時修改(
initdb --wal-segsize)。
WAL 條目寫入目前檔案直到空間用盡,接著 PostgreSQL 開啟新檔案。可以查出某條目位於哪個檔案、以及距檔案起點的偏移量:
=> SELECT file_name, upper(to_hex(file_offset)) file_offset
FROM pg_walfile_name_offset('0/3DF708D8');
file_name | file_offset
−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−
00000001000000000000003D | F708D8
(1 row)檔名由兩部分構成:
- 最高的 8 位十六進位數字定義從備份還原時使用的時間線(timeline)
- 其餘部分代表 LSN 的高 64 位元(低位元則顯示在
file_offset欄位)
用 pg_waldump 檢視條目#
pg_waldump 工具能依 LSN 範圍或特定交易 ID 過濾 WAL 條目。它必須以 postgres 作業系統使用者的身分執行,因為它需要存取磁碟上的 WAL 檔案。
postgres$ /usr/local/pgsql/bin/pg_waldump \
-p /usr/local/pgsql/data/pg_wal -s 0/3DF708D8 -e 0/3DF70948
rmgr: Heap len (rec/tot): 69/ 69, tx: 886, lsn:
0/3DF708D8, prev 0/3DF708B0, desc: HOT_UPDATE off 1 xmax 886 flags
0x40 ; new off 2 xmax 0, blkref #0: rel 1663/16391/16562 blk 0
rmgr: Transaction len (rec/tot): 34/ 34, tx: 886, lsn:
0/3DF70920, prev 0/3DF708D8, desc: COMMIT 2023−03−06 14:01:48.875861
MSK- 第一筆是由 Heap 資源管理器處理的
HOT_UPDATE操作;blkref欄位顯示被更新 heap 頁面的檔名與頁面 ID - 第二筆是由 Transaction 資源管理器監管的
COMMIT操作
檢查點#
要在故障後還原資料一致性(也就是執行復原),PostgreSQL 必須向前重播 WAL,把代表遺失變更的條目套用到對應頁面。判斷什麼被遺失的方式,是比較磁碟上頁面的 LSN 與 WAL 條目的 LSN。
但該從哪一點開始復原?
- 起點太晚 → 在該點之前寫入磁碟的頁面將收不到全部變更,導致不可逆的資料損毀
- 從最開頭起 → 不切實際:既無法存放如此龐大的資料量,也無法接受如此漫長的復原時間
我們需要一個逐步向前移動的檢查點(checkpoint),使得從該點開始復原是安全的,並讓所有更早的 WAL 條目可以被移除。
最直截了當的做法是週期性地暫停所有系統操作、強制把所有髒頁寫到磁碟。這當然不可接受——系統會停頓一段不確定但相當長的時間。
因此檢查點被攤開在一段時間內,實質上構成一個區間。檢查點的執行由一個稱為 checkpointer 的特殊背景行程負責。
三個階段#
1. 檢查點開始
checkpointer 行程把所有能瞬間寫出的東西刷到磁碟:CLOG 交易狀態、子交易中繼資料,以及少數其他結構。
2. 檢查點執行
檢查點的大部分時間花在把髒頁刷到磁碟:
- 首先,在檢查點開始時為髒的所有緩衝區標頭中設一個特殊標記。這步非常快,因為不涉及 I/O
- 接著 checkpointer 走訪所有緩衝區,把被標記的寫到磁碟。它們的頁面不會被淘汰出快取——只是被寫下來,因此使用計數與 pin 計數可以忽略
- 頁面依 ID 順序處理,盡可能避免隨機寫入。為了更好的負載平衡,PostgreSQL 會在不同表空間之間輪替(它們可能位於不同實體裝置上)
- backend 也可以寫出被標記的緩衝區——如果它們先碰到的話。無論如何,緩衝區標記在此階段就被移除,因此就檢查點而言每個緩衝區只會被寫一次
檢查點進行期間,頁面當然仍可能在緩衝快取中被修改。但由於新的髒緩衝區不會被標記,checkpointer 會忽略它們。
3. 檢查點完成
當檢查點開始時為髒的所有緩衝區都已寫入磁碟,檢查點即視為完成。
從這一刻起(而非更早!),檢查點的「開始」位置才會被用作新的復原起點。所有在該點之前寫入的 WAL 條目都不再需要。

圖 10-1:故障發生時,復原所需的 WAL 檔案自最後一個完成檢查點的開始位置起算

圖 10-2:新檢查點完成後,復原起點前移,先前的 WAL 檔案不再需要
最後 checkpointer 建立一筆對應檢查點完成的 WAL 條目,其中指明檢查點的開始 LSN。由於檢查點開始時不記錄任何東西,這個 LSN 可能屬於任何類型的 WAL 條目。
PGDATA/global/pg_control 檔案也會更新為指向最新完成的檢查點(在此過程結束前,pg_control 仍保留前一個檢查點)。

圖 10-3:檢查點的時序——由開始到完成,以及 pg_control 中記錄的最新檢查點與其 REDO 位置
實驗:手動觸發檢查點並在 WAL 中找到它
先弄髒一批快取頁面並記下目前 LSN:
=> UPDATE big SET s = 'FOO';
=> SELECT count(*) FROM pg_buffercache WHERE isdirty; -- 4119
=> SELECT pg_current_wal_insert_lsn(); -- 0/3E7EF7E0手動完成檢查點,所有髒頁被刷到磁碟:
=> CHECKPOINT;
=> SELECT count(*) FROM pg_buffercache WHERE isdirty; -- 0檢視 WAL:
rmgr: XLOG len (rec/tot): 114/ 114, tx: 0, lsn:
0/3E7EF818, prev 0/3E7EF7E0, desc: CHECKPOINT_ONLINE redo
0/3E7EF7E0; tli 1; ... online最新的 WAL 條目與檢查點完成(CHECKPOINT_ONLINE)有關。該檢查點的開始 LSN 標示在 redo 一詞之後,這個位置對應檢查點開始當時最後插入的 WAL 條目。
同樣的資訊也能在 pg_control 檔案中找到:
postgres$ pg_controldata -D /usr/local/pgsql/data | egrep 'Latest.*location'
Latest checkpoint location: 0/3E7EF818
Latest checkpoint's REDO location: 0/3E7EF7E0復原#
伺服器啟動時第一個被拉起的行程是 postmaster,它接著衍生 startup 行程,由後者負責故障時的資料復原。
startup 行程讀取 pg_control 檔案並檢查叢集狀態,以判斷是否需要復原:
- 正常停止的伺服器狀態為
shut down - 未運行的伺服器若狀態為
in production,即代表發生過故障
此時 startup 行程會自動從同一個 pg_control 檔案中所記錄的「最新完成檢查點的開始 LSN」發起復原。
若
PGDATA目錄中含有與備份相關的backup_label檔案,起始 LSN 位置就取自該檔案。
復原如何進行#
startup 行程從指定位置開始逐筆讀取 WAL 條目,並在頁面的 LSN 小於 WAL 條目的 LSN 時把條目套用到資料頁面。
若頁面含有較大的 LSN,WAL 不該被套用——事實上是必須不能套用,因為這些條目被設計成嚴格循序重播。
然而有些 WAL 條目構成完整頁面映像(full page image,FPI)。這類條目可以套用到頁面的任何狀態,因為頁面內容反正會被全部覆蓋——這種修改稱為冪等(idempotent)。
另一個冪等操作的例子是登記交易狀態變更:每個交易狀態在 CLOG 中由特定位元定義,這些位元的設定與先前值無關——因此 CLOG 頁面不需要保存最新變更的 LSN。
WAL 條目套用到緩衝快取中的頁面,與正常運作期間的一般頁面更新完全相同。檔案也以類似方式從 WAL 還原:例如某 WAL 條目顯示某檔案必須存在,但它因故不見了,該檔案就會被重新建立。
復原結束後:
- 所有 unlogged 關聯被對應的初始化分支覆寫
- 執行一次檢查點,把復原後的狀態固化到磁碟
- startup 行程的工作至此完成
側註:為何 PostgreSQL 不需要傳統的 roll-back 階段
古典形式的復原流程包含兩個階段:
- roll-forward 階段:重播 WAL 條目,重做遺失的操作
- roll-back 階段:伺服器中止那些在故障時尚未提交的交易
在 PostgreSQL 中,第二階段不需要。復原之後,未完成的交易在 CLOG 中既沒有 commit 位元也沒有 abort 位元(技術上這代表一筆「活躍」交易),但既然可以確定該交易已不再運行,它就會被視為已中止。
實驗:模擬當機並觀察復原日誌
以 immediate 模式強制停止伺服器來模擬故障:
postgres$ pg_ctl stop -m immediate
postgres$ pg_controldata -D /usr/local/pgsql/data | grep 'state'
Database cluster state: in production啟動伺服器,startup 行程發現發生過故障並進入復原模式:
LOG: database system was interrupted; last known up at 2023−03−06 14:01:49 MSK
LOG: database system was not properly shut down; automatic recovery in progress
LOG: redo starts at 0/3E7EF7E0
LOG: invalid record length at 0/3E7EF890: wanted 24, got 0
LOG: redo done at 0/3E7EF818
LOG: database system is ready to accept connections相對地,若伺服器被正常停止,postmaster 會中斷所有客戶端連線,然後執行最終檢查點把所有髒頁刷到磁碟。此時叢集狀態為 shut down,而 WAL 末尾可看到 CHECKPOINT_SHUTDOWN 條目。
背景寫入#
若 backend 需要把髒頁淘汰出緩衝區,它就得把該頁寫到磁碟。這種情況不受歡迎,因為會造成等待——在背景非同步執行寫入好得多。
這項工作有一部分由 checkpointer 承擔,但仍然不夠。
因此 PostgreSQL 提供另一個專責背景寫入的行程:bgwriter。它依賴與淘汰相同的緩衝區搜尋演算法,但有兩項主要差異:
- bgwriter 使用自己的時鐘指標,它永遠不落後於淘汰用的指標,通常還會超前
- 走訪緩衝區時,不會減少使用計數
髒頁在「緩衝區未被 pin 住且使用計數為零」時被刷到磁碟。因此 bgwriter 跑在淘汰之前,主動把極可能很快被淘汰的頁面寫到磁碟,提高了「被選中淘汰的緩衝區是乾淨的」機率。
WAL 設定#
設定檢查點#
檢查點的持續時間(更精確地說,是寫出髒緩衝區的時間)由 checkpoint_completion_target 定義(預設 0.9)。它的值指定相鄰兩個檢查點開始之間的時間有多少比例分配給寫入。
其他參數的設定可依以下步驟:
- 決定相鄰兩個檢查點之間應儲存的 WAL 檔案量。量越大開銷越小,但這個值終究受限於可用空間與可接受的復原時間。
- 估算正常負載下產生這個量所需的時間:記下起始的插入 LSN,並不時檢查它與目前插入位置的差值。
- 把得到的數字當作典型的檢查點間隔,用作
checkpoint_timeout的值。預設 5 分鐘很可能太小,通常會調高,例如調到 30 分鐘。 - 但負載有時(甚至很可能)會更高,導致該區間內產生的 WAL 檔案過大。此時檢查點必須更頻繁地執行——用
max_wal_size(預設 1 GB)限制復原所需的 WAL 檔案大小;超過此門檻時伺服器會發起額外的檢查點。
復原所需的 WAL 檔案,同時包含「最新完成的檢查點」與「目前尚未完成的檢查點」的條目。因此估算總量時,應把算出的檢查點間 WAL 大小乘以 1 +
checkpoint_completion_target。(PostgreSQL 11 之前保留兩個已完成檢查點的 WAL 檔案,乘數為 2 +
checkpoint_completion_target。)
依此設定,多數檢查點依排程執行(每 checkpoint_timeout 一次);但負載升高時,就在 WAL 大小超過 max_wal_size 時被觸發。
進度追蹤機制#
實際進度會週期性地與預期值比對:
| 指標 | 定義 |
|---|---|
| 實際進度 | 已處理的快取頁面比例 |
| 預期進度(依時間) | 已流逝的時間比例,假設檢查點必須在 checkpoint_timeout × checkpoint_completion_target 區間內完成 |
| 預期進度(依大小) | 已填滿的 WAL 檔案比例,其預期數量依 max_wal_size × checkpoint_completion_target 估算 |
若髒頁寫入超前排程,checkpointer 會暫停一會兒;若任一參數有落後,它會盡快追上。由於時間與資料量都被納入考量,PostgreSQL 能用同一套方法管理排程檢查點與按需檢查點。
WAL 檔案的回收#
一般情況下,磁碟上 WAL 檔案的大小變化如下:

圖 10-4:正常運作下磁碟上 WAL 大小的變化——每次檢查點之間累積,完成後回收
檢查點完成後,復原不再需要的 WAL 檔案會被刪除;但會保留數個檔案(總量最多 min_wal_size,預設 80 MB)供重複使用,只是單純改名。
這種改名減少了不斷建立與刪除檔案的開銷。若不需要,可用
wal_recycle參數關閉此功能。
max_wal_size指定的是期望的目標值,而非硬性上限。負載尖峰時寫入可能落後於排程- 伺服器無權刪除尚待複寫或尚待持續封存處理的 WAL 檔案。若啟用了這些功能,必須持續監控,因為它很容易造成磁碟爆滿
- 可透過
wal_keep_size參數保留一定量的空間存放 WAL 檔案
設定背景寫入#
設定好 checkpointer 之後,也應設定 bgwriter。這兩個行程必須合力在 backend 需要重用髒緩衝區之前就把它們寫到磁碟。
bgwriter 運作時會週期性暫停,每次睡眠 bgwriter_delay(預設 200 ms)。
兩次暫停之間寫出的頁面數量,取決於自上次執行以來 backend 存取的平均緩衝區數量(PostgreSQL 使用移動平均來平滑尖峰,同時避免依賴過舊的資料)。算出的數字再乘以 bgwriter_lru_multiplier(預設 2);但無論如何,單次執行寫出的頁面數不能超過 bgwriter_lru_maxpages(預設 100)。
若偵測不到髒緩衝區(也就是系統中什麼都沒發生),bgwriter 會睡到某個 backend 存取緩衝區為止,屆時它會醒來繼續正常運作。
監控#
檢查點設定可以、也應該依監控資料調校。
- 若依大小觸發的檢查點執行得比
checkpoint_warning(預設 30 秒)所定義的更頻繁,PostgreSQL 會發出警告。這項設定應與預期的尖峰負載保持一致。 log_checkpoints參數(預設 off)啟用後,會把檢查點相關資訊印到伺服器日誌:
LOG: checkpoint complete: wrote 4100 buffers (25.0%); 0 WAL file(s)
added, 1 removed, 0 recycled; write=0.076 s, sync=0.009 s,
total=0.099 s; sync files=3, longest=0.007 s, average=0.003 s;
distance=9213 kB, estimate=9213 kB日誌顯示寫出的緩衝區數量、檢查點後 WAL 檔案變化的統計、檢查點持續時間,以及相鄰兩個檢查點開始之間的距離(位元組)。
pg_stat_bgwriter#
對設定決策最有幫助的資料,是 pg_stat_bgwriter 視圖提供的背景寫入與檢查點執行統計。
9.2 版之前兩項任務都由 bgwriter 執行;之後引入了獨立的 checkpointer 行程,但這個共用視圖的名稱維持不變。
檢查點次數:
checkpoints_timed——排程檢查點(達到checkpoint_timeout時觸發)checkpoints_req——按需檢查點(包含達到max_wal_size時觸發者)
寫出頁面數的統計:
buffers_checkpoint——由 checkpointer 寫出buffers_backend——由 backend 寫出buffers_clean——由 bgwriter 寫出
在設定良好的系統中,
buffers_backend必須遠低於buffers_checkpoint與buffers_clean之和。設定背景寫入時要留意
maxwritten_clean:它顯示 bgwriter 因超過bgwriter_lru_maxpages門檻而不得不停下來的次數。
以下呼叫會清除已蒐集的統計:
=> SELECT pg_stat_reset_shared('bgwriter');