Buffer Management:PostgreSQL 如何用 Clock-Sweep 管理記憶體 | PostgreSQL
Buffer Management 是 PostgreSQL 記憶體架構的核心組件。Shared Buffer Pool 是所有後端程序共享的頁面快取,目標是讓高頻存取的資料頁留在記憶體中,避免反覆讀取磁碟。理解 Buffer 管理機制是效能調校的關鍵——它直接決定了你的查詢是從記憶體還是磁碟取得資料。
Shared Buffer Pool 概述
Shared Buffer Pool 位於 PostgreSQL 管理的共享記憶體中,大小由 shared_buffers 參數決定。所有 Backend Process 與 Background Worker 都存取同一個 Buffer Pool。
記憶體層次結構:
┌─────────────────────────────────────────────────────┐
│ Shared Memory(共享區域) │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Shared Buffer Pool │ │
│ │ (shared_buffers,預設 128MB) │ │
│ │ │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │ BUF │ │ BUF │ │ BUF │ │ BUF │ ... │ │
│ │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ │
│ │ │(8KB) │ │(8KB) │ │(8KB) │ │(8KB) │ │ │
│ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌────────────┐ │
│ │ WAL Buffers│ │ CLOG Cache │ (其他共享結構) │
│ └─────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────┘
每個 Buffer Slot 固定為 8KB(一個 PostgreSQL Page 的大小),Buffer Pool 中的 Slot 數量 = shared_buffers / 8KB。
Buffer 結構
每個 Buffer 由三個部分組成:
Buffer Tag(識別符)
用來識別此 Buffer 對應磁碟上的哪個頁面:
Buffer Tag:
┌──────────────────────────────────────────────────────┐
│ RelFileNode │
│ ┌──────────────┬───────────────┬──────────────────┐ │
│ │ spcOid │ dbOid │ relOid │ │
│ │ (Tablespace) │ (Database) │ (Relation) │ │
│ └──────────────┴───────────────┴──────────────────┘ │
│ ForkNumber(主資料 / FSM / Visibility Map) │
│ BlockNumber(頁面在 Relation 中的位置) │
└──────────────────────────────────────────────────────┘
Buffer Descriptor(元資料)
每個 Buffer 的控制結構,記錄狀態與鎖定資訊:
| 欄位 | 說明 |
|---|---|
buf_tag | Buffer Tag,此 Buffer 對應的頁面識別 |
refcount | Pin 計數,有多少程序正在使用此 Buffer |
usage_count | 使用頻率,Clock-Sweep 演算法使用(0-5) |
flags | dirty(已修改)、valid(有效)等旗標 |
content_lock | 保護 Buffer 內容的 LWLock |
io_in_progress_lock | I/O 進行中的鎖 |
Buffer Content(實際資料)
一個完整的 8KB PostgreSQL Page:
8KB Page 結構:
┌────────────────────────────────────────────────────┐
│ Page Header (24 bytes) │
│ lsn, checksum, flags, lower, upper, special │
├────────────────────────────────────────────────────┤
│ Line Pointer Array (4 bytes per slot) │
├────────────────────────────────────────────────────┤
│ Free Space │
├────────────────────────────────────────────────────┤
│ Tuple Data(從底部往上填充) │
└────────────────────────────────────────────────────┘
Clock-Sweep 替換演算法
當 Buffer Pool 已滿,需要新頁面時,PostgreSQL 使用 Clock-Sweep(時鐘掃描)演算法選擇替換哪個 Buffer。
Clock-Sweep 演算法示意:
Buffer Pool 陣列(簡化為 8 個 Buffer):
┌────┬────┬────┬────┬────┬────┬────┬────┐
│B1 │B2 │B3 │B4 │B5 │B6 │B7 │B8 │
│uc=2│uc=0│uc=3│uc=1│uc=0│uc=4│uc=2│uc=1│
└────┴────┴────┴────┴────┴────┴────┴────┘
↑
clock hand(掃描指針)
演算法步驟:
- clock hand 從當前位置開始向右掃描
- 若
usage_count > 0:減 1,繼續向右 - 若
usage_count = 0:- 若
refcount > 0(有程序正在使用):跳過繼續 - 若
refcount = 0:選擇此 Buffer 替換
- 若
- 替換前若
dirty = true,先寫回磁碟
使用頻率遞增規則:每次存取 Buffer 時 usage_count = min(usage_count + 1, 5)。因此熱點頁面(高頻存取)的 usage_count 快速達到 5,不易被替換;冷頁面的 usage_count 快速降為 0,優先被替換。
Ring Buffer:防止快取污染
大型 Sequential Scan 如果使用普通的 Shared Buffer Pool,會將大量頁面載入 Pool,驅逐其他熱點頁面——這稱為快取污染。PostgreSQL 對特定操作使用 Ring Buffer(環形緩衝區)策略避免此問題。
| 操作類型 | 觸發條件 | Ring 大小 |
|---|---|---|
| 大型 Seq Scan | Relation 大小 > shared_buffers / 4 | 256KB(32 個 Page) |
| Bulk Write(COPY 等) | 大量寫入操作 | 8MB |
| VACUUM | VACUUM 清理操作 | 256KB |
Ring Buffer 運作方式:
Shared Buffer Pool (128MB)
┌──────────────────────────────────────────┐
│ 正常 Buffer(熱點頁面) │
│ 不受 Seq Scan 影響 │
│ │
│ ┌──────────────────────┐ │
│ │ Ring Buffer (256KB) │ ← Seq Scan 專用 │
│ │ [p1][p2]...[p32] │ 循環使用 │
│ │ ↑________________┘ │ │
│ └──────────────────────┘ │
└──────────────────────────────────────────┘
Ring Buffer 的限制:若相同頁面被另一個 Backend 以非 Ring 方式請求,該頁面升格為普通 Buffer,不再受 Ring 限制。
Double Buffering 問題
PostgreSQL 的頁面從磁碟到應用程序需要經過兩層快取:Shared Buffer 和 OS Page Cache,這稱為 Double Buffering(雙重緩衝)問題。
I/O 路徑:
┌──────────────────┐
│ Shared Buffer │ ← 第一層:PostgreSQL 管理
│ (shared_buffers) │
└────────┬─────────┘
│ Cache Miss
▼
┌──────────────────┐
│ OS Page Cache │ ← 第二層:作業系統管理
└────────┬─────────┘
│ Cache Miss
▼
┌──────────────────┐
│ 磁碟(Disk) │
└──────────────────┘
同一份資料可能同時存在於兩層 Cache 中,浪費記憶體。這也是為什麼 shared_buffers 不應設太大——OS Page Cache 同樣是重要的緩衝層。
effective_cache_size 參數告訴 Query Planner 作業系統快取大約有多大,影響 Index Scan vs Seq Scan 的選擇,但不實際分配記憶體。
Buffer Pin 與 Content Lock
存取 Buffer 需要適當的 Pin 和 Lock 保護,確保並行安全。
讀取頁面流程
1. 在 Buffer Tag Hash Table 中查找目標頁面
2. Pin Buffer(refcount++),防止 Buffer 被替換
3. 獲取 Content Lock(shared mode)
4. 讀取頁面內容
5. 釋放 Content Lock
6. (完成後)Unpin(refcount--)
修改頁面流程
1. 查找 / 載入 Buffer
2. Pin Buffer(refcount++)
3. 獲取 Content Lock(exclusive mode)
4. 修改頁面內容
5. 標記 Buffer 為 dirty
6. 同時寫入 WAL 記錄(先 WAL,後 dirty page)
7. 釋放 Content Lock
8. (後續)Unpin
關鍵規則:
refcount > 0(被 Pin):Buffer 不能被替換dirty = true:寫回磁碟前必須先確保對應 WAL 已 flush- WAL Protocol:dirty page 寫回磁碟前,其對應的 WAL 記錄必須已持久化
使用 pg_buffercache 分析 Buffer Pool
pg_buffercache 擴展可以直接查看 Buffer Pool 的內部狀態:
-- 載入擴展(超級使用者執行一次)
CREATE EXTENSION IF NOT EXISTS pg_buffercache;
-- 查看 Buffer Pool 整體使用狀況
SELECT
COUNT(*) AS total_buffers,
COUNT(*) FILTER (WHERE reldatabase IS NOT NULL) AS used_buffers,
COUNT(*) FILTER (WHERE reldatabase IS NULL) AS free_buffers,
pg_size_pretty(COUNT(*) * 8192) AS total_size,
pg_size_pretty(
COUNT(*) FILTER (WHERE reldatabase IS NOT NULL) * 8192
) AS used_size
FROM pg_buffercache;
-- 查看各 Relation 佔用的 Buffer 數量
SELECT
c.relname,
COUNT(*) AS buffer_count,
pg_size_pretty(COUNT(*) * 8192) AS buffered_size,
round(100.0 * COUNT(*) /
(SELECT COUNT(*) FROM pg_buffercache), 2) AS pct
FROM pg_buffercache b
JOIN pg_class c ON c.relfilenode = b.relfilenode
WHERE b.reldatabase = (
SELECT oid FROM pg_database
WHERE datname = current_database()
)
GROUP BY c.relname
ORDER BY buffer_count DESC
LIMIT 20;
監控 Cache Hit Ratio
Cache Hit Ratio 是衡量 Buffer Pool 效能的最重要指標:
-- 資料庫級別的 Cache Hit Ratio
SELECT
datname,
blks_hit,
blks_read,
CASE
WHEN blks_hit + blks_read = 0 THEN NULL
ELSE round(100.0 * blks_hit / (blks_hit + blks_read), 2)
END AS cache_hit_ratio
FROM pg_stat_database
WHERE datname = current_database();
-- 目標:cache_hit_ratio > 99%
-- 若低於 99%,考慮增加 shared_buffers
-- 各表的 Cache Hit Ratio
SELECT
schemaname, tablename,
heap_blks_hit, heap_blks_read,
CASE
WHEN heap_blks_hit + heap_blks_read = 0 THEN NULL
ELSE round(100.0 * heap_blks_hit /
(heap_blks_hit + heap_blks_read), 2)
END AS heap_hit_ratio
FROM pg_statio_user_tables
ORDER BY heap_blks_read DESC
LIMIT 20;
usage_count 分佈分析
-- 查看 usage_count 分佈(了解熱點程度)
SELECT
usagecount,
COUNT(*) AS buf_count,
round(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 2) AS pct
FROM pg_buffercache
GROUP BY usagecount
ORDER BY usagecount;
若大量 Buffer 的 usagecount 為 0,表示 Buffer Pool 可能過大;若 usagecount = 5 的 Buffer 佔比很高,表示熱點集中,Buffer Pool 使用效率良好。
pg_prewarm:預熱 Buffer Pool
PostgreSQL 重啟後 Buffer Pool 是空的。pg_prewarm 擴展可以在啟動時預熱常用頁面:
-- 載入擴展
CREATE EXTENSION pg_prewarm;
-- 手動預熱指定表
SELECT pg_prewarm('my_important_table');
PG11+ 支援自動預熱:
# postgresql.conf
shared_preload_libraries = 'pg_prewarm'
pg_prewarm.autoprewarm = on
pg_prewarm.autoprewarm_interval = 300 # 每 5 分鐘儲存一次 buffer 狀態
自動預熱會在關閉時記錄 Buffer Pool 狀態到 $PGDATA/autoprewarm.blocks,重啟後自動載入。
pg_stat_io(PG16+)
PG16 新增 pg_stat_io 視圖,提供更細粒度的 I/O 統計:
-- PG16+:per-object I/O 統計
SELECT
backend_type,
object, -- relation, temp relation...
context, -- normal, vacuum, bulkread, bulkwrite...
reads, -- 從磁碟讀取次數
hits, -- Buffer Pool 命中次數
evictions, -- 從 Buffer Pool 驅逐的次數
reuses -- Ring Buffer 重用次數
FROM pg_stat_io
ORDER BY reads DESC
LIMIT 20;
context 欄位可以區分不同操作的 I/O:normal(普通查詢)、bulkread(大型 Seq Scan)、bulkwrite(COPY 等)、vacuum(VACUUM 操作)。
生產環境建議配置
# postgresql.conf
# Shared Buffer Pool 大小(通常設 RAM 的 25%)
shared_buffers = 8GB # 若 RAM = 32GB
# 注意:超過 16GB 通常不再帶來明顯效益(雙重緩衝問題)
# 告訴 Planner OS Cache 大小(不分配記憶體)
effective_cache_size = 24GB # 若 RAM = 32GB,設 75%
# 啟用 Huge Pages(減少 TLB miss)
huge_pages = try # PG16+,嘗試使用,失敗則回退
常見陷阱
shared_buffers 設太大
系統 RAM = 32GB
shared_buffers = 24GB ← 錯誤!
OS Page Cache 同樣是重要的緩衝層。shared_buffers 過大會壓縮 OS Page Cache 的空間,反而降低整體命中率。建議不超過 RAM 的 40%,通常 25% 最佳。
誤以為大型 Seq Scan 會污染 Buffer Pool
PostgreSQL 對大表 Seq Scan 自動使用 Ring Buffer(256KB),不會大量驅逐熱點頁面。前提是表大小 > shared_buffers / 4。注意 Index Scan 的頁面不使用 Ring Buffer。
blks_read 不等於真正磁碟 I/O
Shared Buffer 中找不到頁面時 blks_read 計數增加,但頁面可能已在 OS Page Cache 中。使用 iotop 或 pg_stat_io 觀察實際磁碟 I/O 才是真相。
總結
Buffer Management 是 PostgreSQL 效能的關鍵引擎:
- Shared Buffer Pool 以 8KB Page 為單位快取資料,所有 Backend 共享存取
- Clock-Sweep 演算法透過
usage_count智慧地保留熱點頁面、替換冷頁面 - Ring Buffer 防止大型 Seq Scan 和 VACUUM 污染快取
- Double Buffering 提醒我們不能把所有記憶體都給
shared_buffers - Cache Hit Ratio > 99% 是生產環境的健康指標
pg_buffercache和pg_stat_io是深入分析 Buffer Pool 的利器
下一篇,我們將探討 VACUUM 機制——PostgreSQL 如何清理 Dead Tuple、防止表膨脹與 Transaction ID Wraparound。