PostgreSQL VACUUM 与 AUTOVACUUM 深度解析
一、为什么需要 VACUUM
PostgreSQL 使用 MVCC(多版本并发控制)实现事务隔离。每次 UPDATE/DELETE 不会原地修改数据,而是:
- DELETE :将旧行的
xmax设为当前事务 ID,标记为"已删除" - UPDATE :旧行设置
xmax,同时插入一条新行
这些对所有活跃事务都不可见的旧版本称为死元组(dead tuples)。死元组不清理会导致:
- 表文件持续膨胀(table bloat)
- 索引膨胀(index bloat)
- 顺序扫描变慢
- 事务 ID 回绕(Transaction ID Wraparound)------最严重的问题,可能导致数据丢失
二、VACUUM 的核心任务
| 任务 | 说明 |
|---|---|
| 清理死元组 | 回收堆页内死元组占用的空间 |
| 更新 FSM | 将回收的空间记录到空闲空间映射(Free Space Map) |
| 更新 VM | 更新可见性映射(Visibility Map),标记全可见页 |
| 清理索引 | 删除指向死元组的索引条目 |
| 冻结旧事务 ID | 防止事务 ID 回绕 |
| 更新统计信息 | 更新 pg_class.relpages、reltuples 等 |
三、VACUUM 执行流程详解
3.1 整体流程
VACUUM mytable
│
├─ 1. 扫描堆表,收集死元组的 TID 列表(存入 maintenance_work_mem)
│
├─ 2. 清理索引(对每个索引执行 index vacuum)
│ └─ 删除指向死元组 TID 的索引条目
│
├─ 3. 清理堆页(heap vacuum)
│ ├─ 标记死元组为可复用(LP_DEAD → LP_UNUSED)
│ ├─ 更新 FSM
│ └─ 尝试截断尾部空页(VACUUM FULL 才真正收缩文件)
│
└─ 4. 更新系统目录(pg_class、pg_statistic 等)
3.2 两遍扫描机制
当死元组数量超过 maintenance_work_mem 能容纳的 TID 数量时,VACUUM 会分多轮执行:
第一轮:扫描堆页 0~N,收集死元组 TID 直到内存满
→ 清理所有索引(针对这批 TID)
→ 清理这批对应的堆页
第二轮:继续扫描堆页 N+1~M,重复上述过程
...
每轮都需要重新扫描所有索引,因此 maintenance_work_mem 越大,VACUUM 越高效(减少索引扫描轮次)。
3.3 可见性映射(Visibility Map,VM)
VM 是每张表对应的 fork 文件(_vm 后缀),每页对应 2 个 bit:
| bit | 含义 |
|---|---|
ALL_VISIBLE |
页内所有元组对所有活跃事务可见(无死元组) |
ALL_FROZEN |
页内所有元组已冻结(xmin = FrozenTransactionId) |
VACUUM 的优化:
- 跳过
ALL_VISIBLE的页(无需清理死元组) - 跳过
ALL_FROZEN的页(无需冻结处理) - Index-Only Scan 利用 VM 避免回表
3.4 空闲空间映射(Free Space Map,FSM)
FSM 是 _fsm 后缀的 fork 文件,记录每个页的可用空间大小(以 1/256 页大小为精度)。
INSERT/UPDATE 时通过 FSM 快速找到有足够空间的页,避免扩展文件。
VACUUM 清理死元组后更新 FSM,使回收的空间可被复用。
四、事务 ID 回绕(Wraparound)
4.1 问题根源
PostgreSQL 的事务 ID(XID)是 32 位无符号整数,最大约 42 亿。XID 比较使用模运算:
当前 XID 的"过去":前 2^31 个 XID
当前 XID 的"未来":后 2^31 个 XID
当 XID 接近耗尽时,旧事务的数据会被认为是"未来"事务写入的,从而对所有人不可见------数据"消失"。
4.2 冻结(Freeze)机制
VACUUM 将足够旧的元组的 xmin 替换为特殊值 FrozenTransactionId(= 2),该值对所有事务永远可见,彻底脱离 XID 比较逻辑。
冻结条件(默认):
当前 XID - tuple.xmin > vacuum_freeze_min_age(默认 50,000,000)
4.3 强制冻结(Aggressive Vacuum)
当表的 relfrozenxid 距当前 XID 超过 autovacuum_freeze_max_age(默认 200,000,000)时,触发强制 VACUUM,忽略 VM 标记,扫描所有页强制冻结。
安全距离示意:
|<--- 已冻结 --->|<--- 正常范围 --->|<--- 危险区 --->|
0 relfrozenxid 当前XID XID耗尽
↑ ↑
vacuum_freeze_min_age autovacuum_freeze_max_age
4.4 相关参数
| 参数 | 默认值 | 说明 |
|---|---|---|
vacuum_freeze_min_age |
50,000,000 | 元组 xmin 距当前 XID 超过此值才冻结 |
vacuum_freeze_table_age |
150,000,000 | 表 relfrozenxid 距当前 XID 超过此值触发全表扫描冻结 |
autovacuum_freeze_max_age |
200,000,000 | 超过此值强制触发 autovacuum(即使 autovacuum=off) |
五、AUTOVACUUM
5.1 架构
postmaster
└─ autovacuum launcher(常驻进程)
│
├─ 每隔 autovacuum_naptime 唤醒一次
├─ 扫描所有数据库,判断哪些表需要 vacuum/analyze
└─ 启动 autovacuum worker 进程(最多 autovacuum_max_workers 个)
└─ 每个 worker 负责一个数据库内的表
5.2 触发条件
触发 VACUUM 的条件:
死元组数 > autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × reltuples
默认:50 + 0.02 × 表行数,即死元组超过表大小的 2% + 50 行时触发。
触发 ANALYZE 的条件:
修改行数 > autovacuum_analyze_threshold + autovacuum_analyze_scale_factor × reltuples
默认:50 + 0.05 × 表行数,即修改超过 5% + 50 行时触发。
触发强制 VACUUM(Wraparound 防护):
当前 XID - pg_class.relfrozenxid > autovacuum_freeze_max_age(200,000,000)
此时即使 autovacuum = off 也会强制执行。
5.3 Cost-Based 限速
为避免 autovacuum 影响业务,PostgreSQL 内置了基于代价的限速机制:
| 参数 | 默认值 | 说明 |
|---|---|---|
autovacuum_vacuum_cost_delay |
2ms | 每次达到代价上限后休眠时间 |
autovacuum_vacuum_cost_limit |
-1(使用 vacuum_cost_limit=200) | 每轮允许的最大代价 |
vacuum_cost_page_hit |
1 | 命中 shared buffer 的代价 |
vacuum_cost_page_miss |
2 | 从磁盘读取页的代价 |
vacuum_cost_page_dirty |
20 | 弄脏一个页的代价 |
工作原理:
VACUUM 每处理一个页,累加代价
代价 >= vacuum_cost_limit → 休眠 vacuum_cost_delay → 重置代价 → 继续
对于 I/O 压力大的系统,可适当提高 autovacuum_vacuum_cost_limit 或降低 autovacuum_vacuum_cost_delay。
5.4 关键配置参数汇总
| 参数 | 默认值 | 说明 |
|---|---|---|
autovacuum |
on | 是否启用 autovacuum |
autovacuum_max_workers |
3 | 最大 worker 进程数 |
autovacuum_naptime |
1min | launcher 检查间隔 |
autovacuum_vacuum_threshold |
50 | 触发 vacuum 的死元组基准数 |
autovacuum_vacuum_scale_factor |
0.02 | 触发 vacuum 的死元组比例 |
autovacuum_analyze_threshold |
50 | 触发 analyze 的修改行基准数 |
autovacuum_analyze_scale_factor |
0.05 | 触发 analyze 的修改行比例 |
autovacuum_vacuum_insert_threshold |
1000 | 仅 INSERT 场景触发 vacuum 的行数阈值 |
autovacuum_vacuum_insert_scale_factor |
0.2 | 仅 INSERT 场景触发 vacuum 的比例 |
autovacuum_freeze_max_age |
200,000,000 | 强制冻结的 XID 年龄阈值 |
autovacuum_multixact_freeze_max_age |
400,000,000 | MultiXact 强制冻结阈值 |
log_autovacuum_min_duration |
-1(不记录) | 超过此时长的 autovacuum 记录日志 |
5.5 表级参数覆盖
可以对单张表设置独立的 autovacuum 参数,覆盖全局配置:
sql
ALTER TABLE mytable SET (
autovacuum_vacuum_scale_factor = 0.01, -- 更激进,1% 就触发
autovacuum_vacuum_threshold = 100,
autovacuum_vacuum_cost_delay = 10, -- 降低限速,更快完成
autovacuum_freeze_max_age = 100000000
);
六、VACUUM FULL vs 普通 VACUUM
| 特性 | VACUUM | VACUUM FULL |
|---|---|---|
| 锁级别 | ShareUpdateExclusiveLock(不阻塞读写) | AccessExclusiveLock(阻塞所有操作) |
| 空间回收 | 标记为可复用,不归还 OS | 重建表文件,归还 OS |
| 索引处理 | 清理死索引条目 | 重建所有索引 |
| 执行速度 | 快 | 慢 |
| 适用场景 | 日常维护 | 严重膨胀后的一次性整理 |
生产环境避免频繁 VACUUM FULL,可用
pg_repack扩展替代(在线重建,不阻塞业务)。
七、监控 VACUUM 状态
7.1 查看需要 VACUUM 的表
sql
SELECT
schemaname,
relname,
n_dead_tup,
n_live_tup,
round(n_dead_tup::numeric / nullif(n_live_tup + n_dead_tup, 0) 100, 2) AS deadratio,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
7.2 查看 XID 回绕风险
sql
SELECT
datname,
age(datfrozenxid) AS xid_age,
2000000000 - age(datfrozenxid) AS xid_remaining
FROM pg_database
ORDER BY xid_age DESC;
-- 表级别
SELECT
schemaname,
relname,
age(relfrozenxid) AS xid_age
FROM pg_class
JOIN pg_namespace ON relnamespace = pg_namespace.oid
WHERE relkind = 'r'
ORDER BY xid_age DESC
LIMIT 20;
### 7.3 查看正在运行的 VACUUM
sql
SELECT
pid,
datname,
relid::regclass AS table_name,
phase,
heap_blks_total,
heap_blks_scanned,
heap_blks_vacuumed,
index_vacuum_count,
num_dead_tuples,
max_dead_tuples
FROM pg_stat_progress_vacuum;
7.4 开启 autovacuum 日志
sql
-- 记录超过 1 秒的 autovacuum
ALTER SYSTEM SET log_autovacuum_min_duration = '1s';
SELECT pg_reload_conf();
日志示例:
LOG: automatic vacuum of table "mydb.public.mytable":
index scans: 1
pages: 0 removed, 1024 remain, 0 skipped due to pins, 512 skipped frozen
tuples: 50000 removed, 200000 remain, 0 are dead but not yet removable
buffer usage: 2048 hits, 512 misses, 256 dirtied
avg read rate: 3.200 MB/s, avg write rate: 1.600 MB/s
system usage: CPU: user: 0.50 s, system: 0.10 s, elapsed: 2.00 s
八、常见问题与调优
8.1 autovacuum 跑不过来(表持续膨胀)
原因:写入速度 > autovacuum 清理速度,或限速太严格。
解决:
sql
-- 提高并发 worker 数
ALTER SYSTEM SET autovacuum_max_workers = 6;
-- 对热表降低触发阈值
ALTER TABLE hot_table SET (
autovacuum_vacuum_scale_factor = 0.005,
autovacuum_vacuum_cost_delay = 2
);
-- 提高单次处理代价上限(减少休眠)
ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 800;
SELECT pg_reload_conf();
8.2 长事务阻塞 VACUUM
VACUUM 无法清理比最老活跃事务更新的死元组。
sql
-- 找出阻塞 vacuum 的长事务
SELECT
pid,
now() - xact_start AS duration,
state,
query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start
LIMIT 10;
8.3 表膨胀评估
sql
-- 使用 pgstattuple 扩展
CREATE EXTENSION pgstattuple;
SELECT
table_len,
tuple_count,
tuple_len,
dead_tuple_count,
dead_tuple_len,
free_space,
round(dead_tuple_len::numeric / table_len 100, 2) AS deadratio
FROM pgstattuple('mytable');
8.4 手动触发 VACUUM
sql
-- 普通 vacuum(不阻塞业务)
VACUUM mytable;
-- 同时更新统计信息
VACUUM ANALYZE mytable;
-- 详细输出
VACUUM VERBOSE mytable;
-- 强制冻结(防回绕)
VACUUM FREEZE mytable;
-- 重建表(阻塞业务,谨慎使用)
VACUUM FULL mytable;
九、源码关键位置
| 文件 | 内容 |
|---|---|
src/backend/commands/vacuum.c |
VACUUM 主入口,参数解析 |
src/backend/access/heap/vacuumlazy.c |
懒惰 VACUUM 核心逻辑(堆扫描、冻结、FSM/VM 更新) |
src/backend/postmaster/autovacuum.c |
autovacuum launcher 和 worker 进程 |
src/backend/access/heap/heapam.c |
堆访问方法,HOT 更新 |
src/backend/storage/freespace/freespace.c |
FSM 管理 |
src/backend/storage/buffer/bufmgr.c |
VM bit 操作 |
十、总结
死元组产生(MVCC UPDATE/DELETE)
│
▼
autovacuum 触发判断
(死元组比例 > threshold + scale_factor × reltuples)
│
▼
VACUUM 执行
├─ 扫描堆页,收集死元组 TID
├─ 清理索引(删除死索引条目)
├─ 清理堆页(回收空间,更新 FSM)
├─ 更新 VM(标记全可见/全冻结页)
└─ 冻结旧 XID(防回绕)
│
▼
空间可被新数据复用(不归还 OS)
需要归还 OS → VACUUM FULL / pg_repack
VACUUM 是 PostgreSQL 健康运行的核心机制,合理配置 autovacuum 参数、监控死元组积累和 XID 年龄,是 PostgreSQL DBA 日常运维的重要工作。