PostgreSQL VACUUM 与 AUTOVACUUM 深度解析

PostgreSQL VACUUM 与 AUTOVACUUM 深度解析


一、为什么需要 VACUUM

PostgreSQL 使用 MVCC(多版本并发控制)实现事务隔离。每次 UPDATE/DELETE 不会原地修改数据,而是:

  • DELETE :将旧行的 xmax 设为当前事务 ID,标记为"已删除"
  • UPDATE :旧行设置 xmax,同时插入一条新行

这些对所有活跃事务都不可见的旧版本称为死元组(dead tuples)。死元组不清理会导致:

  1. 表文件持续膨胀(table bloat)
  2. 索引膨胀(index bloat)
  3. 顺序扫描变慢
  4. 事务 ID 回绕(Transaction ID Wraparound)------最严重的问题,可能导致数据丢失

二、VACUUM 的核心任务

任务 说明
清理死元组 回收堆页内死元组占用的空间
更新 FSM 将回收的空间记录到空闲空间映射(Free Space Map)
更新 VM 更新可见性映射(Visibility Map),标记全可见页
清理索引 删除指向死元组的索引条目
冻结旧事务 ID 防止事务 ID 回绕
更新统计信息 更新 pg_class.relpagesreltuples

三、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 日常运维的重要工作。

相关推荐
电商API&Tina2 小时前
电商数据采集API接口||合规优先、稳定高效、数据精准
java·javascript·数据库·python·json
lifewange2 小时前
SQL 中 IN 和 AND 可以搭配使用么?
数据库·sql
博语小屋3 小时前
I/O 多路转接之epoll
运维·服务器·数据库
问道飞鱼3 小时前
【大模型学习】LangGraph 深度解析:定义、功能、原理与实践
数据库·学习·大模型·工作流
DJ斯特拉4 小时前
黑马点评技术汇总(四)缓存雪崩 && 缓存击穿
数据库·缓存
lzhdim4 小时前
SQL 入门 7:SQL 聚合与分组:函数、GROUP BY 与 ROLLUP
java·服务器·数据库·sql·mysql
lifewange4 小时前
INSERT INTO ... SELECT ...
数据库·sql
Uso_Magic4 小时前
SQLSERVER__EXPLAIN 常用分析案例。
服务器·数据库·sql
IAtlantiscsdn4 小时前
Redis面试题总结
数据库·redis·缓存