PostgreSQL 19 beta4 给 autovacuum 加上了并行索引清理。有人搭了一张六索引、40万行、80万死元组的表,把并行开关从 0 调到 4,跑完三轮,盯着墙上时钟看结果。
![]()
结论有点冷:1.31 秒串行,1.25 秒并行,约 4%,落在本来就存在的运行间波动里。换句话说,这个数字在噪声范围内。
但同一份日志里还有另一个数字:autovacuum worker 自身消耗的 CPU 时间,降了大约 30%。六个索引里有五个的扫描工作被交给了启动的 worker,不再由 leader 一个人扛。
两个数字,一个几乎没动,一个动了三成。差别在于你盯的是哪块表。
并行的是索引,不是堆
这次测试的表,瓶颈不在索引清理阶段。官方埋点显示,每一轮——无论串行还是并行——都写下了 101,000+ 条 WAL 记录和 70,000+ 个脏缓冲区。autovacuum 并不并行化堆扫描。
所以如果你的表卡在堆扫描或者它产生的 WAL 上,把并行 worker 调高不会让表更快变干净。如果卡在索引清理上,结果可能完全不同——这张表不是那种情况。
并行不是白拿的。同一行日志会报告死元组(TID)存储:串行用 1.45MB,4 个 worker 并行用 3.00MB,同样 80 万死元组,翻了一倍。原因是每个启动的 worker 都要分一份记账空间。
80 万行的 vacuum 上这不算什么。但换成多 TB 的表、autovacuum_work_mem 本来就紧张、还有多个 autovacuum worker 各自拉起并行助手,这个乘法才是要预算的东西,不是 CPU。
那个你以为能管住它的参数,管不住
PostgreSQL 从 11 版就有 max_parallel_maintenance_workers,控制手动 VACUUM (PARALLEL) 或 CREATE INDEX 能用几个 worker。很容易假设它同样能限制 autovacuum 的新并行 worker。
实测:把它设成 0,autovacuum_max_parallel_workers 设成 4,max_parallel_workers 设成 8,autovacuum 依然在日志里写下 parallel workers: index vacuum: 4 planned, 4 launched in total。
真正的闸门是全局池。按 PostgreSQL 19 文档对 autovacuum_max_parallel_workers 的说明,实际并行 worker 数量还会被 max_parallel_workers 进一步限制。把后者设成 0,日志变成 4 planned, 0 launched,和文档一致。那一页里根本没提 max_parallel_maintenance_workers。
要在集群层面给并行 worker 做预算,该拧的是全局池那个旋钮。
打分系统确实在按死元组排序
beta4 的发布说明提到"新 autovacuum 打分系统的修复",但没说它日常干什么。于是建了六张表,死元组数分别是 100、500、5,000、40,000、150,000、500,000,按这个顺序创建,然后在同一个事务里给六张表都打开 autovacuum。
单个 autovacuum worker 按日志时间戳接手的顺序是:
- p_massive(50 万死元组)05:28:13.989
- p_huge(15 万死元组)05:28:14.081
- p_big(4 万死元组)05:28:14.122
- p_med(5,000 死元组)05:28:14.141
- p_small(500 死元组)05:28:14.147
- p_tiny(100 死元组)05:28:14.149
严格按死元组数降序,正好是创建顺序的倒序。至少在这一个信号上,打分系统把最紧急的表排在了最前面。
并发下同时踩了两个坑
把全局 max_parallel_workers 压到 4,然后另开一个会话跑一条故意很慢的并行查询——WHERE 子句里塞 pg_sleep(),三个 worker,扫一张 150 万行的表——占掉四个槽位里的三个。在这个会话运行期间,重新把 wide_bloat 弄脏,打开 autovacuum。
连续三轮 autovacuum,每轮约 40 秒,日志都是 parallel workers: index cleanup: 4 planned, 0 launched。没有空闲槽位,正确地退化成零,而不是报错。
但同一批日志行里更有用的发现是:tuples: 0 removed, 1200000 remain, 800000 are dead but not yet removable。那个长会话不只是占着并行 worker,它还持有一个快照,把那 80 万死元组压在可清理水位线之下。会话开着的整整 40 秒里,什么都没被删掉,并行与否都一样。
会话一被杀掉,下一个周期立刻记下 4 planned, 4 launched,大约一秒清完 80 万死元组。
教训其实不在并行 autovacuum 上:数据库里任何一个地方留着长事务,都会卡住 vacuum,给多少 worker 都没用。并行 autovacuum 改变不了这一点,它只是意味着你配好的 worker 也可能在最糟糕的时刻闲着,原因还是老原因。
3am 想知道的那件事
PostgreSQL 19 的 pg_stat_progress_vacuum 会报告实时阶段和索引进度。在一张三索引、150 万行的 autovacuum 运行中每 300ms 轮询一次,抓到这样的序列:
- 05:30:13.949 | scanning heap | heap_blks_scanned=137335/142858 | indexes_processed=0/0
- 05:30:14.370 | vacuuming indexes | heap_blks_scanned=142858/142858 | indexes_processed=2/4
- 05:30:14.791 | vacuuming heap | heap_blks_scanned=142858/142858 | indexes_processed=0/0
indexes_total 和 indexes_processed 足够判断一次慢 vacuum 是卡在扫堆还是卡在过索引。这正是凌晨三点真正想回答的问题。
路上踩的两个坑
第一次测试看起来像个 bug。用 ALTER SYSTEM 把 max_parallel_maintenance_workers 设成 0,调 pg_reload_conf(),从 pg_file_settings 拿到 applied: t,然后 SHOW max_parallel_maintenance_workers 还是 8。花了十分钟确信这个设置根本没生效,才想起来:容器是用 docker run 命令行上的 -c max_parallel_maintenance_workers=8 启动的。这样设的值在服务器运行期间是固定的。ALTER SYSTEM 照样写进 postgresql.auto.conf,文件也说应用了,但没有任何东西会覆盖命令行参数把它读回来。只能杀掉容器,不带那个 flag 重新起一个。
第二个更小:第一次争用测试用的全局 max_parallel_workers 是 8,竞争查询只占 3 个 worker,留给 autovacuum 请求的 4 个还有 5 个空位——空间充足,什么都没争起来,差点把"没有效果"当成结论写下来。把池子压到总共 4 个,才真正逼出两个请求的竞争。
该拿它怎么办
如果 autovacuum worker 在宽表上 CPU 饥饿,调高 autovacuum_max_parallel_workers 对这项特定成本是真实有效的杠杆,而且它默认关闭,升级时不会有人意外中招。
但如果瓶颈是堆扫描或它写的 WAL,它就不是让表更快变干净的杠杆。先量自己的表再假设它有用——pg_stat_progress_vacuum 的 phase 列会告诉你属于哪一种。要预算的是全局 max_parallel_workers 池,不是 max_parallel_maintenance_workers,因为真正卡住这件事的是前者。而任何地方留着一个长事务,照样会饿死一张膨胀的表,跟你给不给 autovacuum worker 无关。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.