跳至内容
28.2 autovacuum 的触发与资源

28.2 autovacuum 的触发与资源

autovacuum 不是“每隔一分钟把所有表 vacuum 一遍”。

它是一套按数据库调度、按表判断资格、按 worker 执行、按 cost budget 限速的后台系统:

launcher
  -> choose database
      -> worker examines relations
          -> trigger decision
              -> VACUUM / ANALYZE / both
                  -> resource and lock interaction

排障必须分开问:

  1. 表有没有达到触发条件?
  2. 有 worker 能接活吗?
  3. worker 启动后在做什么?
  4. 为什么扫完仍留下旧版本?

“把 scale factor 调小”最多回答第一个问题的一部分。

28.2.1 阈值、比例、插入触发与表级覆盖

UPDATE/DELETE 触发公式

PostgreSQL 18 对普通 vacuum 的变化量阈值为:

$$ T_{\text{vacuum}} = \min\left( T_{\max}, T_{\text{base}} + f_{\text{vacuum}} \times N_{\text{table}} \right) $$

对应:

T_base   autovacuum_vacuum_threshold
f        autovacuum_vacuum_scale_factor
N_table  pg_class.reltuples
T_max    autovacuum_vacuum_max_threshold

当自上次 vacuum 以来被 UPDATE/DELETE 变旧的 tuple 估计数超过这个阈值,表取得 vacuum 资格。

PostgreSQL 18 引入/使用 autovacuum_vacuum_max_threshold 作为上限:

base=500
scale=0.08
max=100,000,000
reltuples=1,000,000,000

base + scale * rows = 80,000,500
effective threshold = 80,000,500

若表为 10 billion rows:

base + scale * rows = 800,000,500
effective threshold = 100,000,000

版本低于 PostgreSQL 18 时,不要照抄这个公式中的 max 项;先查对应 major 文档和 pg_settings 是否存在。

INSERT-only 也需要 vacuum

只插不删的表没有 dead tuple,却仍需要:

  • 更新 visibility map;
  • 让 index-only scan 受益;
  • 冻结旧 XID;
  • 降低以后 aggressive vacuum 的工作。

插入触发公式为:

$$ T_{\text{insert}} = T_{\text{insert-base}} + f_{\text{insert}} \times N_{\text{table}} \times \left(1 - \frac{\text{relallfrozen}}{\text{relpages}}\right) $$

对应:

autovacuum_vacuum_insert_threshold
autovacuum_vacuum_insert_scale_factor
pg_class.reltuples
unfrozen page fraction

这不是简单的:

1000 + 0.2 * rows

它还乘以“未冻结页面比例”。当表逐步 all-frozen,insert-based 触发的 scale 部分也会 变化。

ANALYZE 有自己的阈值

$$ T_{\text{analyze}} = T_{\text{analyze-base}} + f_{\text{analyze}} \times N_{\text{table}} $$

变化量包括 insert/update/delete。vacuum 和 analyze 可能:

only vacuum
only analyze
vacuum then analyze

不要把 last_autovacuumlast_autoanalyze

freeze 资格优先于普通变化量

relfrozenxid 年龄超过 autovacuum_freeze_max_age,系统会强制 vacuum,即使:

  • 普通 autovacuum GUC 为 off;
  • 表级 autovacuum_enabled=false
  • dead tuple 没达到普通阈值。

同理,multixact 有独立的:

relminmxid
autovacuum_multixact_freeze_max_age

因此“关 autovacuum”既不安全,也不能保证后台永远不出现 worker;防回卷维护是正确性 机制,不是可选性能功能。

reltuples 和 change count 都不是精确实时值

触发器依赖:

  • pg_class.reltuples 估计;
  • cumulative statistics 的变化计数;
  • 最近 vacuum/analyze 更新;
  • stats flush 的最终一致性。

边界附近出现几秒或一轮调度差异是正常的。排障时先查看实际输入:

WITH p AS (
  SELECT
    current_setting('autovacuum_vacuum_threshold')::numeric AS base,
    current_setting('autovacuum_vacuum_scale_factor')::numeric AS scale,
    current_setting('autovacuum_vacuum_max_threshold')::numeric AS max_t
)
SELECT
  s.schemaname,
  s.relname,
  c.reltuples,
  s.n_dead_tup,
  least(
    p.max_t,
    p.base + p.scale * c.reltuples
  ) AS estimated_trigger,
  s.last_autovacuum
FROM pg_stat_user_tables AS s
JOIN pg_class AS c ON c.oid = s.relid
CROSS JOIN p
ORDER BY
  s.n_dead_tup
  / nullif(least(p.max_t, p.base + p.scale * c.reltuples), 0)
  DESC NULLS LAST;

这段查询仍没处理表级覆盖,生产版需要把 reloptions 合并进来。

表级覆盖:治疗特殊表,不复制全局配置

高 churn 大表、append-only 表和小型 catalog-like 表,触发策略可能不同:

ALTER TABLE app.hot_orders SET (
  autovacuum_vacuum_threshold = 1000,
  autovacuum_vacuum_scale_factor = 0.01,
  autovacuum_analyze_scale_factor = 0.02
);

查看:

SELECT
  n.nspname,
  c.relname,
  c.reloptions
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.reloptions IS NOT NULL
ORDER BY 1, 2;

恢复继承全局值:

ALTER TABLE app.hot_orders RESET (
  autovacuum_vacuum_threshold,
  autovacuum_vacuum_scale_factor,
  autovacuum_analyze_scale_factor
);

表级覆盖适合:

one relation demonstrably misses its maintenance window
its write pattern differs materially from cluster norm
change rate and resource budget are measured
override is in schema/IaC and reviewed

不适合:

copy every global GUC to every table
set autovacuum_enabled=false as tuning
hide a long-transaction blocker
raise freeze age to silence alerts

第 28 章实验为了让手工 vacuum 不被后台抢跑,在一次性夹具表上临时设置 autovacuum_enabled=false;数据库清理后该设置随表消失。公开结果明确将其列为 fixture_table_autovacuum_enabled=false,不是生产建议。

计算后还要看时间

一个表达到阈值只表示“有资格”,不表示立刻开始。launcher 要轮询数据库,worker 要 可用,其他 relation 可能排在前面。

评估维护能力,应比较:

$$ \text{dead tuple arrival rate} \quad \text{vs} \quad \text{vacuum reclamation rate} $$

若每小时产生 500 million obsolete tuples,而可用 worker 每小时只能处理 300 million, 调低触发阈值只会更早开始积压,不能解决服务率不足。

28.2.2 worker、cost delay、I/O 与业务竞争

launcher、worker slot 和 worker 上限

PostgreSQL 18 需要同时理解:

autovacuum_worker_slots
autovacuum_max_workers
autovacuum_naptime
number of databases

autovacuum_worker_slots 在启动时为 worker 预留 backend slot; autovacuum_max_workers 是可同时运行 worker 的上限。把后者设得高于前者没有效果。

launcher 尝试把工作分散到各数据库;有 $N$ 个数据库时,会试图约每 autovacuum_naptime / N 启动一个 worker。它不是每个数据库独立一套无限 worker。

查询:

SELECT name, setting, unit, context, source, pending_restart
FROM pg_settings
WHERE name IN (
  'autovacuum',
  'autovacuum_worker_slots',
  'autovacuum_max_workers',
  'autovacuum_naptime'
)
ORDER BY name;

worker 数只是并发上限

增加 worker 可能:

  • 减少多个数据库/表的排队;
  • 让更多表并发扫描;
  • 同时增加 I/O、CPU、buffer churn;
  • 放大 autovacuum_work_mem 总预算;
  • 与 foreground query、checkpoint、backup、replay 竞争。

若瓶颈是单块磁盘,三个 worker 已把设备打满,再加三个只会提高 queue depth 和业务 tail latency。

先看:

eligible tables waiting
active autovacuum workers
worker phase
disk latency / queue
CPU busy / run queue
buffer and cache effect
business p95/p99
replica and archive lag

memory 按 worker 放大

autovacuum_work_mem 控制每个 autovacuum worker 可用的 maintenance memory;设为 -1 时回退到 maintenance_work_mem。粗略预算:

$$ M_{\text{auto}} \le W_{\text{active}} \times M_{\text{per-worker}} $$

这仍是上界近似,不是每个 worker 永远一次性占满。它用于预留最坏并发,而不是预测 RSS 精确值。

内存主要影响:

  • 收集 dead item identifiers;
  • index vacuum cycle 频率;
  • maintenance 内部结构。

它不会让一个被旧 snapshot 保留的 tuple 突然可删。

PostgreSQL 18 的 pg_stat_progress_vacuum 暴露:

max_dead_tuple_bytes
dead_tuple_bytes
num_dead_item_ids
index_vacuum_count

可以判断是否因为维护内存限制而反复做 index vacuum cycle。

cost delay 是 I/O 影响控制,不是带宽保证

vacuum 给页面操作累计抽象 cost:

page hit
page miss
page dirty

达到 vacuum_cost_limit 后,sleep vacuum_cost_delay 再继续。

autovacuum 对应:

autovacuum_vacuum_cost_delay
autovacuum_vacuum_cost_limit

若 autovacuum cost limit 非 -1,PostgreSQL 会在并行 worker 之间按比例分配,使各 worker limit 合计不超过该值。这意味着:

worker 变多,不等于每个 worker 都拿到完整 limit。

另外:

  • 手工 VACUUM 的 cost delay 默认关闭,除非显式设置非零;
  • 持有关键锁的操作段不会照常 sleep;
  • failsafe 触发后会停止 cost delay,并跳过非必要工作来优先防回卷;
  • cost unit 不是 IOPS 或 MB/s,必须用 OS/Pigsty I/O 指标校准。

正式实验为了可靠抓取进度,只在 vacuum session 设置:

SET vacuum_cost_delay = '10ms';
SET vacuum_cost_limit = 20;
SET track_cost_delay_timing = on;

session 结束即回退;没有修改集群配置。10 ms 是教学限速,不是推荐生产值。官方文档 指出正常配置通常应使用很小的 delay,大延迟并不理想。

I/O 不是唯一竞争

vacuum 还会:

read heap
dirty heap / VM / FSM
read and update indexes
generate WAL for maintenance changes
use CPU to evaluate tuple visibility
acquire relation/page locks
evict useful shared/OS cache pages

所以 “iowait 不高” 不能证明 vacuum 无影响。可能:

  • 数据在 cache,竞争表现为 CPU 和 buffer churn;
  • device 很快,竞争表现为 foreground tail;
  • cloud storage queue 未映射为 host iowait;
  • cost delay 让 worker 大量 sleep;
  • checkpoint/backup 与 vacuum 交织。

维护优先级不是一刀切

可把对象分三层:

P0 correctness:
  XID/MXID danger
  suspected corruption

P1 service health:
  dead tuple backlog accelerating
  index cleanup not completing
  table growth threatens disk/SLO

P2 efficiency:
  moderate bloat
  stale statistics
  low HOT ratio

P0 不能为了降低业务 I/O 无限限速;P2 不应在业务峰值争抢资源。

28.2.3 进度、阻塞与“为什么没清掉”

先确认 worker 身份

SELECT
  pid,
  datname,
  usename,
  backend_type,
  application_name,
  state,
  wait_event_type,
  wait_event,
  xact_start,
  query_start,
  query
FROM pg_stat_activity
WHERE backend_type = 'autovacuum worker'
   OR query LIKE 'autovacuum:%'
ORDER BY query_start;

防回卷 worker 的 query 文本会带 (to prevent wraparound)。它与普通 autovacuum 的 取消策略不同:冲突锁通常可中断普通 autovacuum,但防回卷 worker 不会被自动中断。

读取原生 progress

SELECT
  p.pid,
  p.datname,
  p.relid::regclass AS relation,
  p.phase,
  p.heap_blks_total,
  p.heap_blks_scanned,
  p.heap_blks_vacuumed,
  p.index_vacuum_count,
  p.dead_tuple_bytes,
  p.num_dead_item_ids,
  p.indexes_total,
  p.indexes_processed,
  p.delay_time
FROM pg_stat_progress_vacuum AS p
ORDER BY p.pid;

PostgreSQL 18 的主要 phase:

initializing
scanning heap
vacuuming indexes
vacuuming heap
cleaning up indexes
truncating heap
performing final cleanup

解释时注意:

  • heap_blks_total 是开始扫描时的规模;
  • VM 跳过的块仍会计入 scanned 的推进;
  • heap_blks_vacuumed 可能跳跃;
  • index 可能有多个 cycle;
  • truncation、锁等待和 index cleanup 的耗时不由 heap 扫描百分比线性预测。

所以:

heap_blks_scanned / heap_blks_total

是 scan progress,不是可靠 ETA。

“没清掉”的决策树

Did VACUUM run?
├─ no
│  ├─ below threshold
│  ├─ worker unavailable
│  ├─ autovacuum/table option disabled
│  ├─ statistics not updating
│  └─ permissions/manual command skipped relation
└─ yes
   ├─ old snapshot still needs tuples
   ├─ replication slot xmin/catalog_xmin retains them
   ├─ prepared transaction retains horizon/locks
   ├─ index cleanup skipped/deferred
   ├─ pages skipped to avoid waits
   ├─ only estimate is stale
   ├─ space became reusable but file did not shrink
   └─ new churn arrived as fast as cleanup

blocker inventory

SELECT
  pid,
  usename,
  application_name,
  state,
  xact_start,
  state_change,
  backend_xid,
  backend_xmin,
  age(backend_xid)  AS xid_age,
  age(backend_xmin) AS xmin_age,
  wait_event_type,
  wait_event
FROM pg_stat_activity
WHERE backend_xid IS NOT NULL
   OR backend_xmin IS NOT NULL
   OR state LIKE 'idle in transaction%'
ORDER BY age(backend_xmin) DESC NULLS LAST;

再查:

SELECT
  slot_name,
  slot_type,
  database,
  active,
  age(xmin) AS xmin_age,
  age(catalog_xmin) AS catalog_xmin_age,
  restart_lsn,
  wal_status,
  inactive_since,
  invalidation_reason
FROM pg_replication_slots;

SELECT
  transaction,
  age(transaction) AS xid_age,
  gid,
  prepared,
  owner,
  database
FROM pg_prepared_xacts
ORDER BY age(transaction) DESC;

不要在第一条查询里直接拼 pg_terminate_backend。先确认:

owner
application
business transaction semantics
retry behavior
prepared transaction coordinator
replication consumer
HA/failover impact

然后才能决定 cancel、terminate、commit、rollback 或 drop slot。

累计结果

SELECT
  relid::regclass,
  n_live_tup,
  n_dead_tup,
  last_vacuum,
  last_autovacuum,
  vacuum_count,
  autovacuum_count,
  total_vacuum_time,
  total_autovacuum_time
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;

PostgreSQL 18 增加/提供 vacuum/analyze 累计耗时列;部署跨版本查询时要先检查列存在。 累计 view 会 reset,必须联合 stats_reset 和 Pigsty 时序数据,不要把 reset 后的 “低计数”解释成改善。

Pigsty:历史趋势与原生瞬时事实互补

Pigsty 的监控栈把 PostgreSQL、pgBouncer、Patroni、主机和日志放到同一组 cls/ins/ip 标签下。对维护问题,常用:

页面看什么
PGSQL Tables / Tabledead/live、scan、vacuum、relation trend
PGCAT Table当前 catalog、size、bloat 类诊断
PGSQL PersistXID、WAL、checkpoint、archive、持久性
PGSQL Activity / Sessionbackend、wait、长事务
PGSQL Replicationslot、replica、replay/retention
PGCAT Locksblocker/waiter
PGLOGautovacuum verbose、warning、cancel/failsafe
NODE Instancedisk latency、queue、space、CPU、memory

Dashboard 回答:

when did it start?
is it accelerating?
which instance/table changed?
what else happened at the same time?

原生 SQL 回答:

which PID and phase now?
which exact xmin/slot/prepared xact retains horizon?
which reloption and effective GUC applies?

两者必须互证。Grafana panel 不是另一个数据库真相层。

处置顺序

1. classify correctness vs service vs efficiency
2. verify actual trigger inputs and table overrides
3. locate active/queued workers and progress
4. inventory holders
5. compare cleanup rate with churn rate
6. check I/O/CPU/memory/WAL/replica side effects
7. choose smallest reversible intervention
8. validate dead/reusable/age outcome
9. record desired state and rollback

跳过第 4 步直接“手工再 vacuum 一次”,通常只会重复同一失败。

本节检查清单

effective update/delete threshold
effective insert threshold
analyze threshold
freeze/MXID age
table reloptions
eligible backlog
worker slots / max workers
per-worker and total memory budget
cost limit distribution
progress phase and cycle
old snapshot/slot/2PC holders
foreground tail and device pressure
cleanup rate vs churn rate

延伸阅读


上一节:死元组与可见性 · 返回本章目录 · 下一节:冻结、XID 与保留者 · 查看全书目录 · 查看索引中心

最后更新于