Skip to content

Commit 70c7b86

Browse files
author
xiongcc
committed
添加第45章:How to monitor xmin horizon to prevent XID/MultiXID wraparound and high bloat
1 parent 788d19d commit 70c7b86

File tree

1 file changed

+128
-0
lines changed

1 file changed

+128
-0
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# How to monitor xmin horizon to prevent XID/MultiXID wraparound and high bloat
2+
3+
>我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享!
4+
5+
在之前的文章中,我们讨论了[如何监控 XID 和 MultiXID 回卷的风险](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0044_how_to_monitor_transaction_id_wraparound_risks.md)。这种检查至关重要,并且是任何监控系统中不可或缺的部分。
6+
7+
然而,虽然它有助于了解风险级别,但并不能揭示问题的根本原因 — 当你在进行 XID 回卷的事后分析时,你肯定需要找到问题的根源,比如使用"五个为什么"方法 (开玩笑的,我们要改进监控并控制 `autovacuum` 的行为,所以我们永远不会在生产环境中遇到 XID 回卷问题)。
8+
9+
这个问题可以通过 `xmin` 视界监控来解决。这种检查对于理解膨胀率增长的原因也很有帮助。
10+
11+
## XID/MultiXID回卷风险增加的四个原因
12+
13+
`autovacuum` 未能冻结元组时,会发生 XID/MultiXID 回卷问题。即使 `autovacuum` 已开启,仍可能存在以下四个原因导致冻结失败:
14+
15+
1. 主节点上有长时间运行的事务。
16+
2. 废弃的复制槽。
17+
3. 备节点上有长时间运行的事务且 `hot_standby_feedback = on` (包括级联复制的情况 — 反馈可以传播)。
18+
4. 废弃的预备事务。
19+
20+
这四个原因都需要检查,因为它们可能包含需要保留数据的事务 ID。
21+
22+
相关的好文章:[VACUUM won't remove dead rows: 4 reasons why](https://cybertec-postgresql.com/en/reasons-why-vacuum-wont-remove-dead-rows/)
23+
24+
澄清 (基于监控系统中常见的错误分析):
25+
26+
- 仅监控长时间运行的事务是不够的 — 这四个原因都必须覆盖。实际上,监控长时间运行的事务更多是为了解决另一类问题:锁风险。
27+
- 仅监控复制槽是不够的 — 某些备节点可能不使用复制槽。此外,仅监控 `pg_stat_replication` 也不够,因为它不会涵盖废弃的复制槽。需要同时检查 `pg_stat_replication``pg_replication_slots`
28+
29+
## xmin视界
30+
31+
"`xmin` horizon" 该术语在 Postgres 文档中有使用 (例如在描述 `pg_stat_activity.backend_xmin` 时),虽然未明确定义。它也在源码中使用,函数 `ComputeXidHorizons()` 的注释中有一个[很好的解释](https://github.com/postgres/postgres/blob/6bf2efb38285626a9de3004dd1c23d9a85453372/src/backend/storage/ipc/procarray.c#L1662)
32+
33+
一行的 `xmin` 表示插入该行的事务 ID — 每个表都有一个隐藏的 (系统) 列 `xmin` (可以尝试:`select xmin, * from your_table;`)。
34+
35+
"`xmin` horizon" 表示必须保留的最老数据快照的事务 ID。
36+
37+
## 关于膨胀
38+
39+
如果 `xmin` 视界在短时间内没有推进,阻止了 `autovacuum`,这通常不是问题 — 这类情况经常发生。
40+
41+
然而,如果这种情况持续很长时间,并且 `xmin` 视界停留在过去,那么会导致两个大问题:
42+
43+
1. 如前文所述,XID/MultiXID 回卷风险增加。
44+
2. 膨胀率增长:无法及时删除死元组,导致当 `xmin` 视界移动时需要进行大量删除操作 — 这使得 `autovacuum` 成为"死元组到膨胀的转换器"。
45+
46+
因此,重要的是监控 `xmin` 视界,并在其滞后过多时 (即 `xmin` 视界年龄过高) 采取行动。
47+
48+
## 如何监控
49+
50+
有两种监控 `xmin` 视界的方法:
51+
52+
1. 观察 `autovacuum` 日志。
53+
2. 查询四个系统视图,以覆盖上述的四个原因。
54+
55+
## 基于日志的监控
56+
57+
基于日志的方法无法帮助理解 `xmin` 视界不推进的背后原因,但它可以展示 `autovacuum` 的实际行为。
58+
59+
以下是一个日志示例及其解读:
60+
61+
```bash
62+
2023-11-10 01:04:03.828 PST [56538] LOG: automatic vacuum of table "nik.public.t": index scans: 0
63+
pages: 0 removed, 4480 remain, 4480 scanned (100.00% of total)
64+
tuples: 0 removed, 1000000 remain, 666667 are dead but not yet removable
65+
removable cutoff: 784, which was 112449 XIDs old when operation ended
66+
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
67+
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
68+
avg read rate: 9.685 MB/s, avg write rate: 0.000 MB/s
69+
buffer usage: 7281 hits, 1698 misses, 0 dirtied
70+
WAL usage: 0 records, 0 full page images, 0 bytes
71+
system usage: CPU: user: 0.11 s, system: 0.02 s, elapsed: 1.36 s
72+
```
73+
74+
其中,问题指标有:
75+
76+
- `666667 are dead but not yet removable` — 大量元组已死但 `autovacuum` 无法删除它们,因为这些元组比当前 xmin 视界"更年轻" (与 `xmin` 视界相比,它们的 `xmin` 值处于未来)。
77+
- `removable cutoff: 784, which was 112449 XIDs old when operation ended` — 这告诉我们 XID视界是 784,并且它的年龄是 112449 — 意味着当 `autovacuum` 完成此次处理时, `xmin` 视界 (仍被认为需要的数据版本) 比当前时间落后超过 112k 个事务。
78+
79+
这表明 `xmin` 视界远远落后于当前,某些东西使其停留在过去。要找出具体原因,我们需要检查几个系统视图。
80+
81+
## 使用系统视图进行监控
82+
83+
以下是一个查询示例:
84+
85+
```sql
86+
with bits as (
87+
select
88+
(
89+
select backend_xmin
90+
from pg_stat_activity
91+
order by age(backend_xmin) desc nulls last
92+
limit 1
93+
) as xmin_pg_stat_activity,
94+
(
95+
select xmin
96+
from pg_replication_slots
97+
order by age(xmin) desc nulls last
98+
limit 1
99+
) as xmin_pg_replication_slots,
100+
(
101+
select backend_xmin
102+
from pg_stat_replication
103+
order by age(backend_xmin) desc nulls last
104+
limit 1
105+
) as xmin_pg_stat_replication,
106+
(
107+
select transaction
108+
from pg_prepared_xacts
109+
order by age(transaction) desc nulls last
110+
limit 1
111+
) as xmin_pg_prepared_xacts
112+
)
113+
select
114+
*,
115+
age(xmin_pg_stat_activity) as xmin_pgsa_age,
116+
age(xmin_pg_replication_slots) as xmin_pgrs_age,
117+
age(xmin_pg_stat_replication) as xmin_pgsr_age,
118+
age(xmin_pg_prepared_xacts) as xmin_pgpx_age,
119+
greatest(
120+
age(xmin_pg_stat_activity),
121+
age(xmin_pg_replication_slots),
122+
age(xmin_pg_stat_replication),
123+
age(xmin_pg_prepared_xacts)
124+
) as xmin_horizon_age
125+
from bits;
126+
```
127+
128+
请注意,`min(...)` 函数无法直接应用于 XID 值,因为它们是 32 位且会循环 — 将 XID 转换为 `int` 是不可行的。但 `age(XID)` 函数在这里非常有用。因此,与考虑 `xmin_horizon` 值相比,我们需要处理 `xmin_horizon_age`

0 commit comments

Comments
 (0)