西西软件园多重安全检测下载网站、值得信赖的软件下载站!
软件
软件
文章
搜索

首页 → 西西教程 → 数据库教程 → PG表从8GB膨胀到200GB:autovacuum停摆的3个原因

PG表从8GB膨胀到200GB:autovacuum停摆的3个原因

相关软件相关文章发表评论 来源:本站整理时间:2026/11/2 17:42:07字体大小:A-A+

作者:xiix点击:4次评论:0次标签: PostgreSQL autovacuum 表膨胀 VACUUM 数据库空间清理

  • 类型:数据库类大小:229M语言:中文 评分:10.0
  • 标签:
立即下载

上周做季度巡检的时候,我在监控面板上看到一台跑了两年的 PostgreSQL 机器,磁盘用量从年初的 60G 一路涨到了 240G,其中数据目录占了 205G。我一开始以为是业务日志没清,或者是有人把备份文件放到了数据目录里。结果查到最后,什么都不是那张最大的业务表,pg_total_relation_size 显示 187G,可里面的活数据撑死也就 30G,剩下的全是死行撑出来的空间,而且 autovacuum 已经整整三周没对这表动手了。

PostgreSQL 表越用越大、磁盘明明没多少真实数据却快满了,autovacuum 到底干了什么?pgAdmin 4数据库管理工具(数据库类,v9.18)自带服务器监控面板,能直接看到每张表的实时行数、膨胀情况和 autovacuum 活动情况,不用每次都 ssh 上服务器敲 psql,排查这种「表变胖」的问题能省下大量翻日志的时间。

这次排查前后花了大半天,最后定位在三个原因上,而且每一个单独拿出来都够让 autovacuum 停摆。所以这篇文章我把这套排查流程完整记下来:怎么确认表是真的膨胀了、autovacuum 为什么不动、手工怎么救场,以及之后该怎么调参数让它别再犯。

运维工作台上的 PostgreSQL 数据库管理界面与监控面板

一、先确认是不是真的膨胀,别急着动手

PostgreSQL 是 MVCC 的,行被 UPDATE 或 DELETE 之后并不会立刻消失,只是被标记成「死行」,空间留等着被 VACUUM 回收。所以「表空间大」本身不等于「有问题」,关键是看死行占比和 vacuum 进度。

第一条命令看单表状态:

SELECT relname,
    n_live_tup,    n_dead_tup,
    pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
    last_autovacuum,
    last_autoanalyze
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 10;

n_dead_tup 是死行估算值,n_live_tup 是活行数。经验上死行超过活行的 20%~30% 就该处理了。我那张表是 live 2.6 亿、dead 4.1 亿,同时还挂着 3 个没清理的长事务,这就是典型的「VACUUM 跑不动」的状态。

第二条命令看是不是有 VACUUM 正在跑、以及它被卡在什么地方:

SELECT pid, now() - xact_start AS runtime, phase,
    heap_blks_total, heap_blks_scanned, heap_blks_vacuumed
FROM pg_stat_progress_vacuum;

如果这里一直有行,但 heap_blks_vacuumed 长时间不动,说明 VACUUM 被卡住了,不是没启动而是推不动。

第三条命令查长事务,这是最容易被忽略的一环:

SELECT pid, now() - xact_start AS age, state, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start LIMIT 10;

只要有一个事务打开着没提交,它持有的快照就会让 VACUUM 无法回收比它更老的死行。我们那台机器上有个跑了 11 天的定时任务一直开着事务没关,就是把 vacuum 拖死的元凶之一。

PostgreSQL 表由整齐数据方块变成大量闲置空块堆积的膨胀对比示意图

二、autovacuum 停摆的三个原因,我全踩过

原因一:长事务和未关闭的空闲事务把快照钉住了

这是最常见的一种。VACUUM 的原理是扫描死行并确认没有活跃事务的快照还能看到它,只要有一个老事务开着,它就不敢回收这些行。表现是 autovacuum 日志里反复出现同一条记录:

WARNING: skipping "orders" --- only vacuuming, not vacuuming indexes
ERROR: canceling statement due to conflicting lock

处理方式很直接:把不需要的长事务杀掉(确认业务允许的话 SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE ...),同时在代码层面修掉「连接池事务不释放」和「定时任务不 commit」这类问题。同时给 autovacuum 加上 idle_in_transaction_session_timeout,让这种空闲事务最多活 10 分钟。

原因二:autovacuum 的 worker 全被占满

autovacuum 是并发跑的,默认全局 worker 数量是 3。如果库里同时有几十张表需要清理,3 个 worker 根本不够用,剩下的表只能排队。我那次遇到的日志是 ERROR: canceling autovacuum task ... 和 worker 被反复抢占。查配置:

SHOW autovacuum_max_workers;
SHOW autovacuum_vacuum_cost_delay;
SHOW autovacuum_vacuum_cost_limit;

机器是 8 核 32G 的,把 autovacuum_max_workers 调到 8~10 之后积压立刻缓解了。另外一个常见坑是磁盘 IO 限制:autovacuum_vacuum_cost_delay 默认是 2ms,成本限流配得太狠,vacuum 扫描速度就只有每秒几十个数据块,8TB 的表根本扫不完。IO 充足的机器上把这个延迟调到 0 或 1ms 效果立竿见影。

原因三:写入太猛,膨胀速度永远跑赢回收速度

autovacuum 的触发阈值是按比例算的:autovacuum_vacuum_scale_factor(默认 0.2)乘以表的行数,再加上 autovacuum_vacuum_threshold(默认 50)。意思是死行超过「表的 20% + 50 行」才会触发 vacuum。问题是小表无所谓,大表就致命了一张 5 亿行的表,要死够 1 亿行才会启动回收,而清理本身又需要好几天。也就是说系统长期处于「边撑大、边慢慢清」的状态,看起来就没停过,其实永远追不上。

正确的做法是对大表单独设置表级参数,别用全局默认:

ALTER TABLE orders SET (
    autovacuum_vacuum_scale_factor = 0.02,
    autovacuum_vacuum_threshold = 5000,
    autovacuum_vacuum_cost_delay = 0,
    autovacuum_analyze_scale_factor = 0.01
);

顺带说一句,autovacuum 还有另一个触发条件是「事务 ID 回卷」(wraparound)。这个是硬性上限,超过 autovacuum_freeze_max_age 时 PostgreSQL 会强制对全库做 anti-wraparound vacuum,宁可停机也必须做,因为事务 ID 回卷会导致旧数据「复活」,那是数据正确性事故,级别比磁盘满了高得多。

运维工程师在服务器上排查 PostgreSQL 空间占用与 vacuum 状态

三、手工救场:VACUUM、VACUUM FULL、pg_repack 怎么选

参数调优需要时间,磁盘可等不了。下面三条命令的区别,选错了能把库搞挂,我说清楚。

第一步,先做普通 VACUUM。它只把死行空间标记成可复用,不锁表也不重写数据,磁盘占用不会立刻下降,但会先把膨胀趋势止住。

VACUUM (VERBOSE, ANALYZE) orders;

第二步,VACUUM FULL 才能真正把空间还给操作系统。它会重写整张表到新文件再切换,中途需要持有 ACCESS EXCLUSIVE 锁,也就是业务完全读写不了。我那张 187G 的表跑了 40 分钟,这 40 分钟里应用全部卡死。生产环境想用 FULL,必须先在低峰期停写,或者在只读副本上做;而且要预留同等大小的磁盘空间重写期间新旧两份文件是同时存在的。

VACUUM FULL (ANALYZE) orders;

第三步,业务不能停的话用 pg_repack。它基于触发器把变更同步到新表,在最后短暂加锁做切换,整体过程几乎无感知,是目前线上缩减大表空间最稳的方案,代价是要装扩展并且占用额外磁盘。

pg_repack -t orders -d appdb

我最后用的是 pg_repack,晚上低峰跑的,停了不到 10 秒,空间从 187G 降到了 46G,回落到正常水位。

四、这套问题以后怎么防

现在我把巡检固定成了几个每周必查的项,写在巡检脚本里,跑一次大概几秒钟:

-- 死行占比 Top 10
SELECT relname, n_live_tup, n_dead_tup,
    round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC LIMIT 10;

配套的告警我设了三条:磁盘使用率超过 75% 报警、超过 85% 紧急;单表死行占比超过 30% 提醒;last_autovacuum 距离当前时间超过 6 小时且表有更新,提醒检查 autovacuum 状态。工具上我用的是 pgAdmin 的监控面板做日常观察,脚本做周报,两者互补。

另外有几个细节值得单独提一句:一是把 log_autovacuum_min_duration 打开(设成 1000 毫秒),autovacuum 干了什么、扫了多少块都会写进日志,出问题时有据可查;二是别小看 pg_stat_activity 里的 state='idle in transaction',这类连接不占 CPU,但会把 vacuum 拖住,是那种「监控上一片祥和、空间却在偷偷涨」的元凶;三是给业务表做定期的分区或者归档,把只读的冷数据挪到历史表或者归档库里,别让一张表无限膨胀。

说到底,PostgreSQL 的表膨胀不是 bug,是 MVCC 机制和默认阈值共同作用的结果,只要 autovacuum 能正常跑起来,这件事根本不会出现在你的凌晨里。真正需要做的是提前把 worker 数量、成本限流和表级阈值调对,再配上一条死行占比的告警,让膨胀在 30% 的时候就有人看见,而不是等到磁盘快满了才来救火。

    相关评论

    阅读本文后您有什么感想? 已有人给出评价!

    • 8 喜欢喜欢
    • 3 顶顶
    • 1 难过难过
    • 5 囧囧
    • 3 围观围观
    • 2 无聊无聊

    发表评论

    昵称:
    表情: 高兴 可 汗 我不要 害羞 好 下下下 送花 屎 亲亲