# 从告警到诊断包

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

---

告警只告诉你一个合同条件成立。它不是事故报告，也不是根因分析。

值班最先丢失的往往不是 metric，而是上下文：

```text
onset 前有哪些变更？
当时谁是 primary？
哪些入口受影响？
长窗和短窗的 event count 是多少？
锁图在三分钟后消失前是什么样？
统计是否刚 reset？
采集链有没有延迟？
日志保留窗口还在吗？
值班执行的第一条查询是否改变了状态？
```

诊断包的任务，是在不增加事故的前提下，自动保存一个**有界、脱敏、带身份与
时间语义**的起点：

```text
alert
  -> manifest
      -> symptom windows
          -> topology and changes
              -> PostgreSQL aggregates
                  -> platform/host correlation
                      -> hypotheses and falsifiers
```

它不能承诺：

```text
captured everything
proved root cause
safe to mutate
compliance achieved
```

机器可读合同：
[`diagnostic-pack-contract.json`](/labs/ch25/diagnostic-pack-contract.json)。

## 25.6.1 自动保存时间窗、拓扑、变更与关键查询 {#item-25-6-1}

### 诊断包从 manifest 开始

任何包先记录：

```json
{
  "run_id": "...",
  "captured_at": "...Z",
  "trigger_alert": "...",
  "service": "pg36_shop",
  "environment": "l2-sandbox",
  "targets": ["pg-test-1"],
  "tool_versions": {},
  "source_hashes": {},
  "risk": "L0",
  "mutation": "none"
}
```

没有 manifest 的截图无法回答：

- 哪次事件；
- 哪个环境；
- 哪一版 query；
- 谁采集；
- 何时采集；
- 是否做了 mutation；
- 是否后来被改。

### target 要通过多源确认

本章正式采集没有相信工作站 SSH alias，而是：

```text
ProxyJump via pg-meta-1
  -> actual sandbox network 10.10.10.11
      -> hostname pg-test-1
      -> Patroni scope pg-test, name pg-test-1
      -> PostgreSQL cluster_name pg-test
      -> pg_is_in_recovery false
      -> two pg_stat_replication rows
```

如果其中冲突：

```text
stop
classify identity mismatch
do not continue to mutation
```

连接成功不是身份验证。

### 时间窗要覆盖 onset 前后

本章合同默认：

```text
before alert  30m
after alert   15m
clock         UTC
```

这不是 universal window。选择应考虑：

- 最长 burn window；
- scrape/evaluation/group delay；
- release duration；
- query/log retention；
- incident severity；
- 存储成本。

至少保存：

```text
alert starts_at
rule evaluation time
collector time
database clock
change event time
```

如果 after window 尚未完成，可以：

1. 先保存 initial pack；
2. 15 分钟后补一个 immutable supplement；
3. 不覆盖 initial；
4. manifest 建立 parent/child。

### 保存 event count，不只保存 ratio

同样 10% bad ratio：

```text
1 bad / 10 total
10,000 bad / 100,000 total
```

动作和置信度不同。SLO section 保存：

- good/bad/eligible count 或 rate；
- long/short window；
- threshold；
- burn；
- low-traffic flag；
- missing/freshness；
- independent probe；
- objective version。

不要只截图红线。

### topology 是事件时刻拓扑

保存：

```text
service entrypoints
HAProxy/PgBouncer path
cluster/member role
Patroni timeline
replication state
declared dependencies
read/write routing
```

不自动保存：

- client address；
- password；
- connection URI；
- inventory secret。

IP 是否允许进入私密包取决于政策；本章公开摘要只保留教学网络身份。

### 变更时间线

收集：

```text
application release
schema migration
PostgreSQL parameter/config
pool size/mode
HAProxy route
failover/switchover
node restart
backup/restore
monitoring rule/dashboard
credential/security policy
capacity/retention
```

每个 event：

```text
event id
actor role
target
requested/approved/executed times
result
rollback/roll-forward
evidence link
```

不要直接收集个人聊天全文作为唯一 change log。

### PostgreSQL section

本章自动保存：

```text
activity aggregate by client state/wait type
lock aggregate
replication identity/state/gap without client address
pg_stat_wal
pg_stat_checkpointer
pg_stat_archiver
nonzero pg_stat_io aggregate
database aggregate
maintenance aggregate
pg_stat_statements aggregate/reset, no query text
```

为什么 activity 只自动聚合：

- 完整 query text 敏感；
- PID 列表可能很大；
- 一次 current sample 很快过时；
- 自动证据的目标是安全起点。

若 incident 需要 blocker graph，runbook 再用受限角色采集二级包。

### SQL query controls

采集连接先执行：

```sql
SET statement_timeout = '5s';
SET lock_timeout = '500ms';
SET default_transaction_read_only = on;
BEGIN READ ONLY;
```

并禁止：

```text
EXPLAIN ANALYZE
pg_stat_reset*
pg_stat_statements_reset
VACUUM / ANALYZE
CHECKPOINT
cancel/terminate
DDL/DML
load generation
failover
restore
reload
```

`READ ONLY` 不是绝对安全证明：

- SELECT 仍可很贵；
- function 可能具有副作用，具体取决于实现与权限；
- 系统视图查询可拿锁；
- external extension 可能访问资源。

因此同时使用 allowlisted query、timeout 和 least privilege。

### queryid-level top list

自动保存：

```text
dbid
userid or role class
queryid
toplevel
calls
total/mean execution
rows
blocks/temp/WAL
stats_since/minmax_stats_since
```

限制：

```text
top 50
no query text
no bind
no client address
```

top 50 不是完整 workload。manifest 要写：

```text
sort key
limit
window/reset
source member
```

### 平台 section

保存：

- exporter/target freshness；
- VictoriaMetrics health/series；
- VMAlert groups/rules/error/missed iteration；
- Alertmanager failure counter；
- node resource trend；
- 日志 query count，不含 body；
- component versions。

本章公开摘要包括：

```text
VMAlert 17 groups
50 alert rules
698 recording rules
0 rule errors
```

它是采集时刻 baseline，不能事后覆盖。

### 文件布局

建议：

```text
incident-<id>/
├── manifest.json
├── symptom.json
├── topology.json
├── changes.json
├── postgresql.json
├── platform.json
├── hypotheses.json
├── validation.json
└── hashes.json
```

正文和 body 类材料如确需保存，放在更严格的 evidence tier，不与常规诊断包
混合。

### 分区大小上限

本章每 section：

```text
manifest        64 KiB
symptom         256 KiB
topology        256 KiB
changes         256 KiB
postgresql      1 MiB
platform        1 MiB
hypotheses      256 KiB
```

超限时：

- 不应无限截断而不标记；
- 记录 `truncated=true`；
- 保存 total count；
- 使用 top-N；
- 提供 restricted source link；
- 按需发起二级采集。

### 初始包与后续包

```text
T0 automatic
  cheap, bounded, no text

T1 operator
  focused blocker/query/log under incident authority

T2 mutation evidence
  before/after, approval, command, verification

T3 postmortem
  decisions, cause, counterfactual, actions
```

不要让 T0 自动化拥有 T2 权限。

### source hash 与可重放

保存：

- collector version；
- query template hash；
- rule/config hash；
- upstream governance run id；
- output hash；
- capture parameters。

这样以后能回答：

```text
same data, same query?
same rule version?
same target?
```

hash 证明 byte identity，不证明内容真实或政策正确；仍需身份和 source trust。

## 25.6.2 区分首发症状、伴随现象与根因证据 {#item-25-6-2}

### 先写 timeline，不先写故事

事实表：

| 时间 | 来源 | 事实 | 置信度 |
|---|---|---|---|
| 10:01:30 | release event | version B 完成 | high |
| 10:03:00 | user SLI | latency short window 超阈值 | high |
| 10:04:00 | PgBouncer metric | wait queue 增长 | medium/high |
| 10:04:20 | trace sample | pool wait 占主要时间 | sampled |
| 10:05:00 | PG activity | active/wait 未显著变化 | point sample |

先不要写：

```text
version B caused database slowdown
```

事实支持的初步说法：

```text
latency symptom began after version B;
pool wait correlated;
current PostgreSQL execution evidence did not show the same growth.
```

### 四种状态标签

```text
symptom
  contract violation directly observed

correlate
  same window changed, causal role unknown

hypothesis
  proposed mechanism with predicted evidence

cause
  mechanism corroborated, alternatives tested, repair verified
```

诊断包中的每条观察带 `role`，避免相关线自动升级成 root cause。

### 首发症状不是第一个 dashboard spike

“first observed” 受：

- scrape interval；
- evaluation interval；
- threshold；
- 日志延迟；
- 时钟；
- 采样；
- dashboard refresh

影响。可以说：

```text
first reliable observation available to this system
```

不要说“绝对第一个事件”，除非证据支持。

### 假设要写预测与反证

模板：

```yaml
hypothesis: pool configuration reduced reusable server connections
mechanism: requests queue before PostgreSQL
predicts:
  - PgBouncer wait rises
  - edge latency rises
  - PostgreSQL active executions do not rise equally
  - host CPU/I/O remains stable
falsified_by:
  - no pool wait
  - server execution time explains latency
  - rollback does not change symptom
next_safe_query:
  - inspect pool wait and config event
mutation_required: false
```

这比“看起来像连接池”更可执行。

### 反例一：CPU 同时升高

```text
availability fast burn
CPU high
```

可能机制：

- 应用 retry storm 让 CPU 高；
- slow query 让 CPU 高并导致错误；
- batch 与 outage 巧合；
- monitoring query 自身；
- 另一 process。

需要：

```text
queryid active work
wait state
request/transaction rate
process CPU
release/batch event
repair response
```

CPU 是 correlate，不是自动 cause。

### 反例二：replica gap 与 stale read

```text
freshness probe bad
replica WAL gap high
```

强候选，但仍检查：

- probe 确实走 replica；
- token commit 已确认；
- clock；
- route；
- cache；
- transaction snapshot；
- replica state/timeline。

如果切 primary path 后新 token 立即满足，而 gap 回落后 replica path 恢复，
机制更强。

### 反例三：归档失败计数

```text
failed_count=21
```

时间线：

```text
last failure 18:57
last success 22:27
current capture 22:48
```

结论：

```text
historical failures occurred
archive later succeeded
no active failure is established by this counter alone
```

这是 falsification：反驳“非零 counter = 当前故障”。

### 反例四：锁已经消失

用户 latency 曾受 lock 影响，但初始包晚到：

```text
current pg_locks no blocker
deadlock/lock-wait log in window
application timeout in window
```

不能因为 current view 空就否认历史；也不能因为日志有等待就证明当前仍阻塞。
timeline 必须保留各自时间语义。

### 修复验证要回到症状

如果动作是 cancel blocker：

```text
blocker disappears
```

只是组件验证。还要：

- user burn short/long window；
- event outcome；
- unknown writes reconciliation；
- correctness；
- pool/entry；
- 副作用；
- recurrence。

动作可能同时让 query 消失和用户失败增加。

### recovery 不是瞬时绿

fast rule 的两个窗口恢复速度不同：

```text
5m clears first
1h remains elevated
```

关闭事件政策可要求：

- 短窗恢复；
- 长窗下降趋势/低于阈值；
- independent probe；
- correctness check；
- no new unknown outcome；
- repair stable for observation period。

不要等所有长窗完全归零，也不要一个样本就关。

### counterfactual

root cause review 要问：

```text
如果没有这次变更，症状是否仍会发生？
如果只改变这个机制，症状是否恢复？
为什么监控提前没有发现？
哪个 guardrail 能阻止复发？
```

不能真实重放生产时，可以：

- staging reproduction；
- synthetic fixture；
- plan/config diff；
- canary；
- independent domain evidence。

明确证据强度。

### 诊断包不是 postmortem

诊断包：

```text
raw-ish bounded facts
initial hypotheses
capture boundary
```

postmortem：

```text
impact
timeline
cause/contributing factors
detection/response
decisions
counterfactual
actions/owners/dates
```

不要在自动包里预填 `root_cause=database`。

## 25.6.3 证据采集本身的负载和权限边界 {#item-25-6-3}

### observer effect

采集会：

- 建立连接；
- 拿 `AccessShareLock`；
- 读取 shared stats；
- 执行 sort/aggregate；
- 查询 storage；
- 扫描日志；
- 占网络与磁盘；
- 出现在 `pg_stat_activity`/`pg_stat_statements`；
- 影响被观察系统。

目标是有界，不是声称零影响。

### 风险分级

| 采集 | 风险 | 默认 |
|---|---:|---|
| health/version/small metric | L0 | 自动 |
| bounded system-view aggregate | L0 | 自动 |
| top queryid without text | L0 | 自动，有 timeout |
| blocker graph/PID detail | L0/L1 data-sensitive | incident role |
| bounded log body | sensitive | 二级授权 |
| raw SQL/bind export | high data risk | 禁止自动 |
| `EXPLAIN` | low/moderate | 评估 function/lock |
| `EXPLAIN ANALYZE` | executes statement | 禁止自动 |
| statistics reset | destructive observation | 禁止 |
| load/fault injection | mutation | 隔离演练 |

### `EXPLAIN ANALYZE` 会执行

对 DML：

```sql
EXPLAIN ANALYZE DELETE ...
```

真的执行 DELETE，除非在可回滚事务中且没有外部副作用；即便 rollback，也会：

- 运行 trigger/function；
- 拿锁；
- 产生 WAL/side effect；
- 消耗资源；
- 影响 sequence/external service 等。

诊断自动化不得把它当只读。

### function 与 extension

`SELECT` 可以调用：

- volatile function；
- security definer；
- external API；
- advisory lock；
- file/extension；
- sleep。

allowlist system catalog/view query，不接受 incident 参数拼任意 SQL。

### query timeout

本章：

```text
statement_timeout  5s
lock_timeout       500ms
parallel capture   max 4
top query          50
log rows           1000
```

timeout 后：

- 记录 section incomplete；
- 保存 error class；
- 不在 tight loop 重试；
- 不自动提高 timeout；
- 交给 operator 决定替代 source。

### 并发限制

对 100 个 database 同时执行 catalog query，单条很轻也会形成 load spike。

策略：

```text
priority:
  affected service/database first

concurrency:
  fixed small pool

jitter:
  avoid synchronized collectors

deadline:
  stop when incident value decays

cache:
  reuse inventory/version
```

### log query cost

无界：

```text
all clusters
30 days
regex on full message
no limit
```

会拖慢 VictoriaLogs，也可能返回大量敏感正文。

有界：

```text
exact cls/ins/database
45m window
severity/SQLSTATE structured filter
limit 1000
count/group first
body only under secondary authorization
```

### metric query cost

危险模式：

- regex 匹配所有 metric；
- 高基数 query label 长窗口；
- subquery 小 step；
- `topk` 前未聚合；
- cross join；
- dashboard 多 panel 同时 refresh；
- incident automation 重复相同查询。

记录：

- expression hash；
- start/end/step；
- series count；
- duration；
- result size；
- timeout/error。

### 权限

数据库：

```text
machine exporter
  dedicated monitoring role, stable views

automatic diagnostic
  aggregate-only allowlist

interactive incident
  time-bounded pg_read_all_stats or narrower

mutation operator
  separate role/approval
```

监控平台：

```text
metric read
log metadata read
log body read
rule edit
silence create
receiver secret
admin
```

不应是一个万能账号。

### evidence 文件权限

本章 private bundle：

```text
directory 0700
files     0600
```

reviewer 检查所有文件 mode。公共仓库只放 allowlist summary：

- 版本；
- 计数；
- pass/fail；
- declared gaps；
- run id；
- production gate。

### evidence secret scan

自动扫描：

- SCRAM verifier；
- private key；
- credential-bearing URI；
- authorization header；
- clear password JSON；
- `query/raw_sql/sql_text` 字段。

扫描通过不证明绝对无秘密：

- 未知格式；
- encoded value；
- 业务 PII；
- hash 可重识别；
- file name。

仍要 data classification 与人工 review。

### retention

本章 private evidence 教学默认 30 天；生产需独立政策。决定：

- 事件/合规需求；
- 敏感程度；
- 调查周期；
- 删除；
- legal hold；
- backup；
- 访问 audit；
- hash/manifest。

不要因为是“监控证据”永久保存。

### 完整性与可更新性

原始 initial pack 不覆盖。修正错误时：

```text
new supplement
references original run_id/hash
states correction reason
preserves original
```

公开摘要可以更新展示，但应保留 underlying immutable run reference。

### failure-safe behavior

若 capture 失败：

```text
do not report empty as healthy
mark section incomplete
record error without secret
continue independent low-cost sections
page metamonitoring if critical path
do not mutate database to make capture pass
```

本章 validator 对 application SLI absence 就是：

```text
declared expected gap
```

而不是失败或绿色。

### 自动化权限不能随事故升级

事故 severity 变高，不代表 collector 自动获得：

- superuser；
- SSH root；
- raw log body；
- query text export；
- failover；
- terminate；
- restore。

需要新 authority 的动作必须停下来进入 SOP/change plan。

### 诊断入口输出

给第 31 章的输入：

```text
manifest identity/time/version
user symptom and windows
entrypoint/path
PostgreSQL state/counters/reset
top queryid aggregates
topology/change timeline
host/platform correlation
known gaps
hypotheses and falsifiers
permissions/risk
```

第 31 章再按症状分支：

```text
slow query
lock/deadlock
connection/pool
replication/freshness
disk/capacity
vacuum/freeze
```

### 本节验收

你应当能说明：

1. 诊断包为什么先有 manifest；
2. target 为什么要多源确认；
3. 为什么同时保存 alert、collector 和 database clock；
4. ratio 为什么必须带 event count；
5. topology 与 change event 为什么要保存事件时刻版本；
6. automatic pack 为什么只保存 queryid 聚合；
7. T0/T1/T2/T3 四层 evidence 权限如何分开；
8. symptom/correlate/hypothesis/cause 的措辞差异；
9. 假设为什么必须带预测与反证；
10. repair 为什么要回到 user/control signal 验证；
11. `EXPLAIN ANALYZE` 为什么不能自动运行；
12. timeout/limit/concurrency 如何让采集 fail-safe；
13. `0700/0600` 与 public allowlist 各解决什么；
14. secret scan 为什么仍不能替代数据分类；
15. 采集失败为什么必须是 incomplete/unknown 而不是 healthy。

---

[上一节：Pigsty 可观测体系](../05/) · [返回本章目录](../) · [下一节：实战：实现并演练观察契约](../07/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
