
在生产环境中数据库 CPU 利用率在某个时间点突然异常增高几分钟后恢复正常。这种情况导致在这几分钟内服务可用性下降。由于数据库使用的是 AWS RDSpostgresql通过 Database Insights 查看异常时间点的 Top SQL发现存在 autovacuum 事件。最终分析到原因是在高事务推进速度下常规 vacuum 无法把业务表的冻结年龄持续压低最终周期性逼近防回卷区间触发anti-wraparound autovacuum从而引起短时 CPU/WAL/IO 峰值。PostgreSQL 面临“事务ID回卷”的风险需要主动进行“冻结”来防范而 MySQL (InnoDB) 则没有这个问题。这种差异源于两者在实现 MVCC 时不同的核心架构选择。下面是具体的分析过程。XID 回卷PostgreSQL 的 MVCCMulti-Version Concurrency Control 依赖 XID即事务 ID。举例说明假设 user 表中有一行数据其由第 50 个事务创建。id name 1 Tom现在第 100 个事务修改了这条记录将 name 改成了 Jerry。在 PostgreSQL 中并不会直接覆盖原纪录而是会有两条记录同时存在id name xmin xmax 1 Tom 50 100 1 Jerry 100 NULL其中xmin表示创建该行数据的事务 IDxmax为删除失效该行数据的事务 ID。换句话说第一行数据对事务 id 在 [50100区间的事务可见第二行数据对事务 id 在 [100, NULL) 区间的事务可见。事务 ID 回卷在 PostgreSQL 中事务 ID 用 32 位整数表示约 42 亿。当达到上限后事务 ID 会回卷至 0。一旦回卷新事务的 ID 就可能比旧事务的 ID 更小从而导致在数据可见性方面产生问题。为了解决这一问题PostgreSQL 通过VACUUM机制将那些足够老的事务 ID 标记为一个特殊的、对所有事务都可见的 “冻结 ID”relfrozenxid。PostgreSQL 为每个表都维护了一个relfrozenxid用来表示这个表中所有事务 ID 小于relfrozenxid的记录都已经被冻结。那如何断定什么是足够老的事务 ID 呢Vacuum 分为两种一种是常规的一种是强制的anti-wraparound VACUUM。常规 vaccum 是动态的其选择冻结的事务 id 会结合不同的参数和当前系统状态算出来。常规 vacuum 消耗的资源少一些对数据库影响相对较小。而强制 vacuum 则是有固定规则的表中当前活跃的最老的事务 id 的年龄接近autovacuum_freeze_max_age时会触发强制 vacuum以保证事务安全。强制 vacuum 会占用大量资源导致 CPU、网络、读写延迟都会增加。autovacuum_freeze_max_age理论上可以是最大事务 id 的一半即 21 亿。默认情况下该值为 2 亿通过 sql 语句可以查询SHOWautovacuum_freeze_max_age;这个值表示当前数据库中活跃的事务 id 的范围不能超过 2 亿。假设某个表中最老的活跃事务 id 是 1 kw当前数据库中最大事务 id 是 2.1 亿则这个表中这条最老的事务年龄就达到了autovacuum_freeze_max_age阈值会触发anti-wraparound VACUUM。autovacuum_freeze_max_age 最大值理论上来说只要允许活跃的事务 id 范围不超过最大值的一半就可以达到循环利用事务 id 的效果。举个例子说明假设数据库事务 id 最大值为 12允许活跃的事务为 6 个。如果当前活跃的事务 id 最大值为 9最小值为 2则 xid 2 的活跃事务必然是比 9 更新的事务。因为只有按照 9 - 10 - 11 - 12 - 1 - 2 的方向演进活跃的事务范围才不超过 6。反过来如果是按照 2 到 9 的顺序演进事务范围就大于 6 了不满足条件。如果允许活跃的事务 6 呢假设为 7。当前活跃的事务 id 最大为 9最小为 3。如果按照 3 - 4 - 5 - 6 - 7 - 8 - 9 的顺序演进满足条件因此认为 xid 9 的事务是新的事务。反过来按照 9 - 10 - 11 - 12 - 1 - 2 - 3 的顺序演进同样满足条件因此认为 xid 3 的事务是新的事务。这就产生矛盾了。如果允许活跃的事务数超过事务 id 最大值的一半就无法判断两个事务的新旧所以autovacuum_freeze_max_age的值最大可以为 2^31。Trouble-Shooting想要判断是否是anti-wraparound VACUUM导致数据库 CPU 飙升可以通过一些直观的手段来查看是否有 Vacuum 事件比如 AWS Database Insights 监控。如果不能直观的看出也可以通过 sql 语句来判断。查询活跃事务 id 最老的表SELECT c.oid::regclass AS table_name, greatest(age(c.relfrozenxid), age(t.relfrozenxid)) AS xid_age, pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size FROM pg_class c LEFT JOIN pg_class t ON c.reltoastrelid t.oid WHERE c.relkind IN (r,m) ORDER BY xid_age DESC LIMIT 10;如果某个表中xid_age接近 2 亿说明即将或者已经触发强制 vacuum。如果还差很多但是 xid_age 增长速度很快间隔几分钟后再次查看那也有可能在不久的将来会触发强制 vacuum。当然也可能触发普通 vacuum 将老龄的事务 id 给冻结了。通过以下语句可以查看该表上次普通 vacuum 的时间SELECT relname, n_tup_ins, n_tup_upd, n_tup_del, n_dead_tup, last_autovacuum, autovacuum_count FROM pg_stat_user_tables WHERE relname xxx;结果示例如下说明普通 vaccum 确实有触发relname n_tup_ins n_tup_upd n_tup_del n_dead_tup last_autovacuum autovacuum_count xxx 1675710 11324652 30733 9366 2026-06-25 10:37:58.796300 52如果写少 upd/del 低同时autovacuum_count低说明这张表不容易被普通 vacuum 清理。与此同时如果全局 xid 推进速度快那这张表就很容易被动变老。再看看全库 xid 推进速度SELECT datname, xact_commit, xact_rollback, blks_read, blks_hit, tup_inserted, tup_updated, tup_deleted FROM pg_stat_database ORDER BY (xact_commit xact_rollback) DESC;结果如下datname xact_commit xact_rollback blks_read blks_hit tup_inserted tup_updated tup_deleted rds 2646817444 1329881 12193124 37985177484 121269311 15274081 1423400xact_commit和xact_rollback高说明全库事务推进快。tup_inserted、tup_updated、tup_deleted高说明全局写活动频繁。结果分析根据以上结果可得结论如下业务表xid_age高且增长速度快说明该表确实是高龄表已经进入中高风险区间具备触发防回卷清理的前提。业务表写多相对其他表来说autovacuum_count多last_autovacuum时间近说明不是没有触发 autovacuum而是触发了很多次但仍然老化偏高。原因是全库事务推进速度太快即使业务表在持续 vacuum也可能很快再次变老。随着 age(relfrozenxid) 接近autovacuum_freeze_max_age阈值系统触发 anti-wraparound autovacuum。要验证也很容易只需要查看数据库 events 或者 logs比如 AWS Log Insights 可以搜索fields timestamp, message | filter message like /wraparound/ # | filter message like /autovacuum/ | sort timestamp desc | limit 100看看是否有防回卷清理或普通清理。优化措施事实上不管是普通 vacuum还是强制 vacuum都会导致 CPU 升高只不过强制 vacuum 更严重一些。可以通过调整 vacuum 相关的 RDS 参数来优化资源占用参数含义默认值调整效果autovacuum_vacuum_scale_factor触发 vacuum 阈值的比例项0.1vacuum 阈值 autovacuum_vacuum_thresholdscale_factor× 表行数scale_factor越小越早触发 vacuum每次 vacuum 就更平滑autovacuum_vacuum_threshold触发 vacuum 阈值基础项PostgreSQL 默认 50正常无需调autovacuum_vacuum_cost_limitvacuum 的成本值GREATEST({log(DBInstanceClassMemory/21474836480)*600},200)这个值本身是根据实例内存来计算的内存越大允许 vacuum 占用资源就越多这是一个经验值不建议调整autovacuum_vacuum_cost_delayvacuum 的成本值达到后暂停时间继承 PostgreSQL 的 2ms与autovacuum_vacuum_cost_limit配合作用本次 vacuum 消耗达到cost_limit后暂停cost_delay时间后再继续越大越平滑但清理更慢autovacuum_work_mem每个 autovacuum worker 的工作内存默认与maintenance_work_mem一样如果太小autovacuum 可能需要多次扫描索引导致执行时间变长和 I/O 增加优化思路是提高 vacuum 频次降低单次 vacuum 体量。因此考虑如下调整对热点大表降低autovacuum_vacuum_scale_factor增加 vacuum 频率以减少单次 vacuum 体量。适当降低autovacuum_vacuum_cost_limit并提高autovacuum_vacuum_cost_delay降低瞬时 CPU/IO 冲击。