暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PostgreSQL数据库|从“表膨胀”到“TXID回卷”:揭开 PostgreSQL Autovacuum 的守护之道

安呀智数据坊 2025-11-05
37

注: 本文为安丫科技刘峰的原创,请尊重知识产权,转发请注明出处,不接受任何抄袭、演绎和未经注明出处的转载。

在任何一个健康的PostgreSQL数据库背后,都有一个默默无闻但至关重要的英雄:autovacuum。

对于许多开发者和初级数据库管理员(DBA)来说,它是一个“即插即用”的后台进程,自动处理着数据库的“家务事”。

然而,要真正发挥PostgreSQL的极致性能,并避免潜在的灾难,深入理解autovacuum的工作原理、如何调优以及如何监控,是必不可少的技能。


01

为什么我们需要 Autovacuum?

 从 VACUUM 的基础说起 

我们已经知道,VACUUM 是 PostgreSQL 的“健康管家”,它主要负责两件生死攸关的大事:


回收空间,对抗膨胀


清理因 UPDATE 和 DELETE 产生的“死亡元组”,将空间标记为可复用,防止表和索引无限膨胀。


冻结事务ID,防止回卷


将旧的事务ID(TXID)标记为“冻结”,以避免发生 TXID 回卷,这种灾难会导致数据库为保护数据而强制停机。


VACUUM 是解决这些问题的动作。

那么问题来了:谁来执行这个动作?以及,何时执行?


1

DBA 将面临四个无法解决的困境

如果我们没有 Autovacuum,就意味着所有 VACUUM 操作都必须由数据库管理员(DBA)手动执行。

让我们设想一下一个只有手动 VACUUM 的世界,DBA 将面临四个无法解决的困境:


困境一:时机之谜——“我该何时运行 VACUUM?”


一个生产数据库可能有成百上千张表。DBA 如何知道哪张表“脏了”,需要清理?


  • 盲目执行?

    每天对所有表运行一次 VACUUM?

    对于大型数据库,这会产生巨大的、不必要的 I/O 开销。

    对于几乎不更新的表,这是纯粹的资源浪费。

  • 基于监控?

    DBA 需要编写复杂的监控脚本,持续查询 pg_stat_user_tables 视图,检查每张表的 n_dead_tup(死亡元组数)。

    当数量超过某个阈值时,再触发 VACUUM。

    但这引出了下一个问题:阈值该设为多少?1万?10万?这对大表和小表来说标准完全不同。


手动管理 VACUUM 的时机,是一项极其繁琐、低效且容易出错的工作。


困境二:规模之痛——“我如何管理成千上万张表?”


数据库的表写入模式千差万别:

  • 高频写入表(如 iot_data, logs)

    可能每分钟都会产生数万个死亡元组。

  • 中频更新表(如 users, orders)

    每天有稳定的更新。

  • 静态配置表(如 countries, settings)

    几乎从不改变。


一个负责任的 DBA 必须为不同类型的表制定不同的 VACUUM 策略。

手动去跟踪和管理这一切,在规模扩大后,会成为一场噩梦。


困境三:沉默的杀手——“我如何处理从不‘脏’的表?”


这是手动 VACUUM 策略中最致命的缺陷。

我们知道,VACUUM 的另一个核心任务是防止 TXID 回卷。

TXID 回卷的风险恰恰来自于那些很少或从不更新/删除的表。

因为它们没有死亡元组产生,所以从“表膨胀”的角度看,它们永远是“干净”的。

一个只关心“脏表”的 DBA,可能永远不会对这些静态表执行 VACUUM。

但这些表中的行所持有的 TXID 却在随着时间的推移而不断“老化”。

当老化到极限时,数据库为了防止数据损坏,会强制关闭写服务。




这是一个巨大的陷阱

最需要 VACUUM 来“冻结”TXID 的表,恰恰是手动策略最容易忽略的表。


困境四:人的弱点——“我能保证 24/7 永不疏漏吗?”


依赖手动操作或简单的 Cron 定时任务,本质上是脆弱的。

  • DBA 会休假,会疏忽。

  • 定时任务可能在业务高峰期触发,影响性能。

  • 在凌晨三点,当一张表因为突发流量而迅速膨胀时,谁在那里手动执行 VACUUM?


2

Autovacuum:从“手动动作”到“智能系统”

的升华

Autovacuum 的诞生,正是为了系统性地解决以上所有困境。

它不是 VACUUM 的替代品,而是围绕 VACUUM 构建的一套自动化、智能化的调度和执行系统。


它就像一个内置在 PostgreSQL 中的、不知疲倦的初级 DBA 团队:

解决了“时机之谜”


Autovacuum 持续在后台监控每一张表。

它使用基于比例(scale_factor)和固定值(threshold)的算法来判断一张表是否需要 VACUUM 或 ANALYZE。它自动回答了“何时执行”的问题。


解决了“规模之痛”


Autovacuum 拥有一个工作进程池(autovacuum_max_workers),可以并发地处理多张脏表。

更重要的是,它允许 DBA 在表级别覆盖全局设置,从而为高频表和静态表制定截然不同的精细化策略。


解决了“沉默的杀手”


Autovacuum 有一个专门的“反回卷”安全模式。

当它检测到整个数据库的 TXID 年龄接近危险阈值时,它会变得极具侵略性,强制对那些最老的、即使是“干净”的表执行 VACUUM,其唯一目的就是去冻结 TXID。

这个救命的安全网是手动策略几乎无法实现的。


解决了“人的弱点”


Autovacuum 是一个 24/7 运行的守护进程。

它还内置了 I/O 节流阀(cost_delay, cost_limit),确保其工作不会过度干扰正常的业务查询。


02

Autovacuum的调优 

autovacuum并非一个单一的进程。

它由一个启动器(launcher)进程和多个工作(worker)进程组成。 

启动器定期(由autovacuum_naptime控制,默认为1分钟)唤醒,检查哪些表需要进行维护,然后启动工作进程来执行实际的VACUUM和ANALYZE任务。

autovacuum的调优可以分为两大核心问题:“何时运行?” 和 “运行多快?”。

我们先来解决第一个问题。


1

调优核心一:决定“何时运行”——

触发机制详解与实验

autovacuum何时对一个表采取行动,主要由以下参数决定:


为了更直观地理解这些参数如何相互作用,让我们通过一个实验来观察。


实验准备


1)连接到您的PostgreSQL数据库。

2)为了能清晰地观察到autovacuum的每一次执行,我们修改配置,让所有autovacuum活动都被记录下来。

-- 超级用户执行,设置为记录所有自动 vacuum 操作(值为0
ALTER SYSTEM SET log_autovacuum_min_duration = 0;
-- 重新加载配置文件
SELECT pg_reload_conf();
-- 查看当前参数值
SHOW log_autovacuum_min_duration;


3)您需要有权限查看PostgreSQL的日志文件,具体位置取决于您的操作系统和安装方式。


场景一:小表(默认配置下的行为)


我们将创建一个小表,并观察默认配置如何轻松应对。


1)创建表并插入数据

CREATE TABLE small_test_table (id INT, data TEXT);
INSERT INTO small_test_table SELECT generate_series(11000), 'initial data';

我们现在有了一个1000行的表。


2)计算触发阈值

根据默认配置 (threshold=50, scale_factor=0.2),VACUUM的触发阈值为: 50 + 0.2 * 1000 = 250 个死元组。

3)制造死元组

我们通过UPDATE操作来制造死元组。

更新200行数据,使其不超过阈值。

UPDATE small_test_table SET data = 'updated data' WHERE id <= 200;


4)查询系统视图

可以验证autovacuum是否已经执行。

SELECT relname, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 'small_test_table';


结果示例

     relname      | n_dead_tup |        last_autovacuum        
------------------+------------+-------------------------------
 small_test_table |        200 | 
(1 row)

在autovacuum执行前,您会看到n_dead_tup(死元组数)变为200,并且last_autovacuum时间戳未被更新。


5)制造更多死元组

我们通过UPDATE操作来制造死元组,使其刚好超过阈值。

UPDATE small_test_table SET data = 'updated data' WHERE id <= 50;


6)等待并观察

现在,等待autovacuum启动器下一次唤醒(最多等待autovacuum_naptime,即1分钟)。


  • 查看日志

    您应该能在PostgreSQL的日志文件中看到类似下面的信息:

2025-10-26 06:52:41.473 CST [234585] LOG:  automatic vacuum of table "postgres.public.small_test_table": index scans: 0
        pages: 0 removed, 9 remain, 9 scanned (100.00% of total), 0 eagerly scanned
        tuples: 1 removed, 1000 remain, 0 are dead but not yet removable
        removable cutoff: 834, which was 0 XIDs old when operation ended
        frozen: 3 pages from table (33.33% of total) had 261 tuples frozen
        visibility map: 4 pages set all-visible, 3 pages set all-frozen (0 were all-visible)
        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: 13.611 MB/s
        buffer usage: 49 hits, 0 reads, 5 dirtied
        WAL usage: 10 records, 6 full page images, 36564 bytes, 0 buffers full
        system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s


  • 查询系统视图

    可以验证autovacuum是否已经执行。

SELECT relname, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 'small_test_table';


  • 结果示例

    relname      | n_dead_tup |        last_autovacuum        
------------------+------------+-------------------------------
 small_test_table |         0 | 2025-10-26 06:52:41.473318+08
(1 row)


在autovacuum执行后,您会看到n_dead_tup(死元组数)变为0,并且last_autovacuum时间戳被更新。



结论

对于小表,默认配置工作得很好,threshold和scale_factor共同作用,能在合理的时机触发清理。

场景二:大表(默认配置的问题与调优)


现在我们来模拟一个大表,并揭示为什么默认配置对其不适用。


1)创建表并插入数据:

CREATE TABLE large_test_table (id INT, data TEXT);
INSERT INTO large_test_table SELECT generate_series(11000000), 'initial data';

我们创建了一个100万行的大表。


2)计算默认触发阈值

50 + 0.2 * 1,000,000 = 200,050 个死元组。




问题显而易见

数据库需要累积超过20万个死元组(占整个表的20%)才会触发一次autovacuum。

在此期间,表已经显著膨胀,查询性能可能已经开始下降。


3)进行调优

我们不希望等待20万死元组的出现。

让我们为这个大表设置更合理的autovacuum参数。

一个常见的策略是完全依赖一个固定的阈值。

ALTER TABLE large_test_table SET (autovacuum_vacuum_scale_factor = 0, autovacuum_vacuum_threshold = 5000);


现在,VACUUM的触发阈值被固定为5000个死元组,这对于一个百万行级别的表来说是一个更及时、更合理的数字。


4)制造死元组并观察

我们更新5100行数据来超过新的阈值。

UPDATE large_test_table SET data = 'updated data' WHERE id <= 5100;


5)等待并观察

同样,等待最多1分钟。

您将在日志中看到autovacuum: VACUUM public.large_test_table的记录,并且通过查询pg_stat_user_tables可以确认死元组已被清理。


实验结论与最终建议


  • Autovacuum并非一刀切

    默认配置对小表友好,但对大表(通常是写入频繁的核心业务表)来说是不合适的,甚至是危险的。

  • 比例因子是陷阱

    autovacuum_vacuum_scale_factor是导致大表autovacuum不及时的主要原因。

  • 表级别调优是关键

    识别出系统中的大表和写入频繁的表,并使用ALTER TABLE为它们设置独立的、更激进的autovacuum策略,是保证数据库健康的关键一步。


最终设置建议总结


1)全局配置 (postgresql.conf)

可以保持相对保守的默认值,服务于数据库中的大量普通小表。

2)识别关键表

找出那些行数巨大(如超过50万行)或写入(UPDATE/DELETE)操作非常频繁的表。

3)应用表级别配置

对于这些关键表,执行以下操作:

-- 方案A:使用更小的比例因子和固定的基础阈值
ALTER TABLE your_large_table SET (autovacuum_vacuum_scale_factor = 0.02, autovacuum_vacuum_threshold = 1000);

-- 方案B:完全使用固定的阈值(更可预测)
ALTER TABLE your_very_busy_table SET (autovacuum_vacuum_scale_factor = 0, autovacuum_vacuum_threshold = 10000);

threshold的具体值应根据表的写入频率和可接受的膨胀程度来决定。

10000是一个相对安全的起点。


2

调优核心二:决定“运行多快”——

速率控制详解

解决了“何时运行”后,我们来看autovacuum如何控制自己的工作速度,以避免对系统造成过大冲击。

这就是基于成本的延迟(Cost-Based Vacuum Delay)机制。

这套系统像一个记账本。

autovacuum有一个“预算”,每执行一个操作(如读取一个数据块)就会“花钱”,当预算用完时,就必须“强制休息”来攒钱。


工作流程


1)autovacuum工作进程开始处理一个表,其内部成本计数器清零。

2)它扫描数据块(page,通常为8KB)。

3)每扫描一个块,就根据上述规则累加成本:

  • 如果块在内存中

    成本 +1 (vacuum_cost_page_hit)。

  • 如果块在磁盘中

    成本 +2 (vacuum_cost_page_miss)。

  • 如果VACUUM需要修改这个块(例如,移除死元组并更新可见性映射),则额外增加成本 +20 (vacuum_cost_page_dirty)

4)在处理完每个块后,检查累计成本是否超过 autovacuum_vacuum_cost_limit。

5)如果超过,进程将暂停 autovacuum_vacuum_cost_delay 毫秒,然后将成本计数器清零,继续从上一个位置工作。





速率的本质

VACUUM的整体速率,实际上就是 (每个工作周期处理的数据量) (每个工作周期花费的时间)。

而暂停时间 (cost_delay) 往往是决定速率的主要因素。


不同参数配置下的VACUUM1秒内清理速率估算


为了直观地展示参数的影响,我们来模拟几种场景。

假设我们的数据块大小为8KB。

默认情况下,autovacuum_vacuum_cost_delay=2ms,我们可以近似估算出vacuum进程1秒内有500个工作周期。

1s=1000ms,1秒可以调用vacuum进程1000/2=500次。


场景一:绝对理想情况(所有块都在缓存,且都被清理)


  • 单个块的成本 = hit(1) + dirty(20) = 21

  • 每个工作周期能清理的块数 = 200 (limit) 21 (cost) ≈ 9.52,取整为 9个块。

  • 1秒内理论上能清理的总块数 = 500 (周期数) * 9 (块/周期) = 4500 个块。

  • 转换成吞吐率 = (4500 * 8 KB) 1024 = 35.16 MB/s


场景二:最坏情况(所有块都不在缓存,且都被清理)

  • 单个块的成本 = miss(2) + dirty(20) = 22

  • 每个工作周期能清理的块数 = 200 (limit) 22(cost) ≈ 9.09,取整为 9个块。

  • 1秒内理论上能清理的总块数 = 500 (周期数) * 9 (块/周期) = 4500 个块。

  • 转换成吞吐率 = (4500 * 8 KB) 1024 = 35.16 MB/s


场景三:只读扫描,无清理(例如,一个干净的表被VACUUM扫描)

  • 场景3A (全在缓存)

    成本为1,周期块数200,总块数 500 * 200 = 100,000,吞吐率 781.25 MB/s。

  • 场景3B (全在磁盘)

    成本为2,周期块数100,总块数 500 * 100 = 500,000,吞吐率 380.62MB/s。


总结



速率控制的调优建议


  • 检查cost_delay

    如果你的PostgreSQL版本低于12且使用SSD,首要任务是检查autovacuum_vacuum_cost_delay是否合理,若设置过大,建议调整为2ms。

  • 按需提高cost_limit

    如果autovacuum总是跟不上,可以适度提高autovacuum_vacuum_cost_limit(例如从200提高到500或1000),让它在每次暂停前能做更多的工作。

  • 手动VACUUM加速

    在维护窗口执行手动VACUUM时,可以通过SET vacuum_cost_delay = 0;临时禁用速率限制,使其全速运行。


03

监控Autovacuum的健康状况 

调优并非一劳永逸,持续的监控至关重要。


关键监控手段


1)查询系统视图

  • pg_stat_activity

    可以查看当前正在运行的autovacuum工作进程。

  • pg_stat_user_tables

    这是最重要的监控视图。

    其中的n_dead_tup(死元组数)、last_autovacuum和last_autoanalyze(上次执行时间)等列,能清晰地反映autovacuum的工作情况。


一个有用的查询,用于找出可能需要关注的表(死元组多且长时间未VACUUM):

SELECT
    relname,
    n_live_tup,
    n_dead_tup,
    last_autovacuum,
    last_autoanalyze
FROM
    pg_stat_user_tables
WHERE
    n_dead_tup > 10000 -- 根据实际情况调整
ORDER BY
    n_dead_tup DESC;


2)日志分析

设置log_autovacuum_min_duration参数(例如250ms或1s),可以将运行时间超过指定阈值的autovacuum活动记录到日志中。

这有助于发现那些VACUUM缓慢、存在问题的表。


写在最后


Autovacuum 不是可有可无的后台线程,而是 PostgreSQL 自愈系统的核心。

理解它,你才能真正掌控数据库的生命节奏;调优它,你才能让系统在负载中依旧平稳。

当下次你再看到“autovacuum”出现在日志里,请记得——那不是噪音,而是守护。

实际上,多数数据库性能问题的根源,常常与操作系统层面的资源配置、性能瓶颈等因素密切相关。

如果你还想系统补齐 Linux 的实战短板,推荐看看刘峰老师的 Linux 系列课程,循序渐进,从命令行到运维部署,一步到位。


作者介绍

大家好,我是刘峰,安丫科技创始人 & 数据库技术高级讲师,专注于 PostgreSQL、国产数据库运维与迁移、数据库性能优化 等方向。

作为 PG中国分会官方授权讲师、PostgreSQL ACE 讲师认证专家,我长期活跃在一线项目实战中,拥有 10年以上大型数据库管理与优化经验,曾深度参与电信、金融、政务等多个行业的数据库性能调优与迁移项目。

欢迎关注我,一起深入探索数据库的无限可能,技术交流不设限!

📌 觉得有收获的话,记得点赞、收藏、转发支持一下哦,别忘了关注我获取更多数据库干货~


安呀智数据坊|我们能做什么

无论你是业务系统的技术负责人,还是数据部门的第一响应人,我们都能为你提供可靠的支持:

  • 数据库类型支持

    Oracle / MySQL / PostgreSQL / SQL Server 等主流数据库

  • 核心服务内容

    性能优化 / 故障处理 / 数据迁移 / 备份恢复 / 版本升级 / 补丁管理

  • 系统性支持

    深度巡检 / 高可用架构设计 / 应用层兼容评估 / 运维工具集成

  • 专项能力补充

    定制课程培训 / 甲方团队辅导 / 复杂问题协作排查 / 紧急救援支持

📮 如果你有一张删不掉的表、一个跑不动的查询,或者一场说不清的升级风险,欢迎来找我们聊聊。


END

关键词回复(可见相应文章):

oracle、mysql、pg、postgresql、sql、性能优化、故障处理、数据迁移、备份恢复、版本升级、补丁管理、深度巡检、解决方案、架构设计......

添好友1对1咨询

\ | /

动动你的手指

【安呀智数据坊】加个星标吧~

这样你就不会丢下我啦~

记得加星标呀!

文章转载自安呀智数据坊,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论