Postgres 监控里 dead tuple 数量一直往上爬,最大那张表两周内磁盘体积翻了三倍,行数却几乎没动。你手动跑了一次 VACUUM ANALYZE,几秒就返回了,啥也没抱怨。autovacuum 日志也说跑过了。但 pg_stat_user_tables.n_dead_tup 还是涨,那张表的 p99 也从 40 ms 一路爬到 900 ms。
锅基本不在 autovacuum 自己身上。多半是集群里某个地方有一条卡住的事务,把全局 xmin horizon 拽住,导致 vacuum 即使看起来跑成功了,实际啥行都回收不到。
常见原因
按命中率从高到低排。
1. 有个 idle in transaction 的连接挂了几个小时
某个 worker 跑了 BEGIN、做了一个查询,然后就去等外部 API 了。连接还活着,事务一直没 commit。
怎么发现:跑 SELECT pid, state, xact_start, now() - xact_start AS age, query FROM pg_stat_activity WHERE state = 'idle in transaction' ORDER BY xact_start; 任何 age 超过几分钟的就是嫌疑人。
2. 一个没人消费的 replication slot
之前给 CDC pipeline 建过一个 logical replication slot,那个 pipeline 挂了 / 被删了 / 落后太多。slot 一直把 catalog_xmin 往后拽,vacuum 没法清掉比 slot 更新的行。
怎么发现:SELECT slot_name, active, restart_lsn, confirmed_flush_lsn, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS lag FROM pg_replication_slots; 看到 active = false、lag 还在涨的那个就是。
3. 有个跑了几小时的分析查询
BI 工具或者 pg_dump 开了一个 REPEATABLE READ 的快照在导数据。它没结束之前,所有该快照能看到的行都不能被清。
怎么发现:在 pg_stat_activity 里找 state = 'active' 又跑了很久的 SELECT,看它的 backend_xmin 是不是特别小。
4. 2PC 留下来的 prepared transaction
一个 two-phase commit 走到 prepared 状态,但没人调 COMMIT PREPARED 或 ROLLBACK PREPARED。这东西很顽固,重连、重启都不会消失。
怎么发现:SELECT * FROM pg_prepared_xacts; 里任何一行基本都是坏的。
5. 某个 standby 开了 hot_standby_feedback,又在跑慢查询
某个 replica 在跑长查询,把自己的 xmin 反向喂给 primary。primary 的 vacuum horizon 就被 replica 那边的活动拖住了。
怎么发现:在 primary 上看 SELECT application_name, backend_xmin FROM pg_stat_replication;,对比 txid_current()。如果 backend_xmin 比 current 落后很多,那就是 replica 在拽。
6. autovacuum 一直在跑,但一直被 cancel
锁竞争重的表上,autovacuum 一启动就让位给冲突锁。日志里看着像跑过,实际零工作量完成。
怎么发现:对比 pg_stat_user_tables.autovacuum_count 和 last_autovacuum。如果 count 一直涨但 n_dead_tup 不掉,就是 vacuum 被反复 cancel 了。Postgres log 里会有 canceling autovacuum task 这种消息。
7. autovacuum_freeze_max_age 触发了 anti-wraparound vacuum,而且卡住了
集群快要到 transaction ID wraparound 的时候,autovacuum 会拉起 aggressive freeze worker,这种是不能被 cancel 的。一旦它被堵,整个集群就一起停摆。
怎么发现:SELECT datname, age(datfrozenxid) FROM pg_database; 超过 2 亿就已经在危险区了。
最短修复路径
第 1 步:先找出最老的 xmin 持有者
这条查询最有用,先跑这个。
SELECT 'idle_in_tx' AS source, pid::text, xact_start::text, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
UNION ALL
SELECT 'long_query', pid::text, xact_start::text, query
FROM pg_stat_activity
WHERE state = 'active' AND xact_start < now() - interval '5 min'
UNION ALL
SELECT 'prepared_xact', gid, prepared::text, '2PC'
FROM pg_prepared_xacts
UNION ALL
SELECT 'repl_slot', slot_name, xmin::text, 'replication'
FROM pg_replication_slots
WHERE xmin IS NOT NULL
ORDER BY 3;
时间最早的那条就是元凶。
第 2 步:干掉或解除它
idle in transaction 的连接,先问下 owner,确认可以就 terminate。
SELECT pg_terminate_backend(12345);
孤儿 replication slot:
SELECT pg_drop_replication_slot('dead_cdc_slot');
prepared transaction:
ROLLBACK PREPARED 'transaction-gid-here';
第 3 步:确认 xmin horizon 真的往前走了
SELECT now(), txid_current(), txid_snapshot_xmin(txid_current_snapshot());
txid_snapshot_xmin 现在应该非常接近 txid_current。如果还差很远,那说明还有第二个 holder,回到第 1 步再扫一遍。
第 4 步:手动给胖表跑一次激进 vacuum
horizon 解开之后,强制跑一次能真的回收 page 的 vacuum。
VACUUM (VERBOSE, ANALYZE) public.orders;
-- 如果能接受短暂排他锁,并且想把磁盘空间拿回来:
VACUUM (FULL, VERBOSE) public.orders;
VACUUM FULL 会重写整张表并拿 ACCESS EXCLUSIVE 锁——要么在维护窗口做,要么用 pg_repack 走在线重建。
第 5 步:集群级别压低 idle_in_transaction 超时
ALTER SYSTEM SET idle_in_transaction_session_timeout = '5min';
ALTER SYSTEM SET statement_timeout = '30min'; -- 需要的话按 session 设
SELECT pg_reload_conf();
这样忘记关的事务会被自动 kill,不用等人来救。
第 6 步:加一个 autovacuum 监控查询
-- 最老事务超过 30 分钟就告警
SELECT max(extract(epoch FROM now() - xact_start)) AS oldest_tx_seconds
FROM pg_stat_activity
WHERE xact_start IS NOT NULL;
接到 Prometheus 或者你用的监控系统,超过 1800 就 page。
第 7 步:给热表单独调 autovacuum 参数
默认的 autovacuum 阈值对写量大的表偏保守。5000 万行、一天 100 万次 update 的表,把 scale factor 调低。
ALTER TABLE public.orders SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_vacuum_cost_limit = 2000,
autovacuum_naptime = 10
);
这种情况不怪你
托管服务(RDS、Cloud SQL、Aurora)有时候会跑自己内部的长事务做 backup 或者大版本升级。这种会短暂占住 xmin,你也 kill 不掉。如果 pg_stat_activity 里唯一的老事务 owner 是 rdsadmin 或 cloudsqladmin,那就等它跑完。
容易被误诊成
磁盘慢。症状一模一样:查询变慢、IOPS 飙、监控看着像吃满了。然后大家就想换更大的实例。真正的问题是索引里全是 Postgres 没法清掉的 dead tuple,所以每次扫描要碰 5 倍多的 page。换大盘只能让你再撑一阵子,根本问题没解。
另一种常见误诊:怪 query planner。plan 变差是因为 statistics 过期了,但 statistics 过期是因为 analyze 没法在满是 dead row 的表上正常工作。
预防
- 集群级别设
idle_in_transaction_session_timeout。事务挂几小时没 commit,基本没有合法理由。 - 监控
oldest_tx_seconds、n_dead_tup比例、pg_replication_slots的 lag。 - 不要随便建 replication slot 又不配套清理逻辑;孤儿 slot 当 memory leak 处理。
- 1000 万行以上、写量大的表,把
autovacuum_vacuum_scale_factor改成 0.01-0.02,别用全局默认的 0.2。 - 每周跑一次
SELECT * FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 20;做体检。
FAQ
- kill backend 会丢数据吗? 不会。Postgres 干净地 rollback 整个事务,应用层只看到连接断开。
VACUUM FULL能常用吗? 不建议。它是排他锁重写整张表。日常清 bloat 用pg_repack,VACUUM FULL留给救火用。