前事不忘,后事之师,不忘国耻!

 用户注册  找回密码
 用户注册
搜索
查看: 25|回复: 0

[开发应用] PostgreSQL 事务 ID 回卷治理实战:从 datfrozenxid 告警到 autovacuum 吞吐调优

[复制链接]

[开发应用] PostgreSQL 事务 ID 回卷治理实战:从 datfrozenxid 告警到 autovacuum 吞吐调优

[复制链接]
dbaai

主题

0

回帖

369

积分

DBAAI

积分
369
11 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
PostgreSQL 的 autovacuum,很多 DBA 平时对它的印象就是"后台清理进程,偶尔占点 IO"。可一旦它失守,问题不是慢一点,而是整个库会逼近一个截止期限:事务 ID 回卷(XID wraparound)。真到那一步,PostgreSQL 会强制停止接收写入、进入单用户模式做 anti-wraparound vacuum,业务直接停摆。这次讲一次真实场景下的排查与治理:告警显示 age(datfrozenxid) 超过 8 亿,离 21.4 亿的回卷边界只差三分之一的余量,最后靠梳理冻结机制、揪出阻塞 vacuum 的长事务和泄漏复制槽、调整 autovacuum 吞吐,把年龄压回 4000 万以内。

一、具体的问题

某分析平台的一套 PostgreSQL 12 主库,凌晨收到监控告警:
  1. database pg_waring: age(datfrozenxid) = 812,345,678, threshold 500,000,000
复制代码

起初值班同学没当回事,觉得"不就是个后台任务没跑完"。结果接下来三天年龄不降反升,日志里开始出现:
  1. WARNING:  database "appdb" must be vacuumed within 1093865 transactions
  2. HINT:  To avoid a database shutdown, execute a database-wide VACUUM...
复制代码

同期症状还包括:


  • autovacuum worker 在几张写入最热的表上反复出现,一跑就是几个小时,pg_stat_activity 里全是 autovacuum: VACUUM public.access_log (to prevent wraparound);
  • 值班尝试 SELECT pg_cancel_backend(pid) 取消,进程原地不动——anti-wraparound vacuum 不接受取消;
  • 磁盘 IO 高峰被 vacuum 吃掉近四成,业务批量任务开始排队。


也就是说,这不是"清理没跟上"的小事,而是冻结进度被什么东西卡住了。要治本,得先弄清 PostgreSQL 为什么要冻结。

二、核心原理

PostgreSQL 的 MVCC 靠每行头部的 xmin(插入它的事务 ID)和 xmax(删除/更新它的事务 ID)来判断行版本可见性。事务 ID 是 32 位无符号整数,取值上限约 21.4 亿(2^31 个用于比较的差值空间),用完会"回卷"——从最大值绕回最小值。如果直接拿回卷后的 ID 做新旧比较,几亿行老数据会被瞬间误判成"来自未来的事务",可见性判断全部错乱。

PostgreSQL 的解法是 冻结(freeze):vacuum 在扫描时,把那些"已经足够老、不会再被未完成事务看到"的行版本标记为 frozen。frozen 行在可见性判断里视作"比任何事务都老",从此不再依赖原始 xmin 参与比较。每一层对象都记录着"最老的未冻结事务 ID":


  • pg_class.relfrozenxid:一张表里最老的未冻结事务;
  • pg_database.datfrozenxid:一个库里最老的未冻结事务。


age(x) 返回当前事务 ID 与 x 的差值,也就是"距离回卷还剩多少额度"。autovacuum_freeze_max_age(默认 2 亿)触发的是预防性冻结扫描;而 age(datfrozenxid) 一旦逼近 21.4 亿,PostgreSQL 会不惜代价强推 anti-wraparound vacuum,甚至强制关库。

所以 autovacuum 的首要使命不是"回收死元组空间",而是推进 relfrozenxid/datfrozenxid。凡是能让 vacuum 无法推进冻结进度的东西,都在透支回卷余量:


  • 长事务 / 空闲事务:vacuum 判断一行能不能冻结,要看它是否可能还被"比最老活跃事务更晚开启的快照"需要。一个挂了一周的 idle in transaction 会话,把整个库的最老 xmin 钉死在原地,vacuum 只能绕着走。
  • prepared xacts(两阶段事务):pg_prepared_xacts 里的孤儿预备事务,同样持有全局最老事务号,而且往往一挂就是几个月,最容易被忽略。
  • 复制槽泄漏:物理槽持有 xmin,逻辑槽还额外持有 catalog_xmin。下游宕机或废弃后槽没删,等于永久钉住一个历史时间点,vacuum 永远无法推进。
  • 冻结扫描吞吐不足:vacuum_cost_delay 的限流默认很保守,写入大的库上,冻结速度赶不上事务消耗速度,年龄自然一路爬升。


三、实例参考(动手步骤)

步骤 0:确认现状基线

先量化"离悬崖多远",把库级和表级的年龄都摸一遍:
  1. -- 库级年龄,全实例排序
  2. SELECT datname, age(datfrozenxid) AS age
  3. FROM pg_database
  4. ORDER BY 2 DESC;
  5. -- 表级年龄 TOP 10(只看普通表)
  6. SELECT c.oid::regclass AS table_name,
  7.        age(c.relfrozenxid) AS xid_age,
  8.        pg_size_pretty(pg_total_relation_size(c.oid)) AS size
  9. FROM pg_class c
  10. JOIN pg_namespace n ON n.oid = c.relnamespace
  11. WHERE c.relkind = 'r' AND n.nspname NOT IN ('pg_catalog','information_schema')
  12. ORDER BY age(c.relfrozenxid) DESC
  13. LIMIT 10;
复制代码

这次排查里,库级年龄 8.1 亿,表级最大的 public.access_log 达到 7.9 亿——问题集中在一张日志大表,其余表都在 2 亿以内,说明冻结缺口高度局部化,不是全库失控。

步骤 1:找到钉住 vacuum 的元凶

冻结进度卡住的三大嫌疑人逐一过堂:
  1. -- 1) 长事务与空闲事务
  2. SELECT pid, usename, state, xact_start, now() - xact_start AS xact_age, query
  3. FROM pg_stat_activity
  4. WHERE xact_start IS NOT NULL
  5. ORDER BY xact_start;
  6. -- 2) 预备事务(两阶段)
  7. SELECT gid, prepared, owner, database FROM pg_prepared_xacts ORDER BY prepared;
  8. -- 3) 复制槽持有的 xmin
  9. SELECT slot_name, slot_type, active, xmin, catalog_xmin,
  10.        age(xmin) AS xmin_age, age(catalog_xmin) AS cat_age
  11. FROM pg_replication_slots;
复制代码

结果三条全中:一个报表程序挂了 6 天的 idle in transaction(连接池泄漏的僵尸会话);一个 4 个月前的 pg_prepared_xacts 孤儿条目,来自某次中断的 ETL;还有一个废弃逻辑复制槽 slot_etl_old,catalog_xmin 年龄 6.7 亿,下游环境早已下线。

先解除钉子(在测试环境验证过、确认无人使用后):
  1. -- 终结僵尸空闲事务
  2. SELECT pg_terminate_backend(pid) WHERE pid = 18432;  -- 按实际 pid
  3. -- 回滚孤儿预备事务
  4. SELECT pg_rollback_prepared('etl_20260610_batch_07');
  5. -- 删除废弃复制槽
  6. SELECT pg_drop_replication_slot('slot_etl_old');
复制代码

步骤 2:提高 vacuum 冻结吞吐再补扫

钉子拔掉后,先临时放大 autovacuum 的成本预算,让它跑得动,而不是干着急:
  1. ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 4000;   -- 默认 200
  2. ALTER SYSTEM SET autovacuum_vacuum_cost_delay = '1ms';  -- 默认 2ms
  3. SELECT pg_reload_conf();
复制代码

然后对目标表主动补一次带 FREEZE 的 vacuum(黄金时段之后执行,注意 IO 影响):
  1. VACUUM (VERBOSE, FREEZE) public.access_log;
复制代码

VERBOSE 的输出盯两个数:new relfrozenxid 与 tuples: ... removed。这条命令跑完后 access_log 的表级年龄从 7.9 亿降到 3800 万。

步骤 3:固化参数与回卷监控

治理不能靠一次手工补扫。把预防参数落到配置,把回卷年龄做成独立告警项:
  1. # postgresql.conf
  2. autovacuum_freeze_max_age = 150000000        # 默认 2 亿,提前触发
  3. vacuum_freeze_min_age = 50000000             # 行版本够老就及时冻
  4. autovacuum_vacuum_cost_limit = 2000          # 常态吞吐,比默认激进一档
复制代码

监控 SQL(建议 5 分钟采样一次,阈值分两档:5 亿告警、10 亿严重):
  1. SELECT max(age(datfrozenxid)) AS max_db_age FROM pg_database;
复制代码

治理前后对比

指标治理前治理后(60 天后复查)
max(age(datfrozenxid))812,345,67841,650,000
回卷 WARNING 日志每小时数条0 条
anti-wraparound vacuum 出现频次每天数小时每周一次、分钟级
vacuum 占用的 IO 高峰占比约 40%< 5%
最老长事务存活时长6 天< 10 分钟(连接池强制 idle_timeout)


四、实操检查清单


  • [ ] 每日采集 age(datfrozenxid),阈值:5 亿告警、10 亿严重、15 亿启动应急预案;
  • [ ] 周期巡检 pg_stat_activity 中 xact_age 超过 1 小时的会话,连接池开启 idle_in_transaction_session_timeout;
  • [ ] 每周检查 pg_prepared_xacts,prepared 超过 1 天且无归属的按流程回滚;
  • [ ] 每周核对 pg_replication_slots:active = false 超过 7 天的槽逐个确认归属,废弃即删;
  • [ ] 表级年龄 TOP 10 纳入巡检,单表年龄接近库级年龄时优先安排 VACUUM (FREEZE);
  • [ ] 写入量大的库调高 autovacuum_vacuum_cost_limit,确认冻结速度大于日均事务消耗速度;
  • [ ] 升级路径核对:PG 13+ 部分场景可开 autovacuum_vacuum_insert_threshold,减少纯插入表的冻结扫描;
  • [ ] 所有参数变更先在测试库验证,保留变更记录与回滚方法。


几个容易踩的坑

坑一:把 freeze 当成缩表手段。 VACUUM (FREEZE) 只改行版本标记,不回收空间、不降低表体积。看到"vacuum 了几十 G"就以为表会变小,是常见误解。治膨胀要走 VACUUM FULL 或 pg_repack,那是另一个独立议题,别和回卷治理混为一谈。

坑二:只盯长事务,漏了复制槽。 长事务好查,pg_replication_slots 的 catalog_xmin 静默钉住 vacuum 的案例在实际环境里更多,尤其是逻辑复制和 CDC 链路下线后槽没清理。巡检清单里必须给它留一行。

坑三:anti-wraparound vacuum 被取消不了就慌。 它不接受 pg_cancel_backend,这是设计如此。此时该做的是确认它没被限流卡死(看 pg_stat_progress_vacuum 的 phase 和已扫描页数在动),必要时临时调大 cost_limit 帮它提速,而不是反复尝试杀进程。

坑四:被迫关库时才第一次接触单用户模式。 真到 age 逼近 21.4 亿,PG 会拒绝写入并要求单用户 anti-wraparound vacuum:postgres --single -D $PGDATA appdb,进去后执行 VACUUM FREEZE;。这个流程应当写成预案演练过一次,而不是出事当天现场学。

坑五:vacuum_freeze_table_age 大于 autovacuum_freeze_max_age。 前者决定 vacuum 扫表时是否走"激进冻结扫描",后者决定 autovacuum 是否强制触发。前者设得比后者还大,等于配置自相矛盾——per-table 触发了,扫描却还是保守模式。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

QQ|Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-10-23 16:56 , Processed in 0.017336 second(s), 9 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表