资讯动态

PostgreSQL VACUUM 与 VACUUM FULL 处理过程全拆解:从原理到实战

发布时间:2026/10/21 8:06:01 来源:尧图企业网站定制
接手过太多“空间没释放、表越查越慢”的求助十个里有八个是Vacuum没跑明白。今天不聊理论书上的定义就站在实际运维的角度把VACUUM和VACUUM FULL从头到尾的处理过程掰开揉碎讲一遍。内容有点长但保证每一段都是能落地的东西。先说给我自己压个题PostgreSQL里VACUUM和VACUUM FULL是两个完全不同的工作量级前者是常规保洁后者是整屋重装。很多人以为跑个VACUUM FULL就能解决一切性能问题结果在生产库上踩了一串大坑。这篇就围绕“处理过程”四个字讲清楚它们各自在底层做了什么、锁怎么影响业务、参数怎么配、监控怎么看。1. 先搞清楚为什么PostgreSQL离不了Vacuum1.1 MVCC与死元组是怎么来的PostgreSQL的多版本并发控制MVCC有一个核心设计更新一条记录时不会原地修改数据而是插入一条新版本行数据同时把旧版本标记为不可见。删除也是一样逻辑上只是把那行数据的xmax字段标记为删除事务ID物理上它还在表文件里躺着。这在并发控制上有天然优势——读写互不阻塞读始终能看到自己事务启动时的快照。但代价就是那些“被标记为删除/更新”的旧版本行在没有任何事务需要它们之后就成了死元组dead tuple。死元组不像临时文件它不会自动消失会持续占用表空间、拖慢索引扫描甚至污染查询规划器的统计信息。我举个例子假设一张100GB的表每天有20%的数据被更新那么一天下来大约会积累20GB的死元组空间。如果不清理一个月就是600GB的膨胀磁盘再大也扛不住。而且查询时数据库要跳过这些死元组扫描成本越来越高。1.2 死元组不及时清理会带来什么后果死元组堆积的第一个后果是表膨胀。表面上看表大小没变实际上有效数据可能只占三分之一。第二个后果是索引膨胀因为索引更新也会留下死条目。第三个后果更隐蔽事务ID回卷transaction ID wraparound。事务ID是32位的大约42亿次事务后会发生回卷。如果数据库检测到某个库的事务ID年龄接近autovacuum_freeze_max_age默认2亿会强制触发anti-wraparound的autovacuum。这种强制vacuum不受常规调度约束会在高峰期直接把数据库卡成“只读保护”模式——这是生产事故里最怕遇到的一类。所以Vacuum不是数据库的附加功能而是PostgreSQL必须按时吃的药。理解了这一点接下来的处理过程才有意义。2. 普通VACUUM的处理过程全拆解2.1 VACUUM的触发路径手动和自动普通VACUUM的触发分两条路手动执行VACUUM命令或者由autovacuum后台进程自动触发。手动触发一般出现在两类场景刚做完大批量删除/更新希望立刻清理或者需要配合ANALYZE更新统计信息。命令很简单VACUUM VERBOSE ANALYZE tablename但要注意VERBOSE会输出每一张表的扫描细节生产环境大批量执行时日志会刷屏。自动触发则依赖pg_stat_user_tables里的统计信息。autovacuum进程会按autovacuum_naptime默认60秒巡检一次判断是否需要触发。判断公式如下vacuum threshold autovacuum_vacuum_threshold autovacuum_vacuum_scale_factor * reltuples默认情况下autovacuum_vacuum_threshold 50autovacuum_vacuum_scale_factor 0.2。也就是一张100万行的表死元组超过50 0.2 * 1000000 200050行时会触发autovacuum。这里有个坑对于小表50行阈值太容易触发对于超大表2%的scale factor又太迟钝。所以从PG 12开始官方不推荐倾斜使用默认值实际运维中我一般把大表的autovacuum_vacuum_scale_factor调到0.05甚至更低或者直接改用autovacuum_vacuum_insert_threshold这类更细的阈值控制。2.2 Vacuum的核心处理流程从扫描到清理普通VACUUM的标准处理流程大致可以分为五个阶段。第一阶段是准备与快照确立。VACUUM会建立一个“全局快照”记录当前所有事务的状态。这个快照决定了哪些元组是死的、哪些是不能清理的。这里有个关键点死元组并不是当前不可见就能清理必须确保任何并发事务都已经不再可能看到它。第二阶段是主表扫描。vacuum进程逐页读取堆表遍历每个页面上的行指针和元组头用之前建立的快照判断元组状态。能清理的元组ID会放进内存里的dead-tuple list同时记录每个页面可以释放的空闲空间。这个阶段也是性能消耗最大的部分因为要扫全表。第三阶段是索引清理。每个索引分别扫描并删除指向死元组的条目。这一步非常重索引越多、死元组比例越高耗时越长。所以如果你发现vacuum老跑不完先数数这张表有几个索引。第四阶段是页面内压缩与空间记录。vacuum不会把数据挪成一片紧凑的连续空间而是把页面内死元组占用的空间整理成空闲空间并记录到free space map。这样后续新插入的数据可以复用这些空间这也是VACUUM不会明显减小表文件大小的原因。第五阶段是更新统计与可见性信息。处理完页面后vacuum会更新pg_stat_user_tables里的n_live_tup、n_dead_tup、last_vacuum等字段同时把页面标记为“全可见”并写入visibility map后续的Index Only Scan才有意义。2.3 一个实际案例手动执行一次VACUUM VERBOSE我在测试环境模拟过一张非常典型的膨胀表。建一张50万行数据的表删除约30万行后执行下面的命令VACUUM VERBOSE ANALYZE user_order;输出关键信息大致如下INFO: table user_order: removed 300000 dead item identifiers in 50 pages INFO: index user_order_pkey now contains 200000 row versions in 400 pages INFO: table user_order: found 200000 removable, 300000 nonremovable row versions观察三个重要数字removable指本次清理掉的死元组数nonremovable指仍然不能被清理的元组通常是被长事务引用或者属于尚未提交的事务。看到nonremovable偏高第一反应应该是查pg_stat_activity是不是有长事务堵着。执行完之后再看pg_stat_user_tablesSELECT relname, n_live_tup, n_dead_tup, last_vacuum, last_autovacuum FROM pg_stat_user_tables WHERE relname user_order;正常情况下n_dead_tup会大幅下降但表文件大小、pg_relation_size(user_order)几乎不会变小。这就是很多新人疑惑的地方——我跑了VACUUM为什么磁盘空间没变原因就是上面第四阶段说的空间只是被标记为可复用并没有还给操作系统。要想真正缩表得请出VACUUM FULL。3. VACUUM FULL到底多做了什么3.1 什么时候必须上VACUUM FULL既然普通VACUUM不归还空间那生产表已经膨胀到查询性能严重受损、普通vacuum又救不回空间时就需要VACUUM FULL。需要注意VACUUM FULL不是普通vacuum的加强版而是另一种完全不同的处理方式。它的本质是重写整张表把所有存活元组复制到新表文件里然后重建索引、切换文件、删除旧文件。我总结了几类必须上VACUUM FULL的典型场景表被大量DELETE或UPDATE后膨胀率超过50%磁盘剩余空间紧张。大表上的频繁更新导致索引严重膨胀查询计划器估算成本过高。普通VACUUM的清理速度跟不上死元组产生速度autovacuum长时间停不下来。把表的fillfactor从默认100调低之后旧数据占满页面导致大量新行插入无法复用历史空间。但注意VACUUM FULL需要ACCESS EXCLUSIVE锁这个锁级别是最高的会完全阻塞任何读写。所以它只适合业务低谷期或者维护窗口执行不能像普通vacuum那样跑在线业务。3.2 VACUUM FULL的处理步骤与锁的影响VACUUM FULL的执行过程可以理解成“重建表”第一步拿ACCESS EXCLUSIVE锁。从这一刻起表上加任何锁的请求都排队等。如果是几TB的大表等锁的查询会一直阻塞甚至引发连接池堆积拖垮整个实例。第二步创建新表文件。它会在表所在表空间里新建一个临时文件大小不确定取决于存活数据量。生产环境有一个必须遵守的铁律执行VACUUM FULL前必须确保磁盘有至少和“当前表存活数据量”一样大的空闲空间。比如一张1TB的表存活数据只有200GB至少要预留200GB以上空间。如果磁盘只剩50GBVACUUM FULL会在复制中途报disk full结果就是表既没重建完旧表也可能处于不确定状态。第三步逐页复制存活元组。系统会扫描整个旧表把死元组全部丢弃把活元组按物理顺序写入新文件。这期间会重新整理页面的填充率所以表文件最终会变得紧凑。第四步在旧表上重建索引。这也是VACUUM FULL比普通vacuum耗时更长的原因之一所有索引都要重新构建。重建过程中如果索引太大同样需要额外的临时磁盘空间。第五步切换文件并清理旧文件。新表文件就绪后系统会更新系统目录指向新文件然后删除旧文件。这一步很快但删除巨大的旧文件也会消耗一定IO尤其是文件系统碎片多的时候。整个过程锁持有的时间几乎等于表重写的时间。我见过一个教训某业务表4TB其中有效数据2.5TB团队在周三下午执行VACUUM FULL执行了快三个小时业务侧所有针对该表的读写全部堵塞最终被迫终止。所以执行前务必评估窗口和磁盘空间。3.3 VACUUM vs VACUUM FULL一张表看清差异对比维度VACUUMVACUUM FULL锁级别ShareUpdateExclusiveLock弱锁阻塞并发DDL但不阻塞读写AccessExclusiveLock最强锁阻塞一切读写表文件大小几乎不变只释放空闲空间供复用显著变小重写表文件是否重建索引不重建只清理索引死条目全部索引重建处理速度快主要耗费IO与CPU慢表越大越慢对在线业务影响基本无感可随时执行必须在维护窗口执行是否能归还空间给OS不能能是否能防事务ID回卷能即使表不膨胀也要定期做能但代价太大没必要专门用它防回卷使用频率常规可依赖autovacuum低频通常膨胀严重才用从表上能看出来日常运维的默认动作应该是普通VACUUMVACUUM FULL只在特殊场景下用。很多人一看到表膨胀就无脑跑VACUUM FULL其实往往用错了方式。4. 实战Vacuum过程中的监控与调优4.1 怎么实时判断Vacuum是否在干活判断vacuum是否在跑最直接的SQL是查pg_stat_activitySELECT pid, datname, usename, state, query, wait_event_type, wait_event FROM pg_stat_activity WHERE query LIKE autovacuum% OR query LIKE VACUUM%;如果看到autovacuum: VACUUM public.tablename这种query说明数据库正在清理这张表。如果卡在某个状态很长时间可以结合pg_stat_progress_vacuum视图看进度SELECT * FROM pg_stat_progress_vacuum;这个视图很关键它能告诉你vacuum当前处于哪个阶段比如scanning heap、vacuuming indexes、cleaning up等等。曾经有个案例一张有30个索引的表跑autovacuum卡在vacuuming indexes阶段快40分钟排查发现是一个索引文件碎片化严重扫描特别慢。最后靠重建那个索引解决。另外一张监控表是pg_stat_user_tables。重点关注n_dead_tup和last_autovacuum两个字段。如果n_dead_tup持续高位且last_autovacuum一直不更新说明autovacuum触发有问题或被长事务阻塞。4.2 膨胀率的计算方法与自动化检查膨胀率我一般用两种方式算。第一种是估算基于pg_class里的relpages和实际数据量对比。更精确的是使用pgstattuple扩展CREATE EXTENSION pgstattuple; SELECT * FROM pgstattuple(user_order);关键输出字段解释tuple_len活元组实际占用的字节数dead_tuple_len死元组占用的字节数free_space空闲空间字节数dead_tuple_percent死元组占总空间比例当dead_tuple_percent超过30%就值得关注超过50%基本确定需要VACUUM FULL了。我在监控脚本里通常还会结合pg_class.relpages做历史趋势曲线连续多轮上涨就告警。还有一个容易漏的点普通Vacuum能清理死元组但不会修正relpages这个统计值吗实际上它会修正但这个修正是懒更新。如果统计值长期异常可以执行ANALYZE强制更新更激进的做法是用VACUUM (ANALYZE)同时处理。4.3 autovacuum关键参数怎么调先看一组我常用的配置基线作为起步值autovacuum_max_workers 3 autovacuum_vacuum_cost_delay 2ms autovacuum_vacuum_cost_limit 2000 autovacuum_vacuum_scale_factor 0.1 autovacuum_vacuum_threshold 100 autovacuum_freeze_max_age 200000000 autovacuum_vacuum_insert_threshold 1000 autovacuum_vacuum_insert_scale_factor 0.1这几个参数的核心逻辑是控制vacuum的“节流”行为。autovacuum使用cost-based I/O限制机制vacuum_cost_limit是每轮最多消耗的cost点数vacuum_cost_delay是消耗完后休眠的毫秒数。默认值通常太保守导致大表vacuum跑得极慢。把cost_delay调小到2ms、cost_limit调大到2000能够明显提升清理速度但也不能无限调大否则会抢占业务IO。超大型表的另一个做法是单独设置表的存储参数而不是全局调参ALTER TABLE user_order SET (autovacuum_vacuum_scale_factor 0.05); ALTER TABLE user_order SET (autovacuum_vacuum_cost_delay 1);对于写入频繁的表我还推荐开启autovacuum_vacuum_insert_threshold相关配置专门应对“只插入不删除”导致的插入死元组膨胀。这个场景很多业务都会有日志流水表、订单记录表插入量巨大但几乎没有update和delete传统dead tuple监控根本看不出来膨胀反而最严重。5. 常见问题与排查技巧实录5.1 为什么Vacuum一直跑不完最常见的三个原因如下。第一长事务卡住快照导致清理不了。vacuum只能清理在所有事务启动之前产生的死元组如果有事务已经开启超过数小时甚至数天那这个事务启动前产生的死元组全部不能被清理。排查方法很简单SELECT pid, xact_start, state, query FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_start ASC LIMIT 5;一旦定位到长事务要么等它结束要么考虑终止该会话。我以前遇到一个凌晨无人值守的备份任务漏了RELEASE导致事务跑了17小时整个库的vacuum全部停摆死元组从几百万涨到几千万。从那次以后我监控里常驻一条长事务告警。第二Cost-based I/O限制卡死。vacuum每处理一阵子就休眠一次如果autovacuum_vacuum_cost_delay偏大、cost_limit偏小一张几千万行的表可能跑几个小时都扫不完。此时可以在会话级执行SET vacuum_cost_delay 0; SET vacuum_cost_limit 10000;再执行VACUUM (VERBOSE) tablename;速度会有明显提升。注意这是临时高消耗IO的做法不能在业务高峰期乱用。第三表上索引过多导致索引清理阶段慢。单表超过10个索引时vacuum的索引阶段可能占总耗时80%以上。解决办法是审视是否有冗余索引低频查询索引该删就删。抓现行的办法是看pg_stat_progress_vacuum里阶段名停在vacuuming indexes的时间。5.2 为什么VACUUM FULL之后性能反而下降这听起来反直觉但真出现过。原因有几层。一个是VACUUM FULL重建索引时使用默认的fillfactor如果重建前某些索引靠近fillfactor70做压缩重建后变成100热数据的索引节点分裂更多缓存命中率反而下降。通常解决方法是给频繁插入的表设置合适的fillfactor比如90或85留出一定的更新空间。另一个原因是VACUUM FULL执行完之后统计信息并没有像ANALYZE一样完全更新。虽然VACUUM FULL会更新部分统计但如果计划器估算不准确查询计划可能走错索引。稳妥做法是VACUUM FULL之后再单独执行ANALYZE尤其在数据分布倾斜的表上。还有一种场景需要特别警惕在主从架构或者逻辑复制架构上VACUUM FULL会产生远大于普通vacuum的WAL量。因为整表重写会产生海量WAL可能把从库的回放延迟拉高到小时级。生产环境执行前必须评估复制延迟承受能力必要时候先摘掉备库。5.3 autovacuum与业务高峰的冲突处理默认情况下autovacuum没有太强的“高峰期避让”机制它只会按照cost_delay降低资源占用。但遇到的是超大表、死元组堆积严重它一样会吃掉大量磁盘IO和CPU。处理这种冲突我的思路是分表调度而不是全局关闭autovacuum。比如对核心大表设置高于全局的autovacuum_vacuum_cost_limit保证它在非高峰能快速完成对低优先级的小表设置更保守的cost delay。还可以用autovacuum_enabled暂时关闭某张表的autovacuum配合外部cron脚本在凌晨执行精确控制的手动vacuum。另一个实用技巧是错峰执行VACUUM。在cron里把不同业务的表分成多个批次分散在凌晨不同时间段执行。避免所有表同一时刻触发vacuum导致IO尖刺。5.4 一条救命的维护SQL批量生成Vacuum命令当我需要手动批量vacuum一张大库里的所有表时我不会一张张写命令而是按膨胀率排序自动生成执行清单SELECT VACUUM VERBOSE ANALYZE || schemaname || . || relname || ; AS vacuum_cmd FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 50;然后拿这个结果配合脚本按批次执行。这个方法帮我处理过无数次“一夜之间库膨胀”的故障。记住执行之前先看磁盘和IO不要一股脑全跑。结尾一点个人经验Vacuum这件事处理过程本身并不神秘真正容易出问题的是“用得不合适”。普通的VACUUM是日常保洁频率要高、负担要轻VACUUM FULL是重装系统必须有计划、有窗口、有回退预案。我自己这两年定的一条规矩是任何表执行VACUUM FULL之前先回答三个问题——磁盘空间够不够业务锁窗口允不允许从库延迟能不能扛得住三个问题答不上来宁可先用普通VACUUM扛着也别冒险。数据库运维里最贵的从来不是命令执行时间而是因为一个错误的运维决策引发的业务阻塞和故障复盘。希望这篇能帮你少踩几个坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑