# Pigsty 可观测体系

LLMS 索引： [llms.txt](/llms.txt)

---

Pigsty 提供的是一套 PostgreSQL 可观测参考实现，而不是另一套数据库语义。

```text
PostgreSQL system views
  -> pg_exporter metrics
      -> VictoriaMetrics time series
          -> recording/alert rules
              -> Grafana dashboard
                  -> Alertmanager route

PostgreSQL / PgBouncer / Patroni / pgBackRest / host logs
  -> Vector
      -> VictoriaLogs
          -> Grafana exploration

application traces, when instrumented
  -> VictoriaTraces
      -> Grafana exploration
```

每一层都可能：

- 正常工作；
- 延迟；
- 丢数据；
- 标签漂移；
- 权限不足；
- 版本升级；
- 将一个原生值重新聚合；
- 把缺失误写成零。

所以“Pigsty 面板显示”仍要回到 PostgreSQL 和应用合同复核。

当前官方入口：

- [Pigsty PostgreSQL Monitoring](https://pigsty.io/docs/pgsql/monitor/)
- [Pigsty Dashboards](https://pigsty.io/docs/pgsql/dashboard/)
- [Pigsty pg_exporter](https://pigsty.io/docs/pg_exporter/)

## 25.5.1 采集、存储、规则、面板与通知链 {#item-25-5-1}

### metrics 采集

Pigsty PostgreSQL 监控主要汇集：

```text
PostgreSQL   pg_exporter, usually 9630
PgBouncer    exporter, usually 9631
Patroni      REST/metrics, usually 8008 or HTTPS target
Node         node_exporter, usually 9100
HAProxy      exporter/stats
other infra  etcd, Vector, Grafana, Alertmanager, storage self metrics
```

实际端口、TLS 和访问控制以 inventory/rendered config 为准，不能把教学默认值
当网络策略。

沙箱 `pg-test-1` 的只读探测：

```text
pg_exporter PostgreSQL   9630
pg_exporter PgBouncer    9631
Patroni API              8008
node_exporter            9100
```

探测 endpoint 可证明 process/HTTP 返回，不能证明：

- 所有 SQL collector 成功；
- 所有 database 被发现；
- 权限足够；
- series 新鲜；
- rule 查询正确；
- 用户服务健康。

### target registration

Pigsty 文档中的 PostgreSQL target 文件沿用：

```text
/etc/prometheus/targets/pgsql/
```

命名不意味着当前存储一定是旧版 Prometheus；Pigsty v4 的当前栈使用
VictoriaMetrics。target 记录：

```yaml
labels:
  cls: pg-test
  ins: pg-test-1
  ip: 10.10.10.11
targets:
  - 10.10.10.11:9630
  - 10.10.10.11:9631
  - 10.10.10.11:8008
```

这个 external identity 会与 raw exporter metric-specific labels 合并。

### raw exporter 与存储后的 label 不同

直接访问 `:9630/metrics`：

```text
pg_activity_count{datname="test",state="idle"} 1
```

写入 VictoriaMetrics 后还会带：

```text
cls="pg-test"
ins="pg-test-1"
ip="10.10.10.11"
instance="10.10.10.11:9630"
job="pgsql"
```

因此调试 label 漂移要分别看：

1. exporter raw output；
2. target/relabel config；
3. storage series；
4. recording result。

只看一层可能误判 producer。

### `pg_exporter` 是 SQL 到 metric 的编译层

它的配置定义：

```text
query
minimum PostgreSQL version
timeout
cache
tag columns
metric columns
type
description
scale
```

同一个 view 可以变成多条 metric。升级 PostgreSQL/pg_exporter/Pigsty 后：

- 列可能增加；
- metric 名可能变化；
- tag 可能变化；
- 旧 dashboard/rule 可能失配；
- 权限 schema 可能变化。

生产需要 versioned config 和 compatibility test。

### exporter 内建最小信号

pg_exporter 即使不加载大量自定义 collector，也有基本自描述/连通信号，例如：

```text
pg_up
pg_version
pg_in_recovery
pg_exporter_build_info
```

它们适合判断：

- exporter process/target；
- database connectivity；
- server version；
- recovery role。

`pg_up=1` 不证明所有高成本 collector、扩展 view 或 application database 可读。

### management endpoint 要保护

pg_exporter 的管理/配置能力不应无条件暴露给业务网络。即使 `/metrics` 可被
监控系统读取，也要区分：

```text
scrape read
health read
configuration reload
profiling/debug
```

使用：

- 网络 ACL；
- reverse proxy auth/TLS；
- bind address；
- least-privilege service；
- 管理面关闭或隔离；
- access log/audit。

不要把“exporter 没有业务写权限”等同于“管理 endpoint 无风险”。

### metrics storage

Pigsty v4 使用 VictoriaMetrics 保存时序。它负责：

- ingest；
- time-series index；
- MetricsQL/PromQL-compatible query；
- retention；
- recording result；
- rule state相关读写；
- API。

本章快照：

```text
VictoriaMetrics version          1.148.0
total series                     44,842
total label-value pairs          387,724
distinct metric names            3,078
```

这些不是“监控容量还剩多少”的答案。还要看：

- ingest rate；
- data size；
- retention；
- series churn；
- query latency；
- cache；
- disk；
- backup；
- self errors；
- cardinality trend。

### logs storage

Vector 收集：

```text
/pg/log/postgres
/pg/log/pgbouncer
/pg/log/patroni
/pg/log/pgbackrest
host/service logs
```

发送到 VictoriaLogs。沙箱 health/version 证明：

```text
VictoriaLogs v1.52.0 endpoint healthy
```

本章没有读取任何日志 body，因此没有证明：

- 所有 source 被采集；
- parser 正确；
- redaction 正确；
- retention 正确；
- query role 正确；
- incident window 有完整日志。

endpoint health 只是第一层。

### traces storage

沙箱有：

```text
VictoriaTraces v0.9.4 endpoint healthy
```

但没有声称 `pg36_shop` 发出 application span。三件事要分开：

```text
trace backend exists
collector receives data
specific service has complete/useful instrumentation
```

没有第三项，不能在架构图上把 trace 当成已覆盖。

### rule evaluation

VMAlert：

- 读取规则；
- 向 datasource 查询；
- 执行 recording/alert；
- remote-write recording/state；
- 把 alert 发给 Alertmanager；
- 暴露自监控 API/metric。

快照：

```text
VMAlert version         1.148.0
groups                  17
alert rules             50
recording rules         698
group/rule errors       0
current firing alerts   0
```

这些是 Pigsty 已有规则。第 25 章的：

```text
18 recording + 13 alert
```

只经过隔离工具测试，未放进在线 VMAlert。

### dashboard

Grafana 将三类 data source 组织为：

```text
PGSQL   PostgreSQL metrics
PGCAT   PostgreSQL catalog/direct datasource
PGLOG   PostgreSQL-related logs
```

当前 Pigsty 文档列出约 26 个 PostgreSQL dashboard，按 overview、cluster、
instance、database 等层级组织。

dashboard 的优势：

- 导航与变量；
- 版本化 panel；
- 多层关联；
- time range；
- release annotation；
- top-N；
- drill-down。

它的边界：

- panel query 可能与告警不同；
- 变量默认值可能聚合错 scope；
- time range/step 改变结果；
- downsampling 隐藏 spike；
- 颜色阈值不等于 SLO；
- Grafana cache/query error；
- 直接 PG datasource 可能与 metric snapshot 不同。

### Alertmanager

Alertmanager 负责：

- group；
- dedup；
- route；
- inhibition；
- silence；
- receiver integration；
- repeat。

它不负责决定 expression 是否有业务意义。VMAlert 能成功发送给 Alertmanager，
也不等于外部 receiver 已收到。

快照：

```text
Alertmanager 0.33.1
current alerts 0
notification failure counters nonzero series 0
```

没有 receipt canary，真实 delivery 仍是盲区。

### 端到端链路的健康层

| 层 | 证据 |
|---|---|
| producer | PostgreSQL view/metric raw output |
| scrape | target up、scrape duration/error |
| ingest | newest sample time、storage health |
| query | exact expression result、error |
| rule | evaluation state/error/duration |
| notifier | VMAlert notifier success/error |
| route | Alertmanager matched receiver |
| integration | external API accepted |
| receipt | human/system acknowledgment |

“面板有数据”通常证明到 query；“Alertmanager 无失败”最多证明部分 notifier/
integration 路径；receipt 需要独立 canary。

### Pigsty 三种监控模式

当前文档：

| 模式 | 场景 | 可见能力 |
|---|---|---|
| RDS / Basic | 只有可连接 PGURL | PG metrics，缺 host/pool/LB/log |
| Managed | 现有数据库且可 SSH/sudo | PG + node，其他可选 |
| Full / Standard | Pigsty 创建并管理 | PG/pool/LB/node/log 完整参考栈 |

这直接影响诊断：

```text
RDS mode has no node metrics
  -> cannot conclude host normal from absent panel

RDS mode has no local PG logs
  -> PGLOG absence is expected capability gap

Managed optional pool
  -> check inventory before diagnosing queue
```

dashboard 应根据 capability 隐藏/标记 unavailable，而不是显示绿色零。

### 平台版本是合同的一部分

本章快照：

```text
Pigsty           v4.5.0
PostgreSQL       18.6
pg_exporter      v1.4.0
VictoriaMetrics  v1.148.0
VictoriaLogs     v1.52.0
VictoriaTraces   v0.9.4
Alertmanager     0.33.1
```

旧教程若仍写 Prometheus、Loki 等历史组件，不能直接套用 v4。规则语法具有兼容
目标，但 storage、API、自监控 metric 和运行特性必须按实际版本核对。

## 25.5.2 以指标语义定位集群、实例、数据库和查询 {#item-25-5-2}

### 三个稳定基础身份

Pigsty 使用：

```text
cls   cluster
ins   instance/member
ip    node address
```

同一个：

```text
cls=pg-test
ins=pg-test-1
ip=10.10.10.11
```

可关联 PostgreSQL、PgBouncer、Patroni、node、HAProxy 与 logs。

它们解决的是平台身份，不自动解决业务身份：

```text
service
environment
operation_class
objective_id
```

需要应用层单独提供。

### 为什么同时保留 `cls`、`ins`、`ip`

| label | 用途 | 变化风险 |
|---|---|---|
| `cls` | 服务集群聚合 | cluster rename/migration |
| `ins` | 稳定成员角色 | rebuild/replacement |
| `ip` | node/网络关联 | IP 重用/迁移 |

IP 不是唯一永久身份；instance 也可能重建。诊断包同时保存 topology event 和
captured_at。

### instance 与 exporter endpoint

storage series 还有：

```text
instance="10.10.10.11:9630"
job="pgsql"
```

这个 `instance` 是 scrape target endpoint，不一定等于 Pigsty `ins`。写 query
时明确：

```promql
sum by (cls, ins, ip) (...)
```

不要误用 Prometheus convention 的 `instance` 代替 Pigsty member identity。

### cluster 级

常见问题：

- 集群是否有 primary；
- 多少 member exporter 可达；
- transaction/WAL 总 workload；
- aggregate capacity；
- replication topology；
- entrypoint health。

查询示意：

```promql
min by (cls) (pg_up)
sum by (cls) (rate(pg_db_xact_total[5m]))
max by (cls) (pg_lag)
```

第一条 `min(pg_up)` 只是“是否每个目标 up”，不是 service availability。

### instance 级

常见：

```text
role
activity/wait
checkpointer
I/O
WAL/archive
replication sender/receiver
autovacuum
host resource
```

本章现场：

```promql
pg_in_recovery{cls="pg-test"}
```

可区分：

```text
pg-test-1 0 primary
pg-test-2 1 replica
pg-test-3 1 replica
```

但角色应与 Patroni、direct SQL 和 topology event 交叉，特别是在切换窗口。

### database 级

raw exporter：

```text
pg_activity_count{datname="test",state="idle"}
pg_db_deadlocks{datname="test"}
pg_db_temp_bytes{datname="test"}
```

storage 加平台身份后，可以：

```promql
sum by (cls, datname, state) (
  pg_activity_count
)
```

database label 是 logical database 名；多个 cluster 可能都有 `postgres` 或
`test`，不能丢 `cls`。

### query 级

现场 `pg_query_calls`：

```text
pg_query_calls{
  datname="postgres",
  query="-1567903771303523871"
} 214
```

这里 `query` 的值是 numeric queryid 字符串，不是 SQL text。语义：

```text
metric label name  query
metric label value queryid
cardinality bound  pg_stat_statements.max per tracking dimensions
```

查询时：

```promql
topk(
  20,
  sum by (cls, datname, query) (
    rate(pg_query_exec_time[5m])
  )
)
```

具体 metric 名与单位由当前 `pg_exporter.yml` 定义，使用前查看 `HELP` 和
dashboard query；不要凭记忆假设 `_exec_time` 是 counter 还是 seconds。

### query label 仍有 cardinality 成本

即使不是 raw text：

- `pg_stat_statements.max=10000`；
- 多个 database/user/toplevel；
- 多个 instance；
- 旧 series retention；
- query churn；

仍可产生大量 series。current snapshot 的 top label cardinality 中
`recording` 有 698 个值，也说明 rule identity 自身会形成规模。

需要：

- 只保留需要的 query metric；
- 记录 dealloc；
- 控制 dynamic SQL shape；
- top-N dashboard；
- retention；
- 不要把 queryid 复制到 page grouping。

### exact metric names 来自当前 exporter

沙箱 pg_exporter v1.4.0 实际暴露：

```text
pg_activity_count
pg_activity_max_conn_duration
pg_activity_max_duration
pg_activity_max_tx_duration

pg_archiver_failed_count / failed_time
pg_archiver_finish_count / finish_time

pg_checkpointer_timed / req / done
pg_checkpointer_write_time / sync_time / buffers_written

pg_db_numbackends / deadlocks / temp_bytes / xact_*

pg_io_read_bytes / write_bytes / extend_bytes
pg_io_read_time / write_time / fsyncs ...

pg_lag
pg_repl_*
pg_query_calls / exec_time / io_time / rows / blocks / wal_bytes
pg_table_age / n_dead_tup / size / bloat estimates
```

这是版本化现场证据，不是永久 API。升级时用：

```bash
curl --fail --silent http://TARGET:9630/metrics
```

仅在受控网络检查 `# HELP`、`# TYPE` 与 label，不要把 endpoint 公网暴露。

### `pg_lag` 与 `pg_repl_replay_diff`

现场：

```text
pg_lag                   all observed 0
pg_repl_replay_diff      two downstream series 0
```

前者可能是 time-like convenience metric，后者是 replay distance；具体实现要
查 exporter SQL。两者都不能代替 commit-correlated SLI。

### archiver metric 命名的实现差异

native view：

```text
archived_count
last_archived_time
failed_count
last_failed_time
```

exporter 观察名：

```text
finish_count / finish_time
failed_count / failed_time
```

规则作者要确认：

- `finish_time` 是 Unix seconds；
- counter type；
- primary/replica exposure；
- reset；
- last success after failure；
- missing on replica。

不能把 native column 名直接猜成 metric 名。

### label join 的显式性

组合两个指标：

```promql
A
and on (service, operation_class, environment)
B
```

必须显式声明 join key。默认所有共同 label 会参与匹配；一侧多一个 `ins` 或
`job`，结果可能空。

调试步骤：

1. 分别查询 A/B；
2. 列 label sets；
3.确定语义上应该一对一、多对一还是聚合；
4.先 aggregate；
5.用 `on`/`ignoring`；
6.检查重复结果。

### 不要通过删除 identity 修复 join

错误：

```promql
sum(A) / sum(B)
```

它可能跨 cluster/environment 聚合，虽然“有数了”，语义已丢失。

正确做法先确定 service scope，再用相同 bounded dimensions。

### scrape freshness

本章正式采集使用：

```promql
timestamp(pg_up)
timestamp(pg_exporter_up)
timestamp(vmalert_iteration_total)
timestamp(alertmanager_notifications_failed_total)
```

并计算 newest sample age，要求不超过 180 秒。`up=1` 但最后样本很旧，不应视为
健康；storage 里旧值可能仍可查询。

### current app SLI absence

查询：

```promql
count({__name__=~"pg36_shop_.*"}) by (__name__)
```

结果 series 为 0。结论：

```text
application SLI not implemented in live sandbox
```

不是：

```text
zero errors
zero latency
100% availability
```

这一区分贯穿全章。

## 25.5.3 面板结论回到 SQL、日志与主机事实复核 {#item-25-5-3}

### dashboard 是索引，不是裁判

一个 panel 应能回答：

```text
query expression
data source
scope variables
unit
legend identity
window/step
missing behavior
link to native evidence
```

看不到 query 的 panel 不适合支撑高风险动作。

### 先检查 dashboard scope

事故前先读变量：

```text
cluster
instance
database
query
time range
timezone
refresh interval
```

常见错误：

- 选了 `pg-meta` 而非 `pg-test`；
- instance 仍是旧 primary；
- database 选 `postgres` 而用户在 `test`；
- time range 不包含 onset；
- browser timezone 与事件 UTC 不同；
- panel 聚合 all instance；
- Grafana repeat panel 隐藏一个 member。

本章 SSH 还遇到一个真实访问路径陷阱：工作站对多个逻辑 IP 的 SSH port
forward 落到同一元节点。只有进入元节点再连接真实沙箱网络，并验证
`hostname + Patroni scope + cluster_name`，才证明查询了 `pg-test-1`。

### 从 availability panel 回到 event

panel：

```text
availability bad ratio 1h / 5m
```

复核：

1. source metric 是否存在；
2. eligible/good selector；
3. event count；
4. low traffic；
5. release/route；
6. missing；
7. reconciliation；
8.独立 probe。

如果 app metric series 根本不存在，panel 不应该显示绿色 0。

### 从 connection panel 回到 activity

panel 显示连接增加：

```sql
SELECT
  backend_type,
  datname,
  state,
  wait_event_type,
  count(*)
FROM pg_stat_activity
GROUP BY backend_type, datname, state, wait_event_type
ORDER BY backend_type, datname, state, wait_event_type;
```

再看：

- PgBouncer client/server/wait；
- HAProxy queue/session；
- application pool config event；
- connection churn log；
- transaction age；
- max connection headroom。

不要直接提高 `max_connections`。

### 从 lock panel 回到 blocker graph

```sql
SELECT
  a.pid AS waiting_pid,
  a.datname,
  a.application_name,
  a.wait_event,
  clock_timestamp() - a.query_start AS wait_age,
  pg_blocking_pids(a.pid) AS blockers
FROM pg_stat_activity AS a
WHERE cardinality(pg_blocking_pids(a.pid)) > 0
ORDER BY a.query_start;
```

然后在受限会话按 queryid/application 找 owner。panel 的 `lock count` 不足以
授权 terminate。

### 从 I/O panel 回到两个系统

PostgreSQL：

```sql
SELECT backend_type, object, context,
       sum(read_bytes), sum(read_time),
       sum(write_bytes), sum(write_time)
FROM pg_stat_io
GROUP BY backend_type, object, context;
```

主机：

```text
device latency/queue/throughput
filesystem capacity
memory/page cache pressure
other process activity
```

只有两层同窗，才能区分 PG workload 与 host path。

### 从 WAL/replication panel 回到原生位置

```sql
SELECT
  application_name,
  state,
  sync_state,
  pg_wal_lsn_diff(sent_lsn, replay_lsn) AS gap_bytes,
  write_lag,
  flush_lag,
  replay_lag
FROM pg_stat_replication;
```

再检查：

- Patroni role/timeline；
- receiver；
- slot；
- WAL generation rate；
- read routing；
- commit token probe。

不要仅凭 panel 的“lag 0”关闭 freshness incident。

### 从 archive panel 回到时间顺序

```sql
SELECT *
FROM pg_stat_archiver;
```

问：

```text
new failure or historical?
last success after failure?
WAL currently generated?
archive queue progressing?
pgBackRest check?
restore evidence age?
```

沙箱就是 `failed_count=21` 但后来成功。panel 若只把 total failed 画红，会永久
误报。

### 从 slow query panel 回到 reset

先看：

```sql
SELECT *
FROM monitor.pg_stat_statements_info;
```

再看 queryid 聚合：

```sql
SELECT
  dbid, userid, queryid, toplevel,
  calls, total_exec_time, mean_exec_time,
  shared_blks_read, temp_blks_written, wal_bytes,
  stats_since, minmax_stats_since
FROM monitor.pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 50;
```

检查：

- reset；
- dealloc；
- member/failover；
- calls vs mean；
- track planning/timing；
- dashboard delta 算法；
- query text 权限。

### 从 vacuum panel 回到对象与 blocker

```sql
SELECT
  schemaname,
  relname,
  n_live_tup,
  n_dead_tup,
  n_mod_since_analyze,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 50;
```

再查：

- progress；
- old transaction/xmin；
- slot/feedback；
- relation size；
- freeze age；
- autovacuum config；
- host I/O。

不要因为 bloat panel 红就执行 rewrite。

### 从 log panel 回到 pipeline

先验证：

```text
source file exists and advances
Vector source healthy
parse errors
VictoriaLogs ingest/query
time zone
retention
redaction
```

然后限定：

```text
cluster / instance / database
severity or SQLSTATE
time window
row limit
```

本章不导出 body；如果必须查看，在受限界面完成。

### 面板与规则必须共享 contract

如果 alert：

```text
bad_ratio 1h + 5m
```

dashboard 却画：

```text
5m p99
```

值班无法复核 alert。至少提供：

- exact long/short expression；
- event count；
- threshold；
- pending/firing start；
- missing/freshness；
- release marker；
- rule evaluation error。

### 用 source link，不复制 payload

alert 携带：

```text
dashboard id
query template id
time range
service/environment
```

而不是把 metric dump、SQL text、log body 全塞进通知。诊断包按权限拉取。

### 三次复核法

高风险动作前至少三类独立证据：

```text
service symptom
  app SLI or independent probe

PostgreSQL fact
  native SQL / log / topology

platform/host fact
  pool / host / rule engine / change event
```

不是机械凑三条，而是让每条能 falsify 竞争解释。

### Pigsty dashboard 的正确使用路径

```text
overview
  locate service/cluster and onset

cluster
  topology, workload, replication, capacity

instance
  role, activity, I/O, WAL, maintenance

database
  transactions, tables, query workload

query/catalog/log
  focused evidence

native SQL and host
  verify before mutation
```

下钻过程中始终保留 time range 和 identity。

### 平台升级验收

Pigsty/pg_exporter/PG major 升级后：

1. inventory/target identity；
2. raw exporter HELP/TYPE/labels；
3. required metric presence；
4. series cardinality；
5. recording rule dry run/unit test；
6. dashboard no-data/error；
7. alert route test；
8. native SQL parity；
9. log parse/redaction；
10. production canary/rollback。

不要以“Grafana 首页能打开”验收监控升级。

### 本节验收

你应当能解释：

1. Pigsty v4 metrics、logs、traces、rules、dashboards、routes 各由谁负责；
2. raw exporter label 与 storage external label 为什么不同；
3. `cls`、`ins`、`ip` 与 scrape `instance` 的区别；
4. RDS/Managed/Full 三种模式会缺哪些信号；
5. `pg_query_*` 的 `query` label 为什么是 queryid 而非 text；
6. exporter endpoint up 为什么不等于所有 collector 正常；
7. VictoriaMetrics series count 为什么需要趋势而非单次上限；
8. VMAlert 无 error 为什么不等于本章规则已部署；
9. Alertmanager 无失败为什么不等于 receiver 已收到；
10. trace backend healthy 为什么不等于应用有 span；
11. 从 connection、I/O、replication、archive、query、vacuum panel 分别回到什么原生证据；
12. 平台升级为什么必须做 metric/rule/dashboard/native parity。

---

[上一节：把观察契约变成告警](../04/) · [返回本章目录](../) · [下一节：从告警到诊断包](../06/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
