PostgreSQL 垃圾清理的高成本和非确定性

John Doe 一月 26, 2026

摘要:你有没有在 PostgreSQL 中遇到过多个 autovacuum 进程不停工作的情况?此时,系统中的大部分资源都被用来进行 VACUUM。在本文中,我们将了解 PostgreSQL 垃圾清理的高成本和非确定性,以及相关的解决方案。

目录

旧行版本清理的用例

下面我们在 PostgreSQL 的数据库中,创建一个表,插入一些数据,并对表数据进行清理和分析。

-- 会话一
CREATE TABLE t_large (id integer, name text);
INSERT INTO t_large (id, name)
  SELECT i, repeat('Pg', 64)
    FROM generate_series(1, 1000000) AS s(i);

CREATE INDEX large_idx ON t_large (id);
VACUUM ANALYZE t_large;

在准备好基础表和初始数据后,让我们启动一个会话,在该会话中开始一个事务,并保持在空闲状态:

-- 会话二
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT name FROM t_large WHERE id = 202510;

让我们对表中的一部分数据进行一些更新,当然,也可以删除一部分数据。

-- 会话一
UPDATE t_large SET name = 'dummy' WHERE mod(id, 100) = 1;
DELETE FROM t_large WHERE mod(id, 100) = 50;

SELECT
  C.reltuples,
  S.n_dead_tup,
  S.n_ins_since_vacuum,
  S.n_mod_since_analyze,
  S.last_vacuum
FROM pg_class C LEFT JOIN
     pg_stat_all_tables S ON C.oid = S.relid
WHERE S.relname = 't_large';
 reltuples | n_dead_tup | n_ins_since_vacuum | n_mod_since_analyze |         last_vacuum
-----------+------------+--------------------+---------------------+------------------------------
     1e+06 |      20000 |                  0 |               20000 | 2026-01-23 12:17:17.96903+08
(1 row)

上面的更新和删除操作,表中已经产生了一定量的旧行版本。现在,我们来对该表进行 VACUUM 清理,以观察 autovacuum 进程清理旧行版本的过程:

-- 会话一
VACUUM (verbose) t_large;
INFO:  vacuuming "postgres.public.t_large"
INFO:  finished vacuuming "postgres.public.t_large": index scans: 0
pages: 0 removed, 20409 remain, 20409 scanned (100.00% of total)
tuples: 0 removed, 1010000 remain, 20000 are dead but not yet removable
removable cutoff: 753, which was 2 XIDs old when operation ended
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 40833 hits, 0 misses, 0 dirtied
WAL usage: 1 records, 0 full page images, 188 bytes
system usage: CPU: user: 0.12 s, system: 0.00 s, elapsed: 0.12 s
...

SELECT
  C.reltuples,
  S.n_dead_tup,
  S.n_ins_since_vacuum,
  S.n_mod_since_analyze,
  S.last_vacuum
FROM pg_class C LEFT JOIN
     pg_stat_all_tables S ON C.oid = S.relid
WHERE S.relname = 't_large';
 reltuples | n_dead_tup | n_ins_since_vacuum | n_mod_since_analyze |          last_vacuum
-----------+------------+--------------------+---------------------+-------------------------------
    990000 |      20000 |                  0 |               20000 | 2026-01-23 12:18:07.681234+08
(1 row)

上面的 VACUUM 操作扫描了全表 20409 个页面,没有清理任何的旧行版本。由于会话二中的事务还在进行中,系统需要保留旧行版本以对该事务保持可见性,会话一的 VACUUM 操作只会做无用功,不会清理任何的垃圾数据。

对于后台 autovacuum 进程定期调度执行的 VACUUM 动作,同样也会出现上面的问题,它会频繁消耗存储的 IO 资源,实际上没有清理掉任何的旧行版本。而且,随着后续不断地进行更新和删除操作,系统产生的旧行版本会持续积累,最终会给系统造成如同雪崩的效应。

旧行版本清理的场景分析

PostgreSQL 采用多版本并发控制(MVCC)机制,当数据被更新或删除时,原始记录不会立即移除,而是标记为不可见,以支持事务回滚和快照读,这就产生了旧行版本。例如,在一个订单状态频繁更新的订单表中,每次更新都会产生旧行版本。

有一个常驻后台进程,称为自动清理启动进程(autovacuum launcher),每隔 autovacuum_naptime 设置的时间,会尝试在每个数据库中启动一个清理工作进程。如果从上次VACUUM以来失效的元组数超过一定阈值,后台 autovacuum 就会对表进行清理,触发清理工作的条件如下:

pg_stat_all_tables.n_dead_tup >
  autovacuum_vacuum_threshold +
  autovacuum_vacuum_scale_factor * pg_class.reltuples

如果自上次VACUUM以来插入的元组数量超过一定阈值,也会对表进行清理,后台 autovacuum 触发清理工作的条件如下:

pg_stat_all_tables.n_ins_since_vacuum >
  autovacuum_vacuum_insert_threshold +
  autovacuum_vacuum_insert_scale_factor * pg_class.reltuples

在上面的用例场景中,当后台 autovacuum 启动的工作进程无法有效地清理旧行版本时,每隔 autovacuum_naptime 设置的时间,会持续不断地启动新的工作进程进行清理,导致在同一时间会有 autovacuum_max_workers 个清理工作进程繁忙地工作。

在数据量庞大且更新频繁的场景下,旧行版本清理成本会很高。因为清理过程(VACUUM)需要扫描大量数据,判断哪些行版本不再需要,并回收空间。要降低旧行版本的清理成本,可以考虑采用以下措施:

  1. 定期执行 VACUUM 操作:合理安排 VACUUM 的执行时间和频率,例如在业务低峰期执行,减少对业务的影响。同时,可以根据表的特点选择合适的 VACUUM 方式,如普通 VACUUM 清理死元组,VACUUM FULL 可释放磁盘空间但会阻塞访问。
  2. 优化 autovacuum 参数:调整 autovacuum_max_workersautovacuum_naptime 等参数,使 autovacuum 进程能够更高效地清理旧行版本。例如,适当降低 autovacuum_max_workers 和增加 autovacuum_naptime,减小无效清理工作给数据库系统带来的影响。

结论

在本文中,我们了解了 PostgreSQL 清理旧行版本可能会出现的雪崩现象,以及缓解该问题的可能措施。

另外,Redrock Postgres 引入了回滚日志,系统可以通过在新行版本上应用回滚日志按需生成旧行版本。另外,在执行删除或非就地更新的事务提交后,会立即重用空间。因而在更新或删除表数据后,并不需要定期触发执行旧行版本的清理工作。

注意:在 Redrock Postgres 中,后台 autovacuum 进程依然存在,需要依赖它们定期对表进行 ANALYZE,更新数据库的统计信息。您可以将 autovacuum_vacuum_threshold 设置为一个超大值(如 2000000000),将 autovacuum_vacuum_insert_threshold 设置为 -1,禁止 autovacuum 进程执行清理工作。