# 观察与告警契约

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

---

监控系统不会自动知道“什么对用户重要”。它只会忠实地计算你交给它的数值。

```text
wrong semantic + perfect query
  = precisely wrong alert
```

因此告警规则之前要有 observation contract：

```text
objective
  -> source
      -> metric/event type
          -> good/total selector
              -> dimensions
                  -> query
                      -> missing semantics
                          -> fallback
                              -> owner/action
```

本节产出第 25 章的实现合同，而不提前声称指标和 page 已经部署。

## 24.4.1 每个 SLI 的数据源、查询、维度和缺失语义 {#item-24-4-1}

### 一个 SLI 需要哪些字段

最小 observation contract：

| 字段 | 问题 |
|---|---|
| `objective_id` | 它实现哪个 SLO/control？ |
| `source` | 谁产生原始事实？ |
| `metric_type` | counter、histogram、gauge 还是 event？ |
| `good_selector` | 哪些事件进入分子？ |
| `total_selector` | 哪些事件进入分母？ |
| `dimensions` | 按哪些有界维度分解？ |
| `query_template` | 如何从窗口计算？ |
| `missing_semantics` | series 消失代表什么？ |
| `fallback` | 主 observation path 失败后看什么？ |
| `metric_status` | 已存在、待实现还是 deprecated？ |

少一个字段就可能改变结论。

### 本章的五个来源

| objective | source | type | 状态 |
|---|---|---|---|
| availability | `pg36_shop_request_outcomes_total` | counter | 应用待实现 |
| latency | `pg36_shop_request_duration_seconds` | histogram | 应用待实现 |
| freshness | `pg36_shop_commit_visibility_probes_total` | counter | synthetic probe 待实现 |
| correctness | `pg36_shop_reconciliation_mismatches` | gauge | 核对任务待实现 |
| restore readiness | `pg36_shop_restore_evidence_age_seconds` | gauge | 证据导出器待实现 |

这些是本书定义的应用 metric contract，不是 Pigsty 内置指标。`metric_status`
故意保留“to implement”，防止目录文档冒充运行事实。

### availability query

原始 counter：

```text
pg36_shop_request_outcomes_total{
  service="pg36_shop",
  operation_class="place-order",
  environment="production",
  eligible="true",
  outcome="good|bad|unknown"
}
```

bad ratio 模板：

```promql
1 -
sum(
  rate(pg36_shop_request_outcomes_total{
    service="pg36_shop",
    operation_class="place-order",
    eligible="true",
    outcome="good"
  }[$window])
)
/
clamp_min(
  sum(
    rate(pg36_shop_request_outcomes_total{
      service="pg36_shop",
      operation_class="place-order",
      eligible="true"
    }[$window])
  ),
  1
)
```

`unknown` 在核对前不进入 good，所以会消耗预算。后续事件若核对为 committed
once，可通过事件管道作有审计的最终分类；不能在 dashboard 手工改值。

`clamp_min` 只防除零，不解决 telemetry missing。no traffic、counter absent 和
ingestion broken 要由独立 freshness/metamonitoring 判断。

### latency query

histogram 需要 `le="0.25"` bucket：

```promql
1 -
sum(
  rate(pg36_shop_request_duration_seconds_bucket{
    service="pg36_shop",
    operation_class="place-order",
    eligible="true",
    availability_good="true",
    le="0.25"
  }[$window])
)
/
clamp_min(
  sum(
    rate(pg36_shop_request_duration_seconds_count{
      service="pg36_shop",
      operation_class="place-order",
      eligible="true"
    }[$window])
  ),
  1
)
```

这里示意把快速失败留在 denominator 而不进入 good bucket。实际 instrumentation
也可以统一用 outcome counter 与 duration histogram 通过 recording rule 对齐，
但必须有自动测试证明：

```text
eligible count in availability
  == eligible count in latency
```

否则两个 SLO 使用不同分母，会产生无法解释的预算。

### freshness query

每个 probe：

```text
write token
commit reconciled
poll declared read path
within_bound = true/false
read_path = replica|primary|application
```

bad ratio：

```promql
1 -
sum(
  rate(pg36_shop_commit_visibility_probes_total{
    service="pg36_shop",
    eligible="true",
    within_bound="true"
  }[$window])
)
/
clamp_min(
  sum(
    rate(pg36_shop_commit_visibility_probes_total{
      service="pg36_shop",
      eligible="true"
    }[$window])
  ),
  1
)
```

probe absence：

```text
unknown + probe-pipeline failure
```

不能用 `pg_last_xact_replay_timestamp()` 填补，因为它没有对应当前 commit。

### correctness query

```promql
max(
  pg36_shop_reconciliation_mismatches{
    service="pg36_shop"
  }
)
```

需要配套：

- `last_success_timestamp`；
- reconciliation input boundary；
- rule/query version；
- invariant；
- result hash。

只导出 `0` 而不监控 job freshness，会在核对任务停摆后永远显示“零差异”。

missing semantics：

```text
stale or absent reconciliation = control failure
```

### restore evidence age

```promql
max(
  pg36_shop_restore_evidence_age_seconds{
    service="pg36_shop",
    recovery_class="named-pitr"
  }
)
```

阈值 90 天：

```text
<= 7,776,000 seconds   control current
>  7,776,000 seconds   evidence stale
absent                 failed control, not infinite freshness
```

导出器必须只接受验证通过且完整性 hash 正确的 restore manifest。不能因为目录中
有一个新文件，就把 evidence age 归零。

### PostgreSQL statistics 用于解释原因

PostgreSQL 18 提供：

- `pg_stat_activity`：backend、状态、query/transaction 时间；
- `pg_stat_database`：transaction、block、tuple 与 session 统计；
- `pg_stat_replication`：sender、state、LSN 与同步状态；
- `pg_stat_wal_receiver`：standby receiver；
- `pg_stat_archiver`：归档成功/失败；
- `pg_stat_io`：按 backend/object/context 的 I/O；
- `pg_stat_ssl`：连接 TLS；
- lock 与 progress views。

完整目录见
[PostgreSQL Monitoring Database Activity](https://www.postgresql.org/docs/18/monitoring.html)。

这些视图回答：

```text
what PostgreSQL is doing
```

不直接回答：

```text
whether pg36_shop users completed place-order correctly
```

访问统计应使用 `pg_monitor` 等受控预定义角色或更窄授权，而不是让 exporter
成为 superuser。预定义角色边界见
[Predefined Roles](https://www.postgresql.org/docs/18/predefined-roles.html)。

### Pigsty v4.5 的实现层

当前 Pigsty 监控栈：

| 组件 | 职责 |
|---|---|
| VictoriaMetrics | 时序 ingestion、storage、query |
| VictoriaLogs | 结构化日志 |
| VMAlert | 规则评估 |
| Alertmanager | 聚合、抑制、路由、通知 |
| Grafana | dashboard 与调查入口 |
| exporter/agent | 暴露数据库、主机和组件事实 |

Pigsty v4 已从旧的 Prometheus/Loki 存储迁到 VictoriaMetrics/VictoriaLogs；
VMAlert 仍使用 PromQL-compatible 规则。不要复制旧文档后声称 v4 仍以
Prometheus server 存储时序。当前架构见
[Monitoring System](https://pigsty.io/docs/concept/monitor/)。

PostgreSQL、PgBouncer、host、load balancer 尽量通过：

```text
cls   cluster identity
ins   instance identity
ip    address identity
```

关联，详见 [PGSQL Monitoring](https://pigsty.io/docs/pgsql/monitor/)。

### 维度：服务与组件分别建模

应用 SLI：

```text
service
operation_class
environment
region/cell
outcome class
synthetic
```

组件 telemetry：

```text
cls
ins
ip
database
user/role (bounded and privacy reviewed)
```

连接方式：

```text
service catalog:
  pg36_shop -> pg-test

metric correlation:
  service="pg36_shop"
  dependency_cls="pg-test"
```

不要强行把 `service` 填成 `ins`，也不要把所有 database metric 复制一份
customer label。

### cardinality 是可靠性和隐私问题

禁止 label：

- `customer_id`；
- `tenant_id`；
- `order_id`；
- raw SQL；
- error message；
- stack trace。

原因：

- series 数无界增长；
- query 和 rule 变慢；
- monitoring storage 自身失稳；
- 用户数据进入广泛可见系统；
- label 变化让 aggregation 不可靠。

使用：

- bounded error class；
- normalized SQL fingerprint；
- exemplar/correlation token；
- 受控日志查明具体事件。

### missing-data truth table

| traffic/probe | SLI series | 结论 |
|---|---|---|
| 有预期流量 | 存在 | 计算 SLI |
| 有预期流量 | 缺失 | observation failure |
| 无真实流量 | synthetic 存在 | limited synthetic evidence |
| 无真实流量 | synthetic 缺失 | unknown |
| exporter 存在 | app SLI 缺失 | component observable, service unknown |
| app SLI 存在 | Alertmanager canary 失败 | health known, notification path broken |

“没有错误 series”与“error counter value is zero”不是一回事。

## 24.4.2 告警必须绑定用户影响、首个安全动作和所有者 {#item-24-4-2}

### page 的门槛

Prometheus 官方 alerting practices 总结为：

- 保持简单；
- 对症状告警；
- 用良好 console 定位原因；
- 避免无事可做的 page。

见 [Alerting practices](https://prometheus.io/docs/practices/alerting/)。

本章把 page 定义为：

```text
urgent
important
actionable
real
owned
```

缺一项就应考虑 dashboard、ticket 或删除。

### 一个 alert contract

每个 accepted alert 至少包含：

```text
id
class
objective
expression
long/short window
severity and route
for
user impact
owner
route id
runbook
first safe action
verification
dashboard
missing semantics
silence policy
test
review expiry
```

这比：

```yaml
alert: DatabaseHighCPU
expr: cpu > 80
severity: critical
```

多出来的内容，正是值班可行动性的来源。

### user impact 要具体

差：

```text
database is unhealthy
```

好：

```text
eligible order attempts are failing or unreconciled fast enough
to spend 2% of the 28-day availability budget in one hour
```

差：

```text
replica lag high
```

好：

```text
commit-correlated reads on the declared replica path miss
the five-second visibility bound
```

### first safe action 不是最终修复

fast-burn availability page：

```text
first safe action:
  freeze latest risky release
  reconcile unknown order outcomes before retrying writes
```

它没有假设根因。之后 Runbook 才检查应用、入口、pool、PostgreSQL、锁和拓扑。

correctness page：

```text
freeze affected writes
preserve reconciliation boundary before repair
```

不能先运行“修复 SQL”，否则会覆盖事故证据。

metamonitoring page：

```text
establish an independent blackbox view
before changing the monitored database
```

监控断了时先恢复观察，不要凭空重启数据库。

### owner 必须有 route

`owner_function=service` 仍不够。规则要带：

```text
route_id
schedule
escalation
notification grouping
repeat interval
```

route 要用 canary 验证：

```text
synthetic alert
  -> rule evaluator
      -> Alertmanager
          -> receiver
              -> acknowledgement record
```

不能只看 Alertmanager 进程 up。

### `for` 防短暂抖动，但会增加检测延迟

Prometheus/VMAlert 风格规则可以使用：

```yaml
for: 2m
```

条件必须持续 2 分钟才 firing。它适合滤除短 blip，但不是越大越好：

```text
evaluation interval
+ query window behavior
+ for
+ notification delay
+ human acknowledgement
= practical detection time
```

correctness mismatch 可以 `for: 0m`，因为单个已确认 mismatch 的后果不同。

当前 Prometheus 规则语义还支持 `keep_firing_for`，用于条件短暂消失后继续
firing 一段时间；使用前要确认 VMAlert 当前版本的兼容和行为，并通过 rule
test 验证。官方字段见
[Alerting rules](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/)。

### silence 不是关闭问题

silence 必须有：

- incident/change id；
- owner；
- reason；
- exact matcher；
- start/end；
- replacement observation；
- review。

禁止：

```text
silence service=* severity=critical for 30 days
```

计划维护也应尽量使用 route/inhibition 和用户 SLO 的明确政策，而不是让所有
信号消失。

### verification 决定何时恢复

fast-burn 恢复：

```text
long and short windows recover
AND sampled idempotency tokens reconcile
AND no correctness control fails
```

只看到 alert `resolved` 可能是：

- series 消失；
- label 改变；
- rule reload 失败；
- traffic 归零；
- silence；
- query error。

Runbook 必须检查 missing semantics 和用户结果。

### alert 要有生命周期

规则不是写完永存：

```text
owner
created
last tested
last fired
false-positive review
runbook validity
expiry/review date
replacement/deprecation
```

orphaned route 或过期 runbook 应使 CI/治理 review 失败。

## 24.4.3 症状告警、原因告警与容量预测分开 {#item-24-4-3}

### 症状 page

症状直接表示用户或数据后果：

- availability fast burn；
- latency fast burn；
- commit-correlated freshness violation；
- correctness mismatch；
- imminent durability loss；
- observation path loss 导致服务状态不可知。

它回答：

```text
why wake a human now?
```

### 原因 telemetry

原因帮助定位：

- CPU；
- disk latency；
- lock waits；
- pool queue；
- replica WAL distance；
- cache hit ratio；
- autovacuum backlog；
- connection count；
- Patroni member state。

同一用户症状可能有多个原因；同一原因也可能被 redundancy 吸收而没有用户
影响。若每个原因都 page：

```text
one incident
  -> CPU page
  -> lock page
  -> pool page
  -> latency page
  -> replica page
```

值班收到五个 notification，却没有更多信息。

原因应进入：

- linked dashboard；
- diagnostic annotation；
- bounded ticket；
- automatic enrichment。

### 两个允许越过“用户症状”的例外

#### 完整性/保密性

一条跨租户数据或 confirmed corruption 即使用户尚未报告，也需要立即处理。

#### 迫近耐久性

例如所有可恢复副本/备份路径都失效且继续运行会导致不可恢复数据风险。此时
page 的依据是已定义的 durability control，不是随意的 component threshold。

例外仍要 owner、Runbook、action 和 verification。

### 容量是有期限的 ticket

容量预测：

```text
forecast_days_to_capacity < lead_time + safety_margin
```

通常不是当前 user incident，因此：

- 创建 owned ticket；
- 带 forecast confidence；
- 检查 demand/query mix/retention；
- 给 due date；
- 不在凌晨 page。

若容量已经造成 latency/availability，则症状 SLO 会 page；capacity facts 作为
诊断。

本章候选：

```text
PG36ShopCapacityHorizon
  route ticket
  forecast horizon 14 days
  first action validate demand/headroom/retention/query mix
```

### metamonitoring

监控系统也会失败：

- exporter 停止；
- agent 无法发送；
- VictoriaMetrics ingestion/query 失败；
- VMAlert rule evaluation error；
- Alertmanager route 失败；
- receiver 不可达；
- dashboard query 误导。

有效 metamonitoring：

```text
whitebox
  ingestion errors / rule errors / queue

blackbox
  expected series freshness
  external service probe
  notification canary end-to-end
```

Prometheus alerting practices 也建议用贯穿 PushGateway/Prometheus/
Alertmanager/email 的 blackbox 测试，而不是只盯每个组件进程。Pigsty v4 中要
把这个思想映射到 VictoriaMetrics/VMAlert/Alertmanager。

### 分类矩阵

| signal | class | route | 例子 |
|---|---|---|---|
| user event bad ratio | symptom | page/ticket by burn | availability |
| data invariant mismatch | integrity | page | duplicate/cross-tenant |
| WAL distance | cause | dashboard | replication diagnosis |
| pool queue | cause | dashboard | latency diagnosis |
| CPU | cause | dashboard/ticket | capacity/diagnosis |
| days-to-full | capacity | ticket | disk/storage |
| notification canary overdue | metamonitoring | page | blind operation |

### 本章明确拒绝的 page

候选：

```text
PostgresInstanceDownWithoutAction
```

被拒绝，因为：

- 一台 instance 可因 offline maintenance 合理下线；
- redundancy 可能仍满足服务；
- 没有声明用户影响；
- 没有 owner；
- 没有 Runbook；
- 没有第一安全动作。

替代：

```text
retain instance state on topology dashboard
correlate endpoint/user-event symptoms
escalate through redundancy/durability policy if needed
```

validator 会故意把它的 `decision` 改成 `accepted`，并确认合同拒绝。

## 24.4.4 产出供 ch25 实现的告警规则清单 {#item-24-4-4}

### 七个 accepted candidates

#### 1. `PG36ShopAvailabilityFastBurn`

```text
class       symptom
route       page / SEV-1
windows     1h + 5m
burn        14.4x
for         2m
action      freeze latest risky release;
            reconcile unknown writes before retry
```

对 99.9%：

$$
14.4 \times (1-0.999)=0.0144
$$

即两个窗口 bad ratio 都高于 1.44%。

#### 2. `PG36ShopAvailabilitySlowBurn`

```text
route       page / SEV-2
windows     6h + 30m
burn        6x
for         5m
action      stop concurrent risky changes;
            segment by operation and release
```

阈值 0.6% bad ratio。

#### 3. `PG36ShopAvailabilityBudgetTicket`

```text
route       ticket
windows     3d + 6h
burn        1x
for         15m
action      open owned budget review
```

用于慢性消耗，不打扰夜间值班。

#### 4. `PG36ShopCorrectnessMismatch`

```text
class       integrity
route       page / SEV-1
condition   mismatch > 0
for         0m after confirmed reconciliation
action      freeze affected writes;
            preserve boundary
```

不使用 burn rate。

#### 5. `PG36ShopFreshnessFastBurn`

```text
class       symptom
route       page / SEV-2
windows     1h + 5m
burn        14.4x against 99% freshness target
action      route affected read-after-write journey to primary
            while preserving probe tokens
```

不要把所有 read traffic 永久移到 primary；这是有界安全动作，恢复后再评审
read policy。

#### 6. `PG36ShopCapacityHorizon`

```text
class       capacity
route       ticket
condition   reviewed forecast < 14 days
action      validate demand/headroom/retention/query mix
```

预测没有足够历史时，创建 evidence-quality ticket，而不是编造 forecast。

#### 7. `PG36MonitoringPathBroken`

```text
class       metamonitoring
route       page / SEV-2
condition   expected probe missing
            OR rule evaluation failure
            OR notification canary overdue
action      establish independent blackbox view first
```

### rule skeleton

第 25 章可从下面开始，但必须使用 recording rules 和测试后的真实 label：

```yaml
groups:
  - name: pg36-shop-slo
    rules:
      - alert: PG36ShopAvailabilityFastBurn
        expr: |
          (
            pg36_shop:sli_availability_bad_ratio:rate1h
              > 14.4 * (1 - 0.999)
          )
          and
          (
            pg36_shop:sli_availability_bad_ratio:rate5m
              > 14.4 * (1 - 0.999)
          )
        for: 2m
        labels:
          severity: sev1
          service: pg36_shop
          class: symptom
        annotations:
          summary: "pg36_shop availability burns fast"
          runbook: "runbook://RB-USER-SYMPTOM"
          dashboard: "dashboard://pg36-shop-slo"
```

这段只是规则语义骨架，不能直接部署，因为：

- recording rules 尚未实现；
- production environment/region labels 尚未固定；
- route 与 receiver 仍是教学标识；
- real owner 尚未批准；
- rule evaluation interval 尚未纳入检测时间；
- test fixture 尚未建立。

### 第 25 章必须交付什么

#### Instrumentation

- counters/histograms 的真实采集；
- eligible/good classifier tests；
- commit token probe；
- reconciliation freshness；
- restore evidence exporter；
- bounded labels。

#### Recording rules

- 5m、30m、1h、6h、3d 等窗口；
- label-preserving aggregation；
- reset 和 counter semantics；
- no-traffic/missing 分支；
- rule unit tests。

#### Alert rules

- 七个 accepted candidates；
- actionless candidate 保持 rejected；
- `for` 和 evaluation delay；
- route/inhibition/silence；
- review expiry。

#### Dashboards

- SLI、target、budget remaining；
- numerator/denominator；
- eligible classification；
- recent changes；
- component cause drill-down；
- missing telemetry state。

#### Delivery tests

- synthetic rule firing；
- label/annotation assertions；
- Alertmanager grouping；
- notification canary；
- acknowledgement；
- resolved 与 missing 的区分。

### 交接门槛

在实现规则前逐项确认：

- [ ] objective id 与 SLO policy 一致；
- [ ] metric status 从“待实现”变更有 evidence；
- [ ] good/total selector 经过代码测试；
- [ ] histogram 有目标 bucket；
- [ ] unknown outcome 不会被算 good；
- [ ] missing series 有独立规则；
- [ ] labels 有界且不泄露数据；
- [ ] Pigsty identity 使用 `cls/ins/ip`；
- [ ] component facts 不冒充 user SLI；
- [ ] multiwindow 使用 AND；
- [ ] page 有 owner、route、Runbook、action 和 verification；
- [ ] capacity 进入 ticket；
- [ ] cause 进入 dashboard；
- [ ] correctness/durability 例外有独立 control；
- [ ] notification path 自身被测试；
- [ ] real pager 测试获得明确授权；
- [ ] sandbox 规则没有被称为 production-approved。

完整输入见：

- [`observation-contract.json`](/labs/ch24/observation-contract.json)
- [`alert-candidates.json`](/labs/ch24/alert-candidates.json)

下一节处理这些指标、变更和操作留下的证据：证据必须足够证明决定，却不能把
秘密和个人数据无边界地复制到治理系统。

---

[上一节：SOP、Runbook 与变更治理](../03/) · [返回本章目录](../) · [下一节：证据、审计与合规](../05/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
