监控 xmin horizon 避免回卷和膨胀
本页目录
在之前的文章中,我们讨论了如何监控 XID 和 MultiXID 回卷的风险。这种检查至关重要,并且是任何监控系统中不可或缺的部分。
然而,虽然它有助于了解风险级别,但并不能揭示问题的根本原因 — 当你在进行 XID 回卷的事后分析时,你肯定需要找到问题的根源,比如使用"五个为什么"方法 (开玩笑的,我们要改进监控并控制 autovacuum 的行为,所以我们永远不会在生产环境中遇到 XID 回卷问题)。
这个问题可以通过 xmin 视界监控来解决。这种检查对于理解膨胀率增长的原因也很有帮助。
XID/MultiXID 回卷风险增加的四个原因
当 autovacuum 未能冻结元组时,会发生 XID/MultiXID 回卷问题。即使 autovacuum 已开启,仍可能存在以下四个原因导致冻结失败:
- 主节点上有长时间运行的事务。
- 废弃的复制槽。
- 备节点上有长时间运行的事务且
hot_standby_feedback = on(包括级联复制的情况 — 反馈可以传播)。 - 废弃的预备事务。
这四个原因都需要检查,因为它们可能包含需要保留数据的事务 ID。
相关的好文章:VACUUM won't remove dead rows: 4 reasons why
澄清 (基于监控系统中常见的错误分析):
- 仅监控长时间运行的事务是不够的 — 这四个原因都必须覆盖。实际上,监控长时间运行的事务更多是为了解决另一类问题:锁风险。
- 仅监控复制槽是不够的 — 某些备节点可能不使用复制槽。此外,仅监控
pg_stat_replication也不够,因为它不会涵盖废弃的复制槽。需要同时检查pg_stat_replication和pg_replication_slots。
xmin 视界
"xmin horizon" 该术语在 Postgres 文档中有使用 (例如在描述 pg_stat_activity.backend_xmin 时),虽然未明确定义。它也在源码中使用,函数 ComputeXidHorizons() 的注释中有一个很好的解释。
一行的 xmin 表示插入该行的事务 ID — 每个表都有一个隐藏的 (系统) 列 xmin (可以尝试:select xmin, * from your_table;)。
"xmin horizon" 表示必须保留的最老数据快照的事务 ID。
关于膨胀
如果 xmin 视界在短时间内没有推进,阻止了 autovacuum,这通常不是问题 — 这类情况经常发生。
然而,如果这种情况持续很长时间,并且 xmin 视界停留在过去,那么会导致两个大问题:
- 如前文所述,XID/MultiXID 回卷风险增加。
- 膨胀率增长:无法及时删除死元组,导致当
xmin视界移动时需要进行大量删除操作 — 这使得autovacuum成为"死元组到膨胀的转换器"。
因此,重要的是监控 xmin 视界,并在其滞后过多时 (即 xmin 视界年龄过高) 采取行动。
如何监控
有两种监控 xmin 视界的方法:
- 观察
autovacuum日志。 - 查询四个系统视图,以覆盖上述的四个原因。
基于日志的监控
基于日志的方法无法帮助理解 xmin 视界不推进的背后原因,但它可以展示 autovacuum 的实际行为。
以下是一个日志示例及其解读:
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
其中,问题指标有:
666667 are dead but not yet removable— 大量元组已死但autovacuum无法删除它们,因为这些元组比当前 xmin 视界"更年轻" (与xmin视界相比,它们的xmin值处于未来)。removable cutoff: 784, which was 112449 XIDs old when operation ended— 这告诉我们 XID视界是 784,并且它的年龄是 112449 — 意味着当autovacuum完成此次处理时,xmin视界 (仍被认为需要的数据版本) 比当前时间落后超过 112k 个事务。
这表明 xmin 视界远远落后于当前,某些东西使其停留在过去。要找出具体原因,我们需要检查几个系统视图。
使用系统视图进行监控
下面的查询分别观察用户数据与系统目录的清理视界。逻辑复制槽的 catalog_xmin 会影响系统目录中旧版本的清理,需要与普通数据的 xmin 分开判断。
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 原始值,作为跨版本核对当前快照视界的参考。