效能#
伺服器正常運行時,WAL 檔案不斷被寫到磁碟。但這些寫入是循序的,幾乎沒有隨機存取,連 HDD 都能勝任。
由於這類負載與典型的資料檔存取非常不同,為 WAL 檔案設置獨立的實體儲存可能是值得的——把
PGDATA/pg_wal目錄換成指向已掛載檔案系統中某目錄的符號連結。
有兩種情況 WAL 檔案既要寫也要讀:一是明顯的當機復原;二是串流複寫——
walsender行程直接從檔案讀取 WAL 條目。因此若備援節點未能在所需頁面還在主伺服器 OS 緩衝區時就收到 WAL 條目,資料就得從磁碟讀取。但這種存取仍是循序而非隨機的。
WAL 條目的寫入有兩種模式,由 synchronous_commit 參數決定(預設 on):
| 模式 | 行為 |
|---|---|
| 同步 | 在交易提交把所有相關 WAL 條目存到磁碟之前,禁止任何後續操作 |
| 非同步 | 交易立即提交,WAL 條目稍後在背景寫到磁碟 |
同步模式#
要可靠地登記「提交」這件事,光把 WAL 條目交給作業系統是不夠的——必須確認磁碟同步已成功完成。由於同步意味著真正的 I/O 操作(相當慢),盡可能少做是有益的。
為此,完成交易並把 WAL 條目寫到磁碟的 backend 可以做一個由 commit_delay 定義的短暫暫停。但這只有在系統中至少有 commit_siblings(預設 5)筆活躍交易時才會發生:暫停期間其中一些可能結束,伺服器就能一次同步所有 WAL 條目。
這很像幫人按住電梯門等他衝進來。
預設沒有暫停(
commit_delay = 0)。只有對「執行大量短 OLTP 交易」的系統,修改commit_delay才有意義。
可能的暫停之後,完成交易的行程把所有累積的 WAL 條目刷到磁碟並執行同步(重要的是存下提交條目以及該交易所有先前的條目;其餘的一併寫出只是因為這不會增加成本)。
從這一刻起,ACID 的持久性要求得到保證——交易被視為已可靠提交。這正是同步模式作為預設的原因。
代價是較長的延遲(
COMMIT指令在同步結束前不會返回控制權)與較低的系統吞吐量,對 OLTP 負載尤其明顯。
非同步模式#
關閉 synchronous_commit 參數即啟用非同步提交。此模式下 WAL 條目由 walwriter 行程寫到磁碟,該行程在工作與睡眠之間交替,暫停時長由 wal_writer_delay 定義(預設 200 ms)。
從暫停中醒來時,walwriter 檢查快取中是否有新的完全填滿的 WAL 頁面:
- 有 → 寫出它們,跳過目前這一頁
- 沒有 → 既然已經醒了,就把目前這個半空的頁面寫出
這個演算法的目的是避免同一頁面被刷出好幾次,對變更密集的工作負載帶來顯著的效能提升。
儘管 WAL 快取以環形緩衝的方式使用,walwriter 在抵達快取的最後一頁時會停下;暫停之後,下一個寫入週期從第一頁開始。因此最壞情況下 walwriter 需要三趟才能處理到某個特定的 WAL 條目:先寫完快取尾端的所有滿頁,再回到開頭,最後才處理含該條目的未滿頁面。但多數情況只需一到兩趟。
每寫出
wal_writer_flush_after(預設 1 MB)的資料量就執行一次同步,寫入週期結束時再執行一次。
非同步提交比同步快,因為不必等待實體寫入磁碟。但可靠性受損:你可能遺失故障前 3 ×
wal_writer_delay時間範圍內已提交的資料(預設即 0.6 秒)。
兩種模式其實互補#
- 同步模式下,長交易的 WAL 條目仍可以被非同步寫出,以釋放 WAL 緩衝區
- 非同步模式下,即將被淘汰出緩衝快取之頁面所對應的 WAL 條目,仍會立即刷到磁碟——否則根本無法繼續運作
多數情況下,效能與持久性之間的艱難抉擇必須由系統設計者做出。
synchronous_commit也能為個別交易設定。若能在應用層把交易分成「絕對關鍵」(如處理財務資料)與「較不重要」兩類,你就能在只冒著遺失非關鍵交易的風險下提升效能。
實測:pgbench 對比同步/非同步模式
30 秒 pgbench 測試,同步模式:
number of transactions actually processed: 20123
latency average = 1.491 ms
tps = 670.809688 (without initial connection time)非同步模式(ALTER SYSTEM SET synchronous_commit = off):
number of transactions actually processed: 61809
latency average = 0.485 ms
tps = 2060.399861 (without initial connection time)非同步模式下,這個簡單基準測試顯示延遲顯著更低、吞吐量顯著更高。當然每個系統的實際數字依當下負載而異,但可以清楚看到:對短 OLTP 交易的影響相當可觀。
容錯#
預寫式日誌必須在任何情況下都能保證當機復原(除非永久儲存本身壞了)。影響資料一致性的因素很多,這裡涵蓋最重要的三個:快取、資料損毀、非原子寫入。
快取#
資料在抵達非揮發性儲存(如硬碟)之前,會經過多層快取:
- 磁碟寫入只是指示作業系統把資料放進它的快取(這快取也在 RAM 中,同樣怕當機),實際寫入由 OS 的 I/O 排程器非同步執行
- 排程器決定刷出時,資料被移到儲存裝置的快取(如 SSD),裝置也可能延後寫入(例如為了把相鄰頁面歸為一組)
- RAID 控制器在磁碟與作業系統之間再加一層快取
除非採取特殊措施,否則「資料何時被可靠地存到磁碟」始終是未知的。這通常不太要緊,因為我們有 WAL——但 WAL 條目本身必須立刻被可靠地存到磁碟。
這對非同步模式同樣成立,否則就無法保證 WAL 條目比被修改的資料更早落盤。
checkpointer 行程也必須以可靠的方式存資料,確保髒頁從 OS 快取寫進磁碟。此外它還必須同步其他行程執行過的所有檔案操作(頁面寫入、檔案刪除):檢查點完成時,這些動作的結果都必須已存在磁碟上。
作業系統提供各種手段保證資料立即寫入非揮發性儲存,歸結為兩種主要做法:
- 寫入後呼叫獨立的同步指令(如
fsync或fdatasync) - 在開啟檔案或寫入時指定「必須同步」(甚至繞過 OS 快取直接寫入)
微妙之處在於:最合適的方法取決於硬體。例如若使用帶備援電池的控制器,就能善用它的快取——電池會在斷電時保護資料。
- 關閉同步(
fsync參數)能提升系統效能,但任何故障都將導致致命的資料遺失- 非同步模式保證能復原到一致狀態,只是最近的部分資料更新可能不見
資料損毀#
技術設備並不完美,資料可能在記憶體中、磁碟上,或透過介面纜線傳輸時受損。這類錯誤通常在硬體層級被處理,但仍有一些會漏網。
為及時捕捉問題,PostgreSQL 總是以校驗和保護 WAL 條目。
資料頁面的校驗和也可以計算——在叢集初始化時啟用,或在伺服器停止時執行
pg_checksums工具啟用。
正式系統中校驗和必須永遠啟用,儘管有(輕微的)計算與驗證開銷。它提高了及時發現損毀的機會,不過仍有一些死角:
- 校驗和只在頁面被存取時驗證,因此資料損毀可能長時間不被察覺,直到它進入所有備份、再也沒有正確資料的來源
- 全零的頁面被視為正確,因此若檔案系統誤把某頁清零,這個問題不會被發現
- 校驗和只為關聯的主分支計算;其他分支與檔案(如 CLOG 中的交易狀態)不受保護
實驗:破壞頁首並觀察校驗和報錯
確認校驗和已啟用:
=> SHOW data_checksums; -- on停止伺服器,把表格主分支第零頁的前 8 個位元組清零:
postgres$ pg_ctl stop
postgres$ dd if=/dev/zero of=/usr/local/pgsql/data/base/16391/16562 \
oflag=dsync conv=notrunc bs=1 count=8重啟後嘗試讀取:
=> SELECT * FROM wal LIMIT 1;
WARNING: page verification failed, calculated checksum 20397 but
expected 28733
ERROR: invalid page in block 0 of relation base/16391/16562若資料無法從備份還原,至少可以嘗試讀取受損頁面(冒著取得亂碼的風險),為此須啟用 ignore_checksum_failure:
=> SET ignore_checksum_failure = on;
=> SELECT * FROM wal LIMIT 1;
WARNING: page verification failed, ...
id
−−−−
2
(1 row)本例一切順利,因為我們破壞的是頁首中非關鍵的部分(最新 WAL 條目的 LSN),而非資料本身。
非原子寫入#
資料庫頁面通常佔 8 kB,但低層級的寫入是以區塊為單位,而區塊往往更小(典型為 512 位元組或 4 kB)。因此故障發生時,一個頁面可能只被寫入了一部分。
對這種頁面套用一般的 WAL 條目毫無意義——它會產生損毀的結果。
為避免部分寫入,PostgreSQL 在頁面於檢查點開始後第一次被修改時,把一份完整頁面映像(full page image,FPI)存進 WAL。這個行為由 full_page_writes 參數控制(預設 on)。
復原過程若遇到 WAL 中的 FPI,會無條件把它寫到磁碟(不檢查其 LSN);與任何 WAL 條目一樣,FPI 受校驗和保護,因此損毀不會被忽略。接著一般的 WAL 條目才套用到這個保證正確的狀態上。
提示位元與 FPI 的關聯#
設定提示位元沒有獨立的 WAL 條目類型:這項操作被視為非關鍵,因為任何存取該頁面的查詢都會重新設定所需的位元。
然而任何提示位元變更都會影響頁面的校驗和。因此若校驗和已啟用(或
wal_log_hints為 on),提示位元的修改就會被記錄為 FPI。
儘管記錄機制會把空白空間排除在 FPI 之外,產生的 WAL 檔案大小仍顯著增加。
啟用
wal_compression參數進行 FPI 壓縮,能大幅改善這個情況。
實測:FPI 佔 WAL 的比例,以及壓縮的效果
執行檢查點後立即跑固定交易數的 pgbench:
=> CHECKPOINT;
=> SELECT pg_current_wal_insert_lsn(); -- 0/42CE5DA8postgres$ pgbench -t 20000 internals產生的 WAL 條目大小約 29 MB。pg_waldump --stats 顯示 FPI 佔總 WAL 的 71.54%:
Type N (%) Record size (%) FPI size (%)
XLOG 4294 ( 3,31) 210406 ( 2,50) 19820068 ( 93,78)
Transaction 20004 ( 15,41) 680536 ( 8,10) 0 ( 0,00)
Heap 80234 ( 61,81) 5946242 ( 70,73) 295664 ( 1,40)
Btree 494 ( 0,38) 32747 ( 0,39) 993860 ( 4,70)
Total 129808 8406672 [28,46%] 21134168 [71,54%]啟用壓縮後重跑同一實驗:
=> ALTER SYSTEM SET wal_compression = on;WAL 大小降到 10 MB,FPI 佔比降到 23.86%。
若資料頁面在兩次檢查點之間被修改多次,FPI 佔比會較小——這是「檢查點不要做太頻繁」的又一個理由。
總結:當因為啟用校驗和或
full_page_writes而產生大量 FPI 時(也就是幾乎總是如此),即使有額外的 CPU 開銷,使用壓縮仍是合理的。
WAL 層級#
預寫式日誌的主要目標是讓當機復原成為可能。但若擴大被記錄資訊的範圍,WAL 也能派上其他用場。
PostgreSQL 提供三種日誌層級,由 wal_level 參數定義(預設 replica,修改需重啟伺服器)。每個層級都包含前一層級所記錄的一切,再加上更多資訊。
minimal#
minimal 層級只保證當機復原。
為節省空間,在目前交易內建立或截斷的關聯上執行的操作,若涉及插入大量資料(如 CREATE TABLE AS SELECT 與 CREATE INDEX),不會被記錄。取而代之,所有必要資料立即被刷到磁碟,系統目錄的變更則在交易提交後才可見。
這為何安全:
- 若這類操作被故障中斷,已落盤的資料仍然不可見,不影響一致性
- 若故障發生在操作完成之後,套用後續 WAL 條目所需的全部資料都已存到磁碟
要讓這項最佳化生效,寫入新建關聯的資料量門檻由
wal_skip_threshold參數定義(預設 2 MB)。若要選用
minimal層級,還必須把max_wal_senders設為 0(PostgreSQL 13 起)。
實驗顯示:在同一筆交易內 TRUNCATE 再插入 100,000 列,產生的 WAL 只有——
- 一筆記錄關聯新檔案建立的
Storage / CREATE條目 - 四筆系統目錄操作條目(
pg_class表格與其三個索引) - 一筆提交條目
資料插入完全沒有被記錄。
replica#
當機復原時,WAL 條目被重播以把磁碟資料還原到一致狀態。備份還原的運作方式類似,但它還能用 WAL 封存把資料庫狀態還原到指定的復原目標點。
被封存的 WAL 條目數量可能相當龐大(例如橫跨數天),因此復原期間會包含多個檢查點。
因此
minimal層級不夠:未被記錄的操作無法重做。備份還原要求 WAL 檔案包含所有操作。複寫亦然:未被記錄的指令不會被送到備援節點,也不會在其上重播。
若備援節點還要用來執行查詢,情況更複雜:
- 它需要知道主伺服器上取得的排他鎖資訊,因為那些鎖可能與備援節點上的查詢衝突
- 它必須能擷取快照,這需要活躍交易的資訊。而在備援節點上,本地交易與主伺服器上運行的交易都必須被納入考量
把這些資料送到備援節點的唯一方式,是週期性地把它們寫進 WAL 檔案。這由 bgwriter 行程執行,每 15 秒一次(此間隔寫死在程式碼中)。
相較於 minimal 層級,同樣的操作在 replica 層級額外產生:
- Standby 資源管理器的複寫相關條目:
RUNNING_XACTS(活躍交易)與LOCK - 記錄
INSERT+INIT操作的條目——它初始化一個新頁面並把新列插入該頁面
logical#
最後,logical 層級啟用邏輯解碼(logical decoding)與邏輯複寫。它必須在發佈端伺服器上啟用。
檢視 WAL 條目會發現這個層級與 replica 幾乎相同:它增加了與複寫來源相關的條目,以及應用程式可能產生的任意邏輯條目。
邏輯解碼大體上依賴活躍交易的資訊(
RUNNING_XACTS),因為它需要擷取快照以追蹤系統目錄的變更。