VACUUM 機制:PostgreSQL 空間回收與防膨脹的關鍵 | PostgreSQL
VACUUM 是 PostgreSQL 因 MVCC 設計而產生的必要維護機制。由於 UPDATE 和 DELETE 並不立即移除舊版本資料,而是留下 Dead Tuple,必須定期執行 VACUUM 來回收空間、更新統計資訊,並防止 Transaction ID Wraparound 問題。理解 VACUUM 是維運 PostgreSQL 的必備知識。
為什麼需要 VACUUM
MVCC 的實作讓 PostgreSQL 在 Heap Tuple 中保留多個版本,供不同 Transaction 看到不同快照。每次 UPDATE 產生新版本、舊版本變成 Dead Tuple;DELETE 也只是邏輯刪除,物理空間仍然佔用。
MVCC 產生 Dead Tuple 的過程:
INSERT(TX 100):
┌──────────────────────────────────────┐
│ xmin=100 xmax=0 balance=100 id=1 │ ← Active tuple(可見)
└──────────────────────────────────────┘
UPDATE(TX 200):
┌──────────────────────────────────────┐
│ xmin=100 xmax=200 balance=100 id=1│ ← Dead tuple(等待回收)
├──────────────────────────────────────┤
│ xmin=200 xmax=0 balance=200 id=1│ ← Active tuple(可見)
└──────────────────────────────────────┘
VACUUM 後:
┌──────────────────────────────────────┐
│ (空閒空間) │ ← 可供新 INSERT 使用
├──────────────────────────────────────┤
│ xmin=200 xmax=0 balance=200 id=1│ ← Active tuple
└──────────────────────────────────────┘
不執行 VACUUM 的後果:
- 表格佔用磁碟空間持續增長(Bloat)
- 查詢需要掃描大量 Dead Tuple,效能下降
- Transaction ID 耗盡(Wraparound),導致資料庫拒絕服務
Dead Tuple 生命週期
Dead Tuple 並非立即可回收——只有當沒有任何活躍 Transaction 需要看到該版本時,VACUUM 才能安全回收。
時間軸:
TX 100 TX 200 TX 300 (長 transaction)
INSERT UPDATE 進行中 結束
│ │ │ │
────●─────────●──────────────●──────────────────●────
│ │
└─ tuple 變成 dead tuple └─ 此時沒有任何活躍
但 TX 300 仍能看到舊版本 TX 需要看到舊版本
VACUUM 不能回收 → VACUUM 可以安全回收
關鍵概念:OldestXmin(最舊的活躍 Snapshot 中的 xmin)。若 Dead Tuple 的 xmax < OldestXmin,說明沒有任何活躍 Transaction 需要看到此版本,VACUUM 可以安全回收。
這也是為什麼長時間 idle in transaction 的 Session 會導致嚴重 Bloat——OldestXmin 無法推進,大量 Dead Tuple 無法被回收。
Standard VACUUM vs VACUUM FULL
| 特性 | Standard VACUUM | VACUUM FULL |
|---|---|---|
| 鎖類型 | ShareUpdateExclusiveLock(不阻塞讀寫) | AccessExclusiveLock(完全鎖表) |
| 空間回收 | 標記為 Free Space,可在表內重用,不釋放給 OS | 完全重建表,真正釋放磁碟空間 |
| 執行速度 | 快(只掃描部分頁面) | 慢(重建整個表) |
| 索引處理 | 清理索引中的 dead entries | 重建所有索引 |
| 線上可用性 | 是 | 否(表不可用) |
| 適用場景 | 定期維護(autovacuum) | 嚴重 Bloat 後的緊急處理 |
生產環境優先使用 Standard VACUUM(或 autovacuum),嚴重 Bloat 考慮 pg_repack(不鎖表的 VACUUM FULL 替代方案)。
VACUUM 的四個工作
VACUUM 執行時進行四項主要工作:
工作 1:回收 Dead Tuples
掃描 Heap Pages,識別可回收的 Dead Tuple,將其空間標記為 free,並清理索引中指向 Dead Tuple 的 entries。
工作 2:更新 FSM(Free Space Map)
FSM 記錄每個 Page 有多少可用空間。VACUUM 後更新 FSM,使 INSERT/UPDATE 能找到合適的 Page 寫入新資料。
工作 3:更新 VM(Visibility Map)
VM 為每個 Page 維護兩個 bit:
| Bit | 名稱 | 說明 |
|---|---|---|
| bit 0 | all-visible | 此 Page 的所有 Tuple 對所有 Transaction 可見 |
| bit 1 | all-frozen | 此 Page 的所有 Tuple 已 Freeze(PG9.6+) |
VM 對效能至關重要:
- VACUUM 設定
all-visible後,後續 VACUUM 可跳過此 Page - Index-Only Scan 依賴 VM——若
all-visible = 1,可直接從 Index 返回值,不需回表
工作 4:Freeze 舊 Transaction ID
將 xmin 足夠舊的 Tuple 標記為 Frozen(xmin = FrozenTransactionId = 2),防止 Transaction ID Wraparound。
Freeze 機制
Transaction ID 是 32-bit unsigned integer,空間僅有約 42 億。PostgreSQL 使用模運算比較 TXid 大小——若 TXid 用盡循環,舊資料的 xmin 可能被誤判為「未來」,導致資料消失。
Freeze 執行時機:
TXid 軸:
│──────────────────────────│─────────────────│
0 current-50M current
若 xmin 在 Freeze Zone(< current - vacuum_freeze_min_age):
VACUUM 執行時將 xmin 設為 FrozenTransactionId (2)
→ Frozen Tuple 永遠可見,不受 Wraparound 影響
Anti-Wraparound VACUUM
每個 Relation 在 pg_class.relfrozenxid 記錄其「最老的未 Freeze TXid」。當 age(relfrozenxid) 超過 autovacuum_freeze_max_age(預設 2 億),PostgreSQL 會強制觸發 Anti-Wraparound VACUUM。
-- 監控 Wraparound 風險
SELECT
datname,
age(datfrozenxid) AS db_age,
round(100.0 * age(datfrozenxid) / 2000000000, 2)
AS pct_toward_wraparound
FROM pg_database
ORDER BY age(datfrozenxid) DESC;
-- 計算距離強制 VACUUM 還有多少 TXid
SELECT
datname,
age(datfrozenxid) AS current_age,
200000000 - age(datfrozenxid) AS txid_until_forced_vacuum,
2147483647 - age(datfrozenxid) AS txid_until_shutdown
FROM pg_database
ORDER BY age(datfrozenxid) DESC;
若 age(datfrozenxid) 接近 2^31(約 21 億),PostgreSQL 會進入 shutdown mode,拒絕所有新 Transaction,只允許超級使用者連線執行 VACUUM FREEZE。
Autovacuum 觸發條件
Autovacuum 根據以下公式決定是否觸發:
VACUUM 觸發閾值:
threshold = autovacuum_vacuum_threshold
+ autovacuum_vacuum_scale_factor × reltuples
預設:50 + 0.2 × 表行數
若 n_dead_tup > threshold → 觸發 autovacuum
例子(100 萬行的表):
threshold = 50 + 0.2 × 1,000,000 = 200,050
→ 超過 20 萬 dead tuple 才觸發
Autovacuum 調校
核心參數:
| 參數 | 預設值 | 說明 |
|---|---|---|
autovacuum | on | 開關(絕對不要關!) |
autovacuum_max_workers | 3 | 同時執行的 autovacuum 數 |
autovacuum_naptime | 1min | 每次巡查間隔 |
autovacuum_vacuum_scale_factor | 0.2 | 比例觸發閾值(20%) |
autovacuum_vacuum_cost_delay | 2ms | 每次 I/O 後的延遲(限速) |
autovacuum_vacuum_cost_limit | 200 | 每輪的 I/O cost 上限 |
autovacuum_freeze_max_age | 200000000 | Freeze 觸發 age |
對於大表,預設 20% 的 scale_factor 可能導致 VACUUM 觸發不夠頻繁:
-- 大型交易表(1000 萬行):設定更激進的閾值
ALTER TABLE transactions SET (
autovacuum_vacuum_scale_factor = 0.01, -- 1%
autovacuum_analyze_scale_factor = 0.005, -- 0.5%
autovacuum_vacuum_threshold = 1000,
autovacuum_vacuum_cost_delay = 1 -- ms,更積極
);
-- 高頻更新的熱點表:不限速
ALTER TABLE hot_table SET (
autovacuum_vacuum_cost_delay = 0,
autovacuum_vacuum_cost_limit = 800,
autovacuum_vacuum_scale_factor = 0.01
);
監控 Dead Tuple 與 VACUUM 狀態
-- 查看所有表的 Dead Tuple 狀態
SELECT
schemaname, relname AS tablename,
n_live_tup AS live_tuples,
n_dead_tup AS dead_tuples,
CASE
WHEN n_live_tup = 0 THEN NULL
ELSE round(100.0 * n_dead_tup / n_live_tup, 2)
END AS dead_ratio_pct,
last_vacuum, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;
-- 查看 VACUUM 即時進度(PG9.6+)
SELECT
pid,
relid::regclass AS table_name,
phase,
heap_blks_total,
heap_blks_scanned,
heap_blks_vacuumed,
num_dead_tuples
FROM pg_stat_progress_vacuum;
VACUUM 命令實戰
-- 對指定表執行 Standard VACUUM
VACUUM accounts;
-- 顯示詳細進度
VACUUM VERBOSE accounts;
-- 同時更新統計資訊
VACUUM ANALYZE accounts;
-- 強制 Freeze
VACUUM FREEZE accounts;
-- PG13+:使用並行 worker 加速
VACUUM (PARALLEL 4) accounts;
-- 組合選項
VACUUM (VERBOSE, ANALYZE, PARALLEL 4) accounts;
Cost-Based Throttling
Autovacuum 使用 Cost-Based 機制限速,避免過度佔用 I/O:
Cost 計算規則:
讀取 Buffer(shared buffer 命中):vacuum_cost_page_hit = 1
讀取 Buffer(需從磁碟讀取): vacuum_cost_page_miss = 2
修改 dirty Buffer: vacuum_cost_page_dirty = 20
每次累積 cost 達到 vacuum_cost_limit(預設 200)
→ 休眠 vacuum_cost_delay(autovacuum 預設 2ms)
→ 繼續下一輪
SSD 環境可以降低 vacuum_cost_delay 到 0 或 1ms;HDD 環境可以提高到 20-50ms。
pg_repack:線上重組替代 VACUUM FULL
pg_repack 能在不鎖表的情況下重建表,相當於線上版 VACUUM FULL:
# 重組指定表(不鎖表,全程可讀寫)
pg_repack -d mydb -t schema.big_table
# 只重建索引
pg_repack -d mydb -t schema.big_table --only-indexes
# 試運行
pg_repack -d mydb --dry-run
工作原理:建立新表 → 記錄變更(trigger)→ 複製資料 → 追趕變更 → 短暫鎖定交換表名 → 刪除舊表。
Bloat 估算
-- 使用 pgstattuple 擴展(精確但慢)
CREATE EXTENSION pgstattuple;
SELECT
relname,
tuple_count,
dead_tuple_count,
round(dead_tuple_percent::numeric, 2) AS dead_pct,
round(free_percent::numeric, 2) AS free_pct
FROM pgstattuple('big_table');
-- 快速估算(使用統計資訊)
SELECT
tablename,
pg_size_pretty(pg_total_relation_size(
schemaname||'.'||tablename)) AS total_size,
round(100.0 * n_dead_tup /
NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_ratio_pct
FROM pg_stat_user_tables
WHERE pg_relation_size(
schemaname||'.'||tablename) > 104857600 -- > 100MB
ORDER BY pg_relation_size(
schemaname||'.'||tablename) DESC;
常見陷阱
禁用 autovacuum
常見理由是「autovacuum 影響效能」。正確做法是調整 cost_delay / cost_limit,而非關閉 autovacuum。關閉會導致表持續膨脹,最終可能 Wraparound。
長 Transaction 阻止 Dead Tuple 回收
-- 排查長 transaction
SELECT pid, now() - xact_start AS duration, state, query
FROM pg_stat_activity
WHERE state IN ('idle in transaction', 'active')
ORDER BY xact_start;
-- 設定自動超時
ALTER SYSTEM SET idle_in_transaction_session_timeout = '5min';
SELECT pg_reload_conf();
大量 DELETE 後表大小不縮小
Standard VACUUM 標記空間為可重用,但不釋放給 OS。若需要釋放磁碟空間,使用 VACUUM FULL 或 pg_repack。
生產環境監控告警
-- 告警 1:Wraparound 風險(age > 5 億)
SELECT datname, age(datfrozenxid)
FROM pg_database
WHERE age(datfrozenxid) > 500000000;
-- 告警 2:Dead Tuple 比例過高(> 10%)
SELECT schemaname || '.' || relname AS table_name,
round(100.0 * n_dead_tup /
NULLIF(n_live_tup, 0), 2) AS dead_ratio
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
AND n_dead_tup > n_live_tup * 0.1;
-- 告警 3:autovacuum 被長 transaction 阻塞
SELECT pid,
now() - xact_start AS transaction_age,
state, left(query, 100) AS query_snippet
FROM pg_stat_activity
WHERE state != 'idle'
AND xact_start IS NOT NULL
AND now() - xact_start > interval '1 hour'
ORDER BY transaction_age DESC;
總結
VACUUM 是 PostgreSQL MVCC 架構的必要代價,也是維運的核心工作:
- Dead Tuple 是 MVCC 的副產品,不清理會導致 Bloat 與效能下降
- Standard VACUUM 標記空間可重用但不縮小檔案,VACUUM FULL 會重建但鎖表
- Visibility Map 讓 VACUUM 跳過乾淨的 Page,也支援 Index-Only Scan
- Freeze 機制 防止 Transaction ID Wraparound,是資料庫存活的底線
- Autovacuum 必須保持開啟,針對大表調整
scale_factor與cost_delay - 嚴重 Bloat 使用 pg_repack 而非 VACUUM FULL,避免鎖表影響服務
- 監控
age(datfrozenxid)與 Dead Tuple 比例是日常維運的必做功課
下一篇,我們將探討 System Catalog——PostgreSQL 如何以 Catalog-driven 架構實現無與倫比的可擴展性。