VACUUM 收缩表文件时的业务影响

John Doe 七月 27, 2026

摘要:在本文中,我们将结合一个真实案例,介绍 VACUUM 收缩表文件时引发的读副本锁阻塞问题。

目录

问题背景

业务场景:客户使用 PostgreSQL 读副本承载读查询,要求查询延迟在秒级 SLA 以内;主库业务模式为对 16 个哈希分区的分区表,频繁执行大批量 DELETE 后通过 COPY 命令刷新数据。

故障表现:读副本上的 SELECT 查询周期性超时,无法满足 SLA。

初步排查:副本的 startup 进程会随机对某个分区申请排他锁,该锁会阻塞所有针对该分区的 SELECT 查询,进而导致超时;且锁的出现与主库的 autovacuum 执行周期强相关。

根因定位

堆表存储空间

PostgreSQL 的 VACUUM 在清理死元组后,会尝试将表末尾连续的空闲页(即高水位线之后的空白空间)归还操作系统,这个截断表文件的操作在主库需要对表加排他锁。

主节点一旦成功截断了表文件,会将这个修改记录到 WAL(预写日志)中。备节点的 Startup 进程在回放这条日志时,为了保证主备物理文件严格一致,必须在备库上获取该表的 AccessExclusiveLock

结果

  1. 阻塞新请求: Startup 进程申请排他锁会进入锁队列,这将直接导致备库上所有后续新发起的 SELECT 查询全部被堵塞
  2. 干掉老请求: 如果备库上正好有较长的查询正在执行,Startup 进程会等待一段固定的时间(受 max_standby_streaming_delay 参数控制)。一旦超时,为了不让备库的数据延迟过大,PostgreSQL 会强行取消正在执行的读业务,抛出大名鼎鼎的 canceling statement due to conflict with recovery 错误。

解决方案

对分区表的所有分区执行参数调整,关闭 VACUUM 的自动截断功能:

ALTER TABLE [table name / partition name] SET (vacuum_truncate = false);

用空间换稳定: 关闭自动截断后,表文件尾部的空闲空间不会还给操作系统,物理文件不会变小。但这些空间依旧留在文件中,可以被该表未来的 INSERTUPDATE 重复利用。这彻底消除了主库释放锁、备库回放锁带来的读业务阻塞问题。

为了平衡“避免锁阻塞”和“磁盘空间回收”,我们可以基于 pg_freespacemap 扩展进行监控和管理:

安装 pg_freespacemap 扩展,编写自定义 PL/pgSQL 函数 show_empty_pages,可以逆向扫描表的页,定位高水位线位置,计算表末尾可截断的空闲页数与对应空间大小。

若监控发现表大小持续增长、末尾空闲空间过多,可在业务低峰期手动执行带截断的 VACUUM 回收空间:

VACUUM (verbose, truncate true) [schema].[table name];

执行该命令时,会在 VACUUM 输出中看到一条额外的消息,表示 VACUUM 截断了表文件:

INFO: table "large_table": truncated 302534 to 302233 pages

VACUUM 截断时的业务先行原则

为了不阻塞主节点上的正常业务,autovacuum 在尝试截断文件时,使用的是条件锁(Conditional Lock)

autovacuum 会尝试获取 AccessExclusiveLock,但如果此时表上有任何其他业务操作(例如 SELECTINSERTUPDATE 正在持有共享锁),条件锁就会获取失败。另外,在从表末尾逆向扫描空闲空间时,检查到有新的业务访问请求时,VACUUM 也会直接放弃截断文件的动作,结束工作。它不会在队列里死等,也不会杀掉业务进程。因此,对于读写极其频繁的表,autovacuum 几乎抓不到那转瞬即逝的空闲窗口,表文件物理大小就降不下来。

总结与建议

该问题在 PostgreSQL 中没有通用的最优解,需要结合业务权衡:

  • 对 SLA 要求高的读副本场景,可以关闭 vacuum_truncate 避免锁阻塞,同时定期监控表文件的空闲空间,手动回收空间;
  • 也可以通过调整 autovacuum 的执行频率等参数,降低截断操作的影响;
  • 核心原则是理解 VACUUM 每一步的副作用,结合自身业务 SLA 与空间诉求做出调整。

当然,也可以考虑使用 Redrock Postgres,从底层存储与事务机制层面,根本上解决数据膨胀的问题。

参考

Shane Borden:理解 PostgreSQL VACUUM 操作中的高水位锁定问题

PostgreSQL 源代码:access/heap/vacuumlazy.c:lazy_truncate_heap()