监控 xmin horizon 避免回卷和膨胀

本页目录

在之前的文章中,我们讨论了如何监控 XID 和 MultiXID 回卷的风险。这种检查至关重要,并且是任何监控系统中不可或缺的部分。

然而,虽然它有助于了解风险级别,但并不能揭示问题的根本原因 — 当你在进行 XID 回卷的事后分析时,你肯定需要找到问题的根源,比如使用"五个为什么"方法 (开玩笑的,我们要改进监控并控制 autovacuum 的行为,所以我们永远不会在生产环境中遇到 XID 回卷问题)。

这个问题可以通过 xmin 视界监控来解决。这种检查对于理解膨胀率增长的原因也很有帮助。

XID/MultiXID 回卷风险增加的四个原因

当 autovacuum 未能冻结元组时,会发生 XID/MultiXID 回卷问题。即使 autovacuum 已开启,仍可能存在以下四个原因导致冻结失败:

  1. 主节点上有长时间运行的事务。
  2. 废弃的复制槽。
  3. 备节点上有长时间运行的事务且 hot_standby_feedback = on (包括级联复制的情况 — 反馈可以传播)。
  4. 废弃的预备事务。

这四个原因都需要检查,因为它们可能包含需要保留数据的事务 ID。

相关的好文章:VACUUM won't remove dead rows: 4 reasons why

澄清 (基于监控系统中常见的错误分析):

xmin 视界

"xmin horizon" 该术语在 Postgres 文档中有使用 (例如在描述 pg_stat_activity.backend_xmin 时),虽然未明确定义。它也在源码中使用,函数 ComputeXidHorizons() 的注释中有一个很好的解释。

一行的 xmin 表示插入该行的事务 ID — 每个表都有一个隐藏的 (系统) 列 xmin (可以尝试:select xmin, * from your_table;)。

"xmin horizon" 表示必须保留的最老数据快照的事务 ID。

关于膨胀

如果 xmin 视界在短时间内没有推进,阻止了 autovacuum,这通常不是问题 — 这类情况经常发生。

然而,如果这种情况持续很长时间,并且 xmin 视界停留在过去,那么会导致两个大问题:

  1. 如前文所述,XID/MultiXID 回卷风险增加。
  2. 膨胀率增长:无法及时删除死元组,导致当 xmin 视界移动时需要进行大量删除操作 — 这使得 autovacuum 成为"死元组到膨胀的转换器"。

因此,重要的是监控 xmin 视界,并在其滞后过多时 (即 xmin 视界年龄过高) 采取行动。

如何监控

有两种监控 xmin 视界的方法:

  1. 观察 autovacuum 日志。
  2. 查询四个系统视图,以覆盖上述的四个原因。

基于日志的监控

基于日志的方法无法帮助理解 xmin 视界不推进的背后原因,但它可以展示 autovacuum 的实际行为。

以下是一个日志示例及其解读:

Shell
2023-11-10 01:04:03.828 PST [56538] LOG:  automatic vacuum of table "nik.public.t": index scans: 0
  pages: 0 removed, 4480 remain, 4480 scanned (100.00% of total)
  tuples: 0 removed, 1000000 remain, 666667 are dead but not yet removable
  removable cutoff: 784, which was 112449 XIDs old when operation ended
  frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
  index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
  avg read rate: 9.685 MB/s, avg write rate: 0.000 MB/s
  buffer usage: 7281 hits, 1698 misses, 0 dirtied
  WAL usage: 0 records, 0 full page images, 0 bytes
  system usage: CPU: user: 0.11 s, system: 0.02 s, elapsed: 1.36 s

其中,问题指标有:

这表明 xmin 视界远远落后于当前,某些东西使其停留在过去。要找出具体原因,我们需要检查几个系统视图。

使用系统视图进行监控

下面的查询分别观察用户数据与系统目录的清理视界。逻辑复制槽的 catalog_xmin 会影响系统目录中旧版本的清理,需要与普通数据的 xmin 分开判断。

SQL
with bits as (
  select
    txid_snapshot_xmin(txid_current_snapshot()) as snapshot_xmin,
    -- 主库客户端会话持有的快照。
    (
      select backend_xmin
      from pg_stat_activity
      where backend_xmin is not null
      order by age(backend_xmin) desc
      limit 1
    ) as data_xmin_pg_stat_activity,
    -- 复制槽为用户表保留的事务 ID。
    (
      select xmin
      from pg_replication_slots
      where xmin is not null
      order by age(xmin) desc
      limit 1
    ) as data_xmin_pg_replication_slots,
    -- 逻辑复制槽为系统目录保留的事务 ID。
    (
      select catalog_xmin
      from pg_replication_slots
      where catalog_xmin is not null
      order by age(catalog_xmin) desc
      limit 1
    ) as catalog_xmin_pg_replication_slots,
    -- 备库反馈到主库的快照视界。
    (
      select backend_xmin
      from pg_stat_replication
      where backend_xmin is not null
      order by age(backend_xmin) desc
      limit 1
    ) as data_xmin_pg_stat_replication,
    -- 预备事务会一直保留事务 ID,直至提交或回滚。
    (
      select transaction
      from pg_prepared_xacts
      where transaction is not null
      order by age(transaction) desc
      limit 1
    ) as data_xmin_pg_prepared_xacts
)
select
  *,
  age(data_xmin_pg_stat_activity) as data_xmin_pg_stat_activity_age,
  age(data_xmin_pg_replication_slots) as data_xmin_pg_replication_slots_age,
  age(catalog_xmin_pg_replication_slots) as catalog_xmin_pg_replication_slots_age,
  age(data_xmin_pg_stat_replication) as data_xmin_pg_stat_replication_age,
  age(data_xmin_pg_prepared_xacts) as data_xmin_pg_prepared_xacts_age,
  greatest(
    age(data_xmin_pg_stat_activity),
    age(data_xmin_pg_replication_slots),
    age(data_xmin_pg_stat_replication),
    age(data_xmin_pg_prepared_xacts)
  ) as data_horizon_age,
  greatest(
    age(data_xmin_pg_stat_activity),
    age(data_xmin_pg_replication_slots),
    age(data_xmin_pg_stat_replication),
    age(data_xmin_pg_prepared_xacts),
    age(catalog_xmin_pg_replication_slots)
  ) as catalog_horizon_age
from bits;

XID 是会回卷的 32 位标识符,不能直接按普通整数的大小判断事务先后,也不应通过强制转换再取 min(...) 来寻找最老事务。这里使用 age(XID) 比较事务年龄。

data_horizon_age 用于观察用户表的清理视界;catalog_horizon_age 还计入逻辑复制槽的 catalog_xmin,用于观察系统目录的清理视界。将两者分开,有助于避免混淆不同的故障原因和处理方式。

查询还保留了 snapshot_xmin 原始值,作为跨版本核对当前快照视界的参考。

讨论与补充

在 GitHub 查看讨论

支持作者

感谢支持 PostgreSQL Howto 的长期维护。

微信赞赏码

读者交流群

二维码若已过期,可通过公众号 PostgreSQL 学徒或邮件联系。

PostgreSQL 学徒读者交流群二维码