28.3 冻结、XID 与保留者
空间膨胀会让系统越来越慢;事务 ID 回卷可能让系统为了保护数据而拒绝写入。
两者都由 VACUUM 参与治理,却不是同一个风险:
space debt:
obsolete tuple -> reusable page space
age debt:
old XID/MXID -> frozen/advanced horizon一张几乎不更新的静态表可能没有 dead tuple,却必须周期性冻结;一张高 churn 表可能 每天 vacuum,仍被一个旧 snapshot 阻止回收。成熟运维要同时看“垃圾速度”和“年龄 安全线”。
28.3.1 XID 年龄、冻结与回卷保护
XID 是全局 32 位循环空间
普通内部 xid 为 32 位,约每 42.9 亿次分配回卷一次。PostgreSQL 用模 $2^{32}$
比较:
relative to current XID:
about 2 billion are in the past
about 2 billion are in the future若一个行版本保留超过约 20 亿个事务,它原本的插入 XID 会从“过去”落到“未来”的 比较区间。冻结的目的,是把已确定对所有当前与未来事务可见的老版本标成永久过去。
注意:
- XID 在事务第一次需要写入时才分配,不一定等于
BEGIN时间顺序; txid_current()等旧接口与xid8/epoch 语义不同;- 不能用 32 位
xmin做长期业务序列或跨回卷排序。
事务标识内部说明见 Transactions and Identifiers。
冻结是 flag,不是把 xmin 改成 2
PostgreSQL 9.4 以后,冻结通常设置 tuple header flag,并保留原 xmin 供取证;不能
因为查询还看到原 xmin 就断言“没冻结”。老版本升级数据库可能仍见
FrozenTransactionId=2。
正确证据是:
relfrozenxid advance
VM all-frozen pages
VACUUM VERBOSE freeze output
pg_visibility checks而不是:
SELECT count(*) WHERE xmin <> '2';关系、数据库和 TOAST 三层年龄
每个普通表/物化视图:
pg_class.relfrozenxid
pg_class.relminmxid每个数据库:
pg_database.datfrozenxid = minimum relation relfrozenxid
pg_database.datminmxid = minimum relation relminmxiddatabase 值用于 cluster-level 预警,relation 值用于定位。表的 TOAST relation 也可能 最老,不能漏。
数据库总览:
SELECT
datname,
age(datfrozenxid) AS xid_age,
mxid_age(datminmxid) AS mxid_age,
datallowconn
FROM pg_database
ORDER BY age(datfrozenxid) DESC;当前数据库按关系下钻:
SELECT
c.oid::regclass AS relation,
c.relkind,
age(c.relfrozenxid) AS heap_xid_age,
age(t.relfrozenxid) AS toast_xid_age,
greatest(
age(c.relfrozenxid),
coalesce(age(t.relfrozenxid), 0)
) AS effective_xid_age,
mxid_age(c.relminmxid) AS heap_mxid_age,
mxid_age(t.relminmxid) AS toast_mxid_age,
pg_total_relation_size(c.oid) AS total_bytes
FROM pg_class AS c
LEFT JOIN pg_class AS t ON t.oid = c.reltoastrelid
WHERE c.relkind IN ('r', 'm')
ORDER BY effective_xid_age DESC
LIMIT 50;relfrozenxid 是最近一次成功推进该边界的 vacuum 结果,不是“最老一行的精确插入
时间”。age() 是相对于当前 XID 的事务数量,不是秒。
把年龄换成时间余量
同样 100 million age:
100 TPS XID allocation about 11.6 days
10,000 TPS XID allocation about 2.8 hours因此告警要看:
$$ \text{headroom seconds} \approx \frac{\text{configured threshold} - \text{current age}} {\text{recent XID allocation rate}} $$
并给 maintenance duration、业务尖峰和失败重试留余量。仅用固定 age percentage, 无法表达突然增长的 XID burn rate。
获取近似分配速率可对固定间隔的 pg_current_xact_id()/监控计数做差,但读取函数是否
分配 XID、采样事务本身的影响要按接口语义处理。Pigsty 的 PGSQL Persist 历史曲线更
适合看持续速率,原生 catalog 负责当前边界。
regular、aggressive 和 failsafe
regular VACUUM
scans pages likely needing work
may skip all-visible pages
aggressive VACUUM
visits every page that might contain unfrozen XID/MXID
advances relfrozenxid / relminmxid when full necessary coverage achieved
failsafe
last-resort anti-wraparound mode
removes cost delay
may bypass non-essential index maintenance
prioritizes age correctness相关门槛:
vacuum_freeze_min_age
vacuum_freeze_table_age
autovacuum_freeze_max_age
vacuum_failsafe_age
vacuum_multixact_freeze_min_age
vacuum_multixact_freeze_table_age
autovacuum_multixact_freeze_max_age
vacuum_multixact_failsafe_agePostgreSQL 会限制部分有效值,例如 vacuum_freeze_table_age 不会有效超过
autovacuum_freeze_max_age 的 95%。不要只读配置文件;读 pg_settings.setting
确认实际值。
eager freeze
PostgreSQL 18 的普通 vacuum 可能主动扫描一部分 all-visible 但未 all-frozen 的页面,
提前冻结,减少以后 aggressive vacuum 的工作;相关行为可用
vacuum_max_eager_freeze_failure_rate 调整。
这意味着:
regular vacuum不再等价于“绝不扫描可跳过页”,但它仍不保证每次全表 aggressive coverage。
实验中的年龄证据
第 28 章夹具不是回卷压力测试;它绝不消耗数十亿 XID。它只验证:
baseline relfrozenxid age 14
post VACUUM (FREEZE) age 2
all-frozen pages 10,625这些值证明 freeze 路径在一次性表上生效,不证明生产 threshold、maintenance duration 或 headroom 合理。
28.3.2 长事务、backend_xmin 与 idle in transaction
事务久不等于一定持有旧 snapshot,反之亦然
xact_start 告诉你事务开始时间;backend_xmin 告诉你该 backend 对 vacuum horizon
的贡献。它们相关,但不等价:
old xact_start + backend_xmin likely reclamation holder
old xact_start + no backend_xmin still examine locks/XID/state
recent xact_start + old xmin possible imported/exported snapshot
idle outside transaction usually no MVCC snapshot retention
idle in transaction dangerous candidate查询:
SELECT
pid,
datname,
usename,
application_name,
client_addr,
state,
xact_start,
query_start,
state_change,
backend_xid,
backend_xmin,
age(backend_xid) AS xid_age,
age(backend_xmin) AS xmin_age,
wait_event_type,
wait_event,
left(query, 200) AS query
FROM pg_stat_activity
WHERE pid <> pg_backend_pid()
AND (
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, xact_start;idle in transaction 为什么危险
应用执行:
BEGIN
SELECT ...
client pauses / forgets COMMITbackend 不用 CPU,却可能:
- 保留 snapshot,阻止 dead tuple 回收;
- 持有 relation/row/advisory locks;
- 占用 backend 与连接池槽;
- 让 DDL、vacuum、reindex 等等待;
- 让 table/index/WAL 间接增长。
所以“CPU 为 0”不是无害。
timeouts 要按角色与协议设计
PostgreSQL 18 提供:
idle_in_transaction_session_timeout
transaction_timeout
statement_timeout
lock_timeout
idle_session_timeout其中:
idle_in_transaction_session_timeout专门终止在开放事务中 idle 过久的 session;transaction_timeout限制整个事务跨度;- 普通 idle session 不持有开放事务,危害与 idle-in-xact 不同;
- pool/middleware 可能无法优雅处理被服务端突然关闭的连接。
更稳妥的做法:
ALTER ROLE app_rw IN DATABASE appdb
SET idle_in_transaction_session_timeout = '2min';
ALTER ROLE analyst IN DATABASE appdb
SET transaction_timeout = '30min';示例值不是通用推荐。应用必须:
- 正确 rollback/retry;
- 不在事务中等待用户输入或远程 API;
- 流式读取时理解 cursor/snapshot 生命周期;
- 为 migration、batch、backup 使用独立角色和窗口。
精确处置,不批量杀 idle
处置协议:
1. identify PID + role + database + application + client
2. verify backend_xmin/xid/locks and business transaction
3. contact owner or follow pre-approved runbook
4. prefer graceful commit/rollback
5. cancel statement if only statement must stop
6. terminate session only when necessary and retry-safe
7. verify holder disappeared
8. rerun/observe vacuum and business correctness正式实验只终止:
database pg36_maintenance
role dbuser_pg36maint
application pg36-ch28-old-snapshot
PID exact observed PID
backend_xmin non-null
matched sessions exactly 1结果:
terminated sessions 1
remaining holder sessions 0
unrelated terminated 0没有使用“杀掉所有 idle in transaction”。
读副本也可能把 horizon 反馈到主库
hot_standby_feedback 可降低 standby query cancellation,但会把所需 xmin 反馈到
primary,导致 primary 保留 dead tuple。取舍是:
cancel long replica query
vs
retain primary heap versions有 replication slot 时,还要联合 pg_replication_slots.xmin。只在 standby 查
pg_stat_activity 可能找不到 primary 膨胀的完整原因。
28.3.3 复制槽 xmin 与孤儿 pg_prepared_xacts
复制槽有两类保留线
pg_replication_slots 中:
| 字段 | 保留什么 |
|---|---|
xmin | 数据库必须保留的最老事务;更晚删除的 tuple 不能被 vacuum 移除 |
catalog_xmin | 逻辑解码所需系统目录 tuple |
restart_lsn | consumer 仍可能需要的最老 WAL |
confirmed_flush_lsn | 逻辑 consumer 已确认接收的位置 |
wal_status/safe_wal_size | WAL 保留状态与走向 lost 的余量 |
空间问题要分开:
xmin/catalog_xmin -> heap/catalog bloat and vacuum horizon
restart_lsn -> pg_wal retention“复制槽只会撑大 WAL”是错的。
查询:
SELECT
slot_name,
slot_type,
database,
active,
active_pid,
age(xmin) AS xmin_age,
age(catalog_xmin) AS catalog_xmin_age,
restart_lsn,
confirmed_flush_lsn,
wal_status,
safe_wal_size,
inactive_since,
conflicting,
invalidation_reason,
failover,
synced
FROM pg_replication_slots
ORDER BY
greatest(
coalesce(age(xmin), 0),
coalesce(age(catalog_xmin), 0)
) DESC,
slot_name;inactive 不等于 orphan
一个 inactive slot 可能是:
- 正常短暂断线;
- 灾备系统等待窗口;
- CDC consumer 故障;
- 切换后遗留;
- 已废弃对象;
- standby 同步 slot。
drop slot 前必须确认:
consumer owner
expected reconnect
replica/CDC resume semantics
required WAL/rows already lost?
will replica need rebuild?
failover/synced restrictions官方文档明确提示:若删除仍会回来使用的 slot,对应 replica 可能需要重建。
因此:
SELECT pg_drop_replication_slot('unknown_slot');不是发现 inactive 后的第一步。
prepared transaction 没有客户端也能继续持有状态
两阶段提交:
BEGIN
changes
PREPARE TRANSACTION 'gid'
-- original session may leave
COMMIT PREPARED / ROLLBACK PREPARED later进入 prepared 后,它仍可持有:
- 已分配 XID;
- 行/表锁;
- 可见性和清理边界影响;
- 未决业务结果。
查询:
SELECT
transaction,
age(transaction) AS xid_age,
gid,
prepared,
clock_timestamp() - prepared AS prepared_for,
owner,
database
FROM pg_prepared_xacts
ORDER BY age(transaction) DESC;不要因为 pg_stat_activity 没有对应客户端,就判断“事务已消失”。
orphan 处置需要业务协调器事实
GID maps to which distributed transaction?
coordinator decision is commit or rollback?
did participants commit elsewhere?
is retry idempotent?
what locks and rows are affected?没有这些事实时,随意 ROLLBACK PREPARED 可能破坏跨系统一致性,随意
COMMIT PREPARED 也可能提交应回滚的业务。
正确 runbook:
inventory
-> coordinator/ledger lookup
-> peer participant status
-> explicit decision
-> COMMIT/ROLLBACK PREPARED
-> verify age/locks/business invariants若业务并不需要 2PC,应让 max_prepared_transactions=0 保持禁用,而不是启用后希望
“没人会忘”。
统一保留者清单
一张事故表至少包括:
| source | identifier | age | owner | needed by | action | proof |
|---|---|---|---|---|---|---|
| backend | PID/app | xmin age | team | transaction | commit/terminate | holder gone |
| slot | slot name | xmin/catalog age | CDC/DBA | consumer | resume/drop | consumer state |
| prepared | GID | xid age | coordinator | distributed tx | commit/rollback | business ledger |
只有 owner 和 action 明确,才进入执行。
28.3.4 紧急态先解除保留并让 VACUUM 完成
这一目的标题刻意没有写“立刻 VACUUM FREEZE”。
对 PostgreSQL 18,必须区分:
proactive maintenance
warning / shrinking headroom
write-refusal protection state它们的正确动作不同。
第一阶段:预防与早期告警
正常系统应在远离危险线时:
monitor xid/mxid age and burn rate
keep autovacuum healthy
let aggressive vacuum complete
remove long-lived holders
schedule targeted VACUUM where needed
validate relfrozenxid advancement在这个阶段,针对静态大表使用 VACUUM (FREEZE) 可以是有计划的维护动作:
VACUUM (FREEZE, VERBOSE) app.archive_2020;前提是:
- 已评估 I/O、WAL、锁和 replica;
- 不是为了掩盖未知 blocker;
- 目标表与 TOAST 均被验收;
- 生产窗口已批准。
Pigsty 的:
pig pg freeze mydb封装的是 freeze vacuum;适合明确的计划动作,不应脱离 PostgreSQL 版本语义当作所有 XID 事故的万能按钮。
第二阶段:系统已接近或进入拒绝新 XID
PostgreSQL 18 官方文档说明:
- 临近回卷点会先产生必须 vacuum 的 warning;
- 剩余不足约 3 million XID 时,系统拒绝分配新 XID以保护数据;
- 已在运行的事务可继续,新的只读事务可启动;
- 普通
VACUUM仍可执行。
此时的优先顺序是:
1. resolve old prepared transactions
2. end long-running open transactions
3. remove only confirmed obsolete replication slots
4. run ordinary VACUUM in affected database / oldest relations
5. restore normal operation
6. repair autovacuum/root cause官方文档对 PostgreSQL 18 还明确说:
do not use VACUUM FULL
do not use VACUUM FREEZE
single-user mode is normally unnecessary and undesirable原因是 hard-stop 状态要做恢复正常所需的最小工作:
VACUUM FULL自身需要/消耗 XID、强锁且重写;VACUUM FREEZE做超过最小恢复所需的工作;- single-user mode 会绕开保护并引入停机风险。
这与某些旧版本文章或旧 runbook 不同。执行时以安装版本官方文档为准。
“让冻结完成”的正确含义
在日常或 early warning 阶段:
不要为了降低 I/O 不断 cancel 防回卷 vacuum;解除 blocker,让 aggressive freeze 推进并验收年龄。
在已经拒绝 XID 的 hard-stop 阶段:
不要把
FREEZEoption 当口号;按 PostgreSQL 18 最小恢复流程先解除 prepared xact、长事务和旧 slot,再让普通VACUUM完成。
两者共同反对:
raise autovacuum_freeze_max_age to silence alert
disable autovacuum
cancel every anti-wraparound worker
restart hoping age disappears
delete pg_xact files
reset xid counters by hand这些都没有安全地冻结旧 tuple,可能把正确性风险推向灾难。
紧急 runbook 的 fail-closed 门
incident:
postgresql_version: 18.x
primary_identity: verified
xid_or_mxid: xid
oldest_database: ...
oldest_relations: [...]
burn_rate_per_second: ...
estimated_headroom: ...
holders:
prepared: [...]
backends: [...]
slots: [...]
ownership_confirmed: false
execution:
ordinary_vacuum_only: true
vacuum_full: forbidden
vacuum_freeze_in_hard_stop: forbidden
force_drop: forbidden
single_user: not_planned
validation:
write_assignment_restored: ...
datfrozenxid_advanced: ...
oldest_relation_advanced: ...
warnings_stopped: ...
autovacuum_root_cause: ...ownership_confirmed=false 时,不能自动 drop slot 或 resolve prepared transaction;
需要事故指挥者和业务 owner 决策。
XID 与 MXID 要分案
MXID 用于多事务共同锁行,拥有独立:
pg_multixact storage
relminmxid / datminmxid
freeze thresholds
failsafe thresholds
exhaustion effectsXID 耗尽会阻断所有需要新 XID 的写;MXID 耗尽主要阻断需要创建新 multixact 的锁类 写入。事故名称、监控和 runbook 不能只写“wraparound”。
恢复写入不等于事故闭环
普通 vacuum 让系统重新接受写入,只是止血。根因可能仍是:
application transaction leak
stale logical slot
2PC coordinator failure
worker starvation
I/O insufficient
autovacuum misconfiguration
huge database never covered
maintenance repeatedly canceled事故关闭条件:
headroom restored
burn rate understood
all holder ownership recorded
oldest tables and TOAST advancing
autovacuum completion observable
alerts and capacity model corrected
recurrence test passed否则下一次只是时间问题。
本节检查清单
installed PostgreSQL major/minor
database xid/mxid age
top relation + TOAST age
configured and effective thresholds
XID/MXID burn rate and headroom time
backend_xmin holders
idle-in-transaction sessions
slot xmin/catalog_xmin/restart_lsn
prepared transaction GID/owner/decision
anti-wraparound worker phase
ordinary vs aggressive vs failsafe state
version-correct emergency procedure
post-recovery root-cause action延伸阅读
- PostgreSQL 18:Preventing Transaction ID Wraparound Failures
- PostgreSQL 18:Transactions and Identifiers
- PostgreSQL 18:Vacuuming / Freezing Configuration
- PostgreSQL 18:
pg_database - PostgreSQL 18:
pg_replication_slots - PostgreSQL 18:
pg_prepared_xacts - PostgreSQL 18:Client Timeouts
- Pigsty:
pig pg freeze
上一节:autovacuum 的触发与资源 · 返回本章目录 · 下一节:膨胀与重建 · 查看全书目录 · 查看索引中心