PostgreSQL autovacuum 与表膨胀
监控 dead tuples、冻结风险和 vacuum 进度并安全调整高写入表
标准 VACUUM 的目标不只是“释放空间”:它让 dead row versions 可复用、维护 planner statistics 和 visibility map,并防止 transaction ID/multixact wraparound。多数系统应保持 autovacuum 开启。
日常观测
SELECT
schemaname, relname,
n_live_tup, n_dead_tup,
last_vacuum, last_autovacuum,
vacuum_count, autovacuum_count,
last_analyze, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 30;统计是估算且会重置,不能仅凭一个 n_dead_tup 阈值判断膨胀。结合表大小、更新速率、查询延迟、autovacuum 日志和趋势。
查看正在运行的 vacuum:
SELECT
pid, datname, relid::regclass AS relation,
phase, heap_blks_total, heap_blks_scanned, heap_blks_vacuumed,
index_vacuum_count, dead_tuple_bytes, num_dead_item_ids,
indexes_total, indexes_processed
FROM pg_stat_progress_vacuum;这些字段名对应 PostgreSQL 18;较早 major 的 progress view 列可能不同,跨版本监控应先核对目标版本目录。
为什么没有触发或跟不上
- 表很大,默认 scale factor 对应的变更行数过高;
- worker、I/O 或维护内存不足;
- 长事务、prepared transaction、复制槽或 standby snapshot 阻止回收;
- vacuum 经常被冲突锁取消;
- 写入峰值持续高于清理能力。
针对已验证的热点表覆盖参数,而不是先全局激进调整:
ALTER TABLE app.events SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_vacuum_threshold = 1000,
autovacuum_analyze_scale_factor = 0.01
);参数只是示例。根据表大小和每天变更量计算触发频率,并观察 I/O、WAL、延迟与实际完成时间。
手工维护边界
VACUUM (ANALYZE, VERBOSE) app.events;普通 VACUUM 主要让空间在关系内部复用,通常不会把文件缩回操作系统。VACUUM FULL 会重写整张表、需要额外磁盘并获取 ACCESS EXCLUSIVE 锁,不是日常清理命令。
测量与修复膨胀
pg_stat_user_tables 中的 dead tuple 比例是估算值。pgstattuple 会扫描整个关系,给出精确的 dead tuple 与空闲空间:
CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT * FROM pgstattuple('app.events');
-- dead_tuple_count, dead_tuple_percent, free_space, free_percent当必须把空间还给操作系统时,普通 VACUUM 做不到,而 VACUUM FULL 在整个重写期间阻塞读写。常用的在线方案:
# pg_repack 在后台重建表,只持有短暂的锁
sudo -u postgres pg_repack -h db.example -U postgres -d commerce --table=events-- 仅索引膨胀:不锁表重建单个索引
REINDEX INDEX CONCURRENTLY app.events_pkey;REINDEX CONCURRENTLY 自 PostgreSQL 12 起内建。pg_repack 是第三方扩展,有独立的版本与运维要求——安装、版本核对和演练都应独立于核心功能进行。
VACUUM FULL 看似卡住时
几乎总是在等现有会话释放 ACCESS EXCLUSIVE 锁。重试前先找出阻塞者:
SELECT a.pid, a.usename, a.state, a.wait_event, left(a.query, 160) AS query
FROM pg_stat_activity a
WHERE a.pid = ANY (pg_blocking_pids(12345)); -- VACUUM FULL 会话的 pid确认持有者可以放弃后再 cancel 或 terminate。即使成功运行,VACUUM FULL 也需要约等于表大小的空闲磁盘并重写全部索引——这也是日常回收空间优先用 pg_repack 的原因。
事务 ID wraparound 防护
事务 ID 是 32 位:约 20 亿个事务之后,旧元组会看起来“来自未来”,PostgreSQL 会在此之前停止接受写入。autovacuum 通过 freeze 旧元组回收事务 ID 空间来防止回卷。
按 database 和表跟踪 freeze age:
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;
SELECT n.nspname, c.relname,
age(c.relfrozenxid) AS xid_age,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY xid_age DESC
LIMIT 20;常用的告警起点:database 级 xid_age 超过 10 亿需要关注,超过 15 亿属于紧急情况——具体阈值应结合事务速率校准。如果 autovacuum 来不及 freeze,在低峰窗口强制 freeze,并在 pg_stat_progress_vacuum 中观察进度:
VACUUM (FREEZE, VERBOSE) app.events;整库 age 偏高时省略表名,对整个 database 执行。
不要关闭 autovacuum 解决性能问题
先找出具体表、阶段、等待事件和资源瓶颈。关闭 autovacuum 会积累 dead tuples、陈旧统计和冻结风险;反 wraparound vacuum 即使表级设置关闭也可能运行。
Last updated on