跳至内容

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 relminmxid

database 值用于 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_age

PostgreSQL 会限制部分有效值,例如 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 COMMIT

backend 不用 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_lsnconsumer 仍可能需要的最老 WAL
confirmed_flush_lsn逻辑 consumer 已确认接收的位置
wal_status/safe_wal_sizeWAL 保留状态与走向 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 保持禁用,而不是启用后希望 “没人会忘”。

统一保留者清单

一张事故表至少包括:

sourceidentifierageownerneeded byactionproof
backendPID/appxmin ageteamtransactioncommit/terminateholder gone
slotslot namexmin/catalog ageCDC/DBAconsumerresume/dropconsumer state
preparedGIDxid agecoordinatordistributed txcommit/rollbackbusiness 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 阶段:

不要把 FREEZE option 当口号;按 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 effects

XID 耗尽会阻断所有需要新 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

延伸阅读


上一节:autovacuum 的触发与资源 · 返回本章目录 · 下一节:膨胀与重建 · 查看全书目录 · 查看索引中心

最后更新于