PostgreSQL 以固定大小的數據塊(Page)存儲數據,默認大小為 8 KB。當客户端執行更新或插入操作時,PostgreSQL 並不會立即將變更寫入磁盤,而是先將相關數據頁加載到共享內存(Shared Buffers)中,在內存中完成修改,並將該頁面標記為“髒頁”。所謂“髒頁”,是指內存中的頁面版本已經新於磁盤上的對應版本。
在向客户端返回操作結果之前,PostgreSQL 會先將變更記錄寫入預寫日誌(Write-Ahead Log,WAL),以保證即使數據庫發生崩潰也能恢復數據一致性。但實際的數據文件並不會立刻更新,只有在檢查點(Checkpoint)觸發或後台寫進程執行刷新時,髒頁才會被寫回磁盤。
在此之前,髒頁會持續累積在內存中,直到通過以下三種機制之一被刷新:
- 後台寫進程(
Background Writer,BGWriter):一個常駐後台進程,在可用的乾淨緩衝區數量下降時,持續將髒頁寫入磁盤。 - 檢查點進程(Checkpointer):在觸發檢查點時(如達到
checkpoint_timeout或 WAL 超過max_wal_size),將所有髒頁刷新到磁盤。 - 後端進程(Backend):在緊急情況下(如共享緩衝區幾乎全部為髒頁),普通後端進程會自行寫髒頁,可能導致用户查詢阻塞。
理解併合理控制髒頁的刷新時機與方式,是優化 PostgreSQL 性能的關鍵。
髒頁為何影響性能
髒頁會從多個方面影響數據庫性能:
1. 檢查點期間的 I/O 峯值
檢查點發生時,所有髒頁都必須被刷新到磁盤。如果髒頁數量較多,會在短時間內產生大量磁盤 I/O,影響其他查詢性能。checkpoint_timeout、checkpoint_completion_target 和 max_wal_size 等參數決定了檢查點觸發的頻率以及刷新髒頁的節奏。
2. 後端寫入帶來的查詢阻塞
當共享緩衝區被大量髒頁佔滿,而 BGWriter 無法及時清理時,後端進程將被迫自行寫盤,直接阻塞正在執行的用户查詢。為避免此類情況,應通過合理的內存與刷新參數配置,讓 BGWriter 承擔絕大多數寫入工作。實踐中通常包括:
- 為
shared_buffers分配足夠內存; - 調整
bgwriter_delay、bgwriter_lru_maxpages、bgwriter_lru_multiplier、bgwriter_flush_after等參數,使髒頁持續、平穩地寫入磁盤; - 通過增大
checkpoint_timeout、提高checkpoint_completion_target以及增大max_wal_size,減少檢查點引發的突發寫入。
3. 吞吐量與崩潰恢復時間的權衡
較少的刷新頻率(如較大的 checkpoint_timeout)可以降低 I/O 開銷,但會增加數據庫崩潰後需要回放的 WAL 數量;更頻繁的刷新可以加快恢復速度,但可能降低運行時性能。合理的參數配置需要根據實際業務負載在兩者之間取得平衡。
PostgreSQL 管理髒頁的核心機制
後台寫進程(Background Writer,BGWriter)
BGWriter 是一個獨立進程,其目標是在後台持續寫出髒頁,保持一定數量的乾淨緩衝區可用。根據官方文檔描述:
- 當共享緩衝區中可用的乾淨頁面數量低於閾值時,BGWriter 會寫出部分髒頁並將其標記為乾淨。
- 若同一頁面在一個檢查點週期內被多次修改,可能會被多次寫盤,從而增加總體 I/O。
BGWriter 的主要配置參數(位於 postgresql.conf)包括:
調優建議:
如果在 pg_stat_bgwriter 中觀察到後端進程寫盤較多,應適當提高 bgwriter_lru_maxpages 和 bgwriter_lru_multiplier;若 BGWriter 本身導致 I/O 過高,則可適當降低相關參數。通過調整 bgwriter_delay,在寫入頻率與 CPU 開銷之間取得平衡。
檢查點(Checkpointer)
檢查點觸發時,PostgreSQL 會將所有髒頁寫入磁盤,並在 WAL 中記錄檢查點位置。合理調整檢查點參數有助於平滑 I/O 壓力:
通過增大 checkpoint_timeout 和 checkpoint_completion_target,可以將寫入分散到更長時間窗口內;max_wal_size 決定了自動檢查點的觸發時機,從而間接影響髒頁刷新頻率。
共享緩衝區(Shared Buffers)
shared_buffers 決定了 PostgreSQL 可用於緩存數據頁和存放髒頁的內存大小。該參數直接影響頁面在內存中的停留時間以及寫盤頻率。
在專用數據庫服務器上,通常建議將其設置為物理內存的 25%~ 40%。當 shared_buffers 較大時,往往需要相應提高 max_wal_size,以避免檢查點過於頻繁。
shared_buffers 過小會導致髒頁頻繁被淘汰,引發後端寫盤;設置過大則可能在檢查點時一次性刷新大量頁面,造成 I/O 峯值。合理的 shared_buffers 配合 BGWriter 調優,可以顯著減少後端寫入。
自動清理(Autovacuum) 與 凍結清理(Vacuum Freeze)
自動清理(Autovacuum)在更新可見性信息或凍結元組(Freeze)時,也會產生髒頁。應確保 Autovacuum 運行頻率足以防止表膨脹,但又不過於激進,以免產生不必要的寫入。可根據業務負載調整 autovacuum_vacuum_cost_limit 和 autovacuum_vacuum_scale_factor。在 SSD 環境下,適當提高 Autovacuum 強度通常是有益的。
面向性能的調優實踐
1. 先監控,再調優
通過 pg_stat_bgwriter 重點關注以下指標:
buffers_checkpoint:檢查點寫出的髒頁數量。buffers_clean:BGWriter 寫出的頁面數量。buffers_backend:後端進程寫出的頁面數量。
目標是將 buffers_backend 控制在接近 0 的水平。
2. 合理配置共享緩衝區大小
以物理內存的 25% 作為起點,根據負載特徵調整:
- 若業務場景以讀操作為主,可適當增大共享緩衝區。
- 若寫操作導致檢查點產生大幅性能波動,則需調小共享緩衝區。
3. 調整 BGWriter 參數
- 降低
bgwriter_delay參數值(如設置為 100 毫秒),提高後台寫入器的喚醒頻率。 - 針對高寫入負載場景,增大
bgwriter_lru_maxpages(建議取值範圍 200–1000)與bgwriter_lru_multiplier(建議取值範圍 3–4)參數,提升單次週期內的髒頁處理能力。 - 將
bgwriter_flush_after參數設為與存儲系統最佳寫入粒度匹配的值;對於固態硬盤,512KB–1MB 為推薦取值區間。
4. 優化檢查點行為
- 增大
checkpoint_timeout參數,降低檢查點觸發頻率(建議取值範圍 15–60 分鐘)。 - 將
checkpoint_completion_target參數提升至 0.7–0.9,使檢查點的寫入操作均勻分佈。 - 增大 max_wal_size 參數,避免檢查點被過度頻繁觸發。
5. 避免後端寫入
當 buffers_backend 持續增長時,應優先增加 shared_buffers 或增強 BGWriter 的寫盤能力。後端寫盤往往是查詢延遲的主要來源。
6. 操作系統層面優化
- Linux 系統中,應避免
vm.dirty_background_ratio和vm.dirty_ratio設置過高,以免內核長時間累積髒頁,造成突發寫回。 - 關閉透明大頁(THP),並在內存充足的服務器上啓用靜態大頁(Huge Pages),以提升整體性能。
7. 持續評估與迭代
不同業務負載差異較大,應結合 pg_stat_activity、pg_stat_bgwriter 以及 PostgreSQL 17 引入的 pg_stat_io 等視圖,持續評估調優效果,並逐步調整參數。
總結
“髒頁”本質上是 PostgreSQL 內存中等待寫回磁盤的已修改數據頁。通過髒頁機制,PostgreSQL 能夠合併寫入操作,並藉助 WAL 保證崩潰安全性。但如果相關參數配置不當,髒頁處理可能引發 I/O 峯值和查詢延遲。
深入理解共享緩衝區、後台寫進程、檢查點與 WAL 的協同機制,併合理調優 bgwriter_delay、bgwriter_lru_maxpages、bgwriter_lru_multiplier、checkpoint_timeout、shared_buffers 等關鍵參數,有助於在保障數據可靠性的同時,實現平穩、可預測的數據庫性能。
原文鏈接:
https://stormatics.tech/blogs/what-are-dirty-pages-in-postgresql
作者:Umair Shahid