跳至内容
25.1 从问题选择可观测信号

25.1 从问题选择可观测信号

先选指标,再问它能说明什么,是监控系统膨胀的主要原因。

有 PostgreSQL
  -> 收集所有 pg_* 指标
      -> 导入所有 dashboard
          -> 给红色曲线加阈值
              -> 事故时再猜它们与用户有什么关系

更可靠的顺序是:

decision
  -> question
      -> evidence
          -> semantics
              -> collection cost
                  -> action and verification

例如,“副本 lag 多大”不是一个完整问题。它可能对应三种完全不同的决定:

决定真正问题需要的信号
是否把 read-after-write 流量路由到副本已知 commit 是否在该读取路径可见commit token probe
是否有 WAL 保留风险primary 与 replica 的 LSN 距离是否持续扩大replication position + WAL retention
是否解释查询结果陈旧用户查询走了哪条路径、返回哪个版本routing event + application semantics

一个 pg_lag 无法同时替代这三个答案。

本节建立一份问题驱动的信号合同。可执行版本见 signal-contract.json

25.1.1 用户体验、服务入口、数据库与主机四层

第一层:用户是否获得了正确服务

用户层不是浏览器 RUM 的同义词,而是最接近服务承诺的测量点。对 pg36_shop

journey       place-order
eligible      authorized and valid request admitted by the app
good          success returned and token reconciles to one committed order
timely        final result within 250 ms
fresh         committed token visible on declared read path within 5 s
correct       no unexplained invariant mismatch

这些定义直接决定分子和分母:

$$ \text{availability bad ratio}

\frac{\text{failed or unreconciled eligible attempts}} {\text{all eligible attempts}} $$

如果只收 HTTP 2xx/5xx,会漏掉:

  • 返回 200,但事务后来失败;
  • 客户端超时,但写入已经提交;
  • 重试创建了两个订单;
  • 响应成功,但订单属于错误租户;
  • primary 写入成功,随后从副本读取不到;
  • 应用在 admission 前正确拒绝无效请求。

所以用户层信号常常要组合两个测量点:

application edge outcome
  +
commit outcome / idempotency reconciliation

eligible event 必须先定义

没有 eligible 分母,错误率会随着流量分类任意变化。例如:

all inbound requests
  includes scanners, malformed requests, health checks, unauthorized calls

admitted order attempts
  excludes pre-admission syntax and authorization rejection
  includes server outcomes after admission

“用户断开”是否计入也不能临场决定:

disconnect before admission and no server outcome
  -> may be excluded by a predeclared rule

disconnect after transaction may have committed
  -> unknown outcome, must reconcile

正确性不是可用性的一部分

如果 10,000,000 个订单中有一行串租:

availability could still be 99.99999%
security and correctness are still failed

因此正确性使用 control signal:

pg36_shop_reconciliation_mismatches > 0

它需要:

  • invariant 是有界枚举;
  • reconciliation 有输入边界;
  • 结果有 hash 或不可变运行记录;
  • stale/missing 本身是控制失败;
  • 修复后由独立核对确认,而不是把 gauge 手工设为零。

第二层:请求在哪个入口发生了什么

入口层回答的是路径问题:

client
  -> application edge
      -> HAProxy service port
          -> PgBouncer pool
              -> PostgreSQL session

每个入口有不同的拒绝、排队和重试语义:

入口关键问题典型证据
application edge请求是否 admission、是否重试、最终 outcomerequest counter、duration histogram、release id
HAProxy选择了哪个 backend、健康检查如何判断backend/session/queue、routing event
PgBouncer等待 server connection 还是正在执行client/server/pool state、wait duration
PostgreSQLbackend 在执行、等待还是 idle in transactionactivity、wait event、locks

“连接数高”可能代表:

healthy concurrency
idle application connections
pool wait
session leak
long transaction
blocked query fan-out
maintenance workers

如果没有入口身份与状态,单个总数无法区分。

用 operation class,避免用 endpoint 爆炸

应用指标需要能分段,但不能把完整 URL 做标签:

good:
  operation_class="place-order"
  operation_class="read-order"
  operation_class="reconcile-order"

bad:
  path="/tenant/123/order/9a7..."

operation class 是有界业务语义;原始 path 可能包含 tenant、order 和 token, 既造成 cardinality 爆炸,也造成数据泄露。

第三层:PostgreSQL 能否解释用户症状

数据库层分成当前状态与累计事实:

current
  pg_stat_activity
  pg_locks
  progress views
  pg_stat_replication

cumulative
  pg_stat_database
  pg_stat_io
  pg_stat_wal
  pg_stat_checkpointer
  pg_stat_archiver
  pg_stat_statements

它们回答:

  • 请求是否在 PostgreSQL 内;
  • backend 在 CPU 上运行还是等待;
  • 等待是 lock、I/O、client、WAL、buffer pin 还是其他类别;
  • transaction 已持续多久;
  • 哪个 queryid 消耗时间、I/O、临时块或 WAL;
  • checkpoint 写入和同步成本如何变化;
  • WAL 生成、发送、归档和回放是否推进;
  • vacuum/freeze 是否被阻止;
  • dead tuple、对象大小和统计新鲜度如何变化。

它们不直接回答:

  • 用户请求是不是 eligible;
  • 应用是否返回了正确结果;
  • 哪个 tenant 受到影响;
  • 用户是否走了 replica read path;
  • 恢复点是否被业务接受。

数据库信号是解释层,不是服务层的替代品。

current 与 cumulative 必须分开

下面两个事实可以同时成立:

pg_stat_activity now shows no lock wait
pg_stat_database deadlocks counter increased in the last hour

前者是“现在没有”,后者是“窗口内曾经发生”。不能因为 current view 已经恢复, 就否定累计异常;也不能因为累计 counter 非零,就宣称当前仍在发生。

第四层:主机是否构成资源约束

主机层观察:

  • CPU utilization、run queue、steal;
  • memory pressure、swap、OOM;
  • filesystem capacity、inode、mount state;
  • block-device latency、queue、throughput;
  • network loss、retransmit、bandwidth;
  • clock synchronization;
  • process、cgroup、systemd 和 kernel event。

主机指标要与 PostgreSQL 语义交叉:

high CPU
  + PostgreSQL active non-waiting backends
  + queryid execution time rises
  -> consistent with compute saturation

high CPU
  + user SLI healthy
  + batch window declared
  -> may be expected work

disk latency rises
  + pg_stat_io read_time rises
  + shared reads rise
  -> storage path becomes a stronger candidate

disk latency rises
  + PG reads unchanged
  -> investigate other processes or filesystem activity

主机指标很适合 falsify 假设。例如,若 PostgreSQL 认为 I/O 时间增加,而设备 层没有对应变化,可能是 OS cache、采集时间窗、虚拟化层或统计口径不同,而不是 直接得出“磁盘坏了”。

四层不是固定下钻顺序

通常从用户症状向下,但也有例外:

durability control
  archive stopped before user impact
  -> page is justified by imminent durability loss

metamonitoring
  rule evaluation failed
  -> health becomes unknown
  -> establish independent observation first

freeze-age horizon
  no current impact
  -> ticket before emergency anti-wraparound work

正确原则不是“永远只看用户”,而是:

page requires current user impact,
integrity/durability emergency,
or loss of the observation path.

原因和容量信号默认进入 diagnostic 或 ticket。

建立问题卡

每个新信号先填一张问题卡:

question: 已知 commit 是否在 replica read path 五秒内可见?
decision: 是否临时把 read-after-write journey 路由到 primary?
layer: user
source: commit-correlated synthetic probe
good: token visible within 5s
bad: token not visible within 5s
missing: unknown and probe-path failure
dimensions: [service, read_path, environment]
fallback: direct primary-path probe
owner: service
first_safe_action: preserve tokens and use primary path
verification: new tokens meet bound on declared path

若填不出 decision、missing 和 action,这个信号还不适合变成告警。

反例:从副本时间戳推用户新鲜度

常见快捷方式:

clock_timestamp() - pg_last_xact_replay_timestamp()

在一个空闲副本上,最近没有 WAL,它可能显示“很久以前”;但副本其实完全 追平。持续写入时,它又只能说明最后一次 replay 的时间,不知道用户关心的 commit 是否已经可见。

新鲜度需要:

write unique token
capture commit outcome
poll declared read path
measure until token visible

WAL distance 和 replay timestamp 仍有诊断价值,但不能替代 commit correlation。

25.1.2 指标、日志、事件和追踪各回答什么

metric:把总体变成可计算时间序列

metric 最适合:

  • event ratio;
  • latency distribution;
  • rate、increase 和 trend;
  • 容量 horizon;
  • 规则自动评估;
  • 多实例聚合。

四种常见类型:

类型语义PostgreSQL/Pigsty 示例主要陷阱
counter只增,进程/reset 后重置transaction、deadlock、WAL byte直接比较总值
gauge可升可降的当前/最近值connections、lag、object size对瞬时噪声 page
histogrambucket counter + count/sumrequest durationbucket 不一致、聚合错误
summary客户端计算 quantile某些应用延迟quantile 难以跨实例聚合

counter 要转成窗口

rate(pg_db_xact_total[5m])
increase(pg_archiver_failed_count[15m])

不要:

pg_archiver_failed_count > 0

除非语义明确要求“生命周期内从未失败”。大多数运行告警关心的是新失败与当前 推进,而不是历史存在。

gauge 需要稳定条件

pg36_shop_restore_evidence_age_seconds > 90 * 24 * 60 * 60

evidence age 是 gauge,但它不是“数据库坏了”。它是恢复控制未满足,应进入 change gate 或 ticket,除非业务政策另有紧急定义。

histogram 要从 bucket 算事件比率

延迟 SLO 目标是 99% 在 250 ms 内:

$$ \text{latency bad ratio}

1 - \frac{\text{rate}(\text{bucket}_{le=0.25})} {\text{rate}(\text{count})} $$

它与 p99 不完全等价。event-based SLO 直接计算“多少 eligible events 超标”; quantile 是分布位置,适合探索,但不一定能直接算错误预算。

metric contract 的最小字段

name
type and unit
producer
labels and cardinality bound
counter reset or gauge freshness
scrape interval
retention
missing semantics
query examples
owner
deprecation plan

没有 type,就不知道能否 rate();没有 reset,就不知道突然下降是改善还是 重启;没有 missing,就可能把 absence 变成健康。

log:保存离散上下文

日志适合回答:

  • 哪个错误类别发生;
  • 状态何时迁移;
  • lock wait 在 deadlock_timeout 后是否被记录;
  • 哪个 temporary file 被创建;
  • Patroni 何时改变角色;
  • pgBackRest 哪一步失败;
  • 配置 reload 或连接认证发生什么。

日志不适合作为默认总体统计:

scan all PostgreSQL logs
count matching strings
divide by all lines

因为:

  • 日志行不等于 eligible event;
  • multiline 和格式变化影响计数;
  • sampling 会丢事件;
  • rotation/retention 改变分母;
  • 查询成本随数据量增长;
  • 原始 SQL、参数和用户信息可能泄露。

structured log 仍然需要 schema

推荐将字段分为:

stable dimensions
  cluster / instance / database / severity / error_code

bounded correlation
  release / operation_class / incident_id

restricted payload
  message / statement / detail / parameter

restricted payload 不应自动复制到 alert annotation、ticket 或公开 evidence。 “JSON 格式”只解决解析,不解决敏感性。

event:把变化放进时间线

发布、切换、配置和容量变化最好是结构化 event:

{
  "event_id": "change-20260729-017",
  "kind": "application-release",
  "target": "pg36_shop/l2-sandbox",
  "actor_role": "shop-release-operator",
  "started_at": "...",
  "completed_at": "...",
  "result": "success",
  "rollback_ref": "..."
}

它能帮助回答:

symptom started at 10:02
release completed at 09:58
pool config changed at 10:01
failover did not occur

event 本身仍不证明因果。一个发布接近事故,只是强候选,需要机制和修复验证。

event 的 clock 与 identity

事件至少记录:

  • stable event id;
  • actor role,而不是随意字符串;
  • exact target;
  • UTC 时间和时间源;
  • request、approval、execution、result;
  • rollback/roll-forward reference;
  • source integrity。

若主机时钟相差两分钟,所谓“先发生”会被颠倒。诊断包要同时保存:

collector clock
database clock
rule evaluation clock

trace:跟随单次路径

trace 能展示:

edge span
  -> service span
      -> pool wait
          -> database call
              -> downstream service

它适合:

  • 一次请求在哪里耗时;
  • 重试和 fan-out 如何展开;
  • 哪个 dependency 返回错误;
  • sampled slow path 与正常 path 有何不同。

它不适合单独算完整 SLO:

  • 采样意味着不是全部事件;
  • tail sampling 会改变总体;
  • trace backend 丢失不能解释为“无慢请求”;
  • baggage 可能携带 tenant、token 或 PII;
  • 数据库 span 未必包含真实执行等待。

不要把 SQL 文本塞进 span

推荐:

db.system=postgresql
db.namespace=test
db.operation.name=SELECT
db.query.summary=read-order
db.queryid=<bounded hash identity if policy allows>

谨慎或禁止:

db.statement=<raw SQL with literals>
bind.parameters=<customer data>
connection.string=<credential>

queryid 也不是绝对安全身份:它是 hash,可能冲突;相同文本在不同 search_path 下语义也可能不同。它的价值是聚合和关联,不是授权或数据分类。

四种信号如何组合

一个 latency fast burn 的调查:

metric
  availability healthy, latency bad ratio burns

event
  pool size changed four minutes before onset

trace
  sampled requests spend time waiting for a server connection

log
  PgBouncer reports pool saturation but no auth errors

SQL
  PostgreSQL active sessions and query cost are stable

host
  CPU and storage are stable

这组证据支持“入口池排队”而不是“数据库执行变慢”。如果回滚池配置后:

  • queue 恢复;
  • trace pool wait 恢复;
  • user latency windows 恢复;
  • 没有 correctness mismatch;

因果证据才明显增强。

选择最便宜的充分信号

同一问题可能有多种来源:

count transaction commits
  pg_stat_database counter        cheap, aggregate
  parse every log line            expensive, context-rich
  trace every transaction         very expensive, sampled

如果只需要总体 rate,优先 counter;若要解释一类错误,再查询有界日志;若要 追单次跨服务路径,再使用 trace。不要因为存储便宜就永久收集一切。

25.1.3 标签基数、采样、保留与缺失数据

cardinality 是维度乘积

一个 metric 的 series 数近似为:

$$ \text{series} \approx \prod_{i=1}^{n}\left|\text{label}_i\right| \times \text{histogram buckets} $$

假设:

service             20
environment         3
operation_class     30
status_class        6
release             10 retained concurrently
histogram bucket    15

则:

$$ 20 \times 3 \times 30 \times 6 \times 10 \times 15 = 1{,}620{,}000 $$

如果再加:

tenant_id  100,000
order_id   unbounded

系统不仅会爆炸,还会把业务标识复制到监控存储。

bounded label allowlist

本章允许:

Pigsty identity
  cls / ins / ip

service identity
  service / operation_class / environment

bounded optional
  read_path / invariant / recovery_class / objective_id / release

禁止:

customer_id / tenant_id / order_id / idempotency_token
trace_id / raw_sql / raw query text / error_message
client_addr / password / token

release 虽然有界,也要限制同时保留多少版本;滚动发布若不断产生唯一 commit SHA,而旧 series 长期不消失,仍会增长。

Pigsty 的 pg_query_* 指标有一个需特别说明的例外:label 名为 query,值是 数值型 queryid,而不是 SQL 文本。它的上限受 pg_stat_statements.max 约束; 仍要结合 database,并禁止把 raw query text 填进同名 label。

label 与 annotation 的边界

label 用于:

  • series identity;
  • query aggregation;
  • alert grouping/routing;
  • silence matcher。

annotation 用于人读的说明,不参与 series identity。但 annotation 也不能放 秘密。一个安全模式:

labels:
  service: pg36_shop
  operation_class: place-order
  environment: production
  objective_id: SLO-AVAILABILITY

annotations:
  summary: availability budget burning
  runbook: RB-USER-SYMPTOM
  dashboard: dashboard://pg36-shop-slo

不要:

labels:
  order_id: 8f...
  query: SELECT ...
annotations:
  error: password authentication failed for ...

先测 cardinality,再加维度

增加 label 前回答:

  1. 值域是否有硬上限?
  2. 谁控制它?
  3. 每个值保留多久?
  4. query 与 dashboard 是否真实使用?
  5. alert 是否需要它来路由?
  6. 是否能在日志/trace 中按需查,而不进入 metric?
  7. 该字段是否是 PII、secret 或业务主键?

若只是“以后可能有用”,默认不加。

sampling 必须改变措辞

日志或 trace 被采样后,允许说:

sampled slow requests show pool wait

不允许说:

all slow requests are caused by pool wait

采样合同至少包括:

  • head、tail 或 probabilistic;
  • sample rate;
  • error 是否强制保留;
  • rate 随流量是否变化;
  • dropped count;
  • decision point;
  • 是否能重建总体;
  • 变更历史。

PostgreSQL log_min_duration_samplelog_statement_sample_rate 也是采样政策。 如果前者关闭,后者值为 1 并不代表所有语句都会被采样记录。

retention 决定能否回答窗口问题

若 SLO 是 rolling 28d,而原始 SLI 只保留 7d:

cannot recompute 28d objective from raw data

可以使用长期 recording rule,但必须记录:

  • 原始数据保留多久;
  • 聚合数据保留多久;
  • rule expression 的版本;
  • label 是否在聚合时丢失;
  • reset/缺口如何进入结果;
  • 回填是否发生。

诊断与治理保留期也不同:

数据典型目的关注点
高频 raw metric近期诊断容量大、粒度高
recording rule长期趋势/SLO语义版本
log body事件上下文敏感、访问、删除
tracesampled path成本与 PII
incident evidence决策与复盘完整性、最小化

“永久保存以备万一”通常同时违反成本和隐私原则。

缺失数据至少有七种解释

1. 真实没有事件
2. producer 没有初始化零 series
3. exporter 失败
4. scrape/ingestion 延迟
5. storage/query 失败
6. label 或 metric rename
7. service / instance 已按计划退役

还有 counter reset、staleness marker 和查询窗口不足。

absent() 不是万能答案

判断 SLI missing 需要一个独立期望:

pg36_expected_service_traffic == 1
unless on (service, environment)
pg36_sli_sample_fresh == 1

如果服务夜间没有流量,request counter 没新样本不一定故障。可以用:

  • 独立 synthetic probe;
  • admission counter;
  • deployment/inventory declaration;
  • scrape target freshness;
  • expected schedule。

关键是不能用被监控对象自身同时证明“应该有数据”和“数据存在”。

zero、empty、NaN、stale 与 error

结果含义
0series 存在,当前值或计算结果为零
empty vectorselector 没匹配 series
NaN运算未定义,例如 0/0
stale时序被标记过期
query error规则没有获得结果

把 empty vector 用 or vector(0) 填零可能很危险:

bad_ratio or vector(0)

如果 bad ratio 消失是 exporter 故障,这会把“未知”变成“100% 健康”。只有在 零值语义、identity join 与独立 freshness 都被证明时,才能安全补零。

低流量与分母

本章记录规则没有用一个任意常数把分母抬高:

bad_rate / total_rate

低流量时要显式决策:

  • 使用更长窗口;
  • 同时要求 minimum event count;
  • 使用 synthetic probe;
  • 保持 unknown;
  • 用 ticket 而不是 page;
  • 聚合到更稳定的 operation class。

若写:

bad_rate / clamp_min(total_rate, 1)

每秒不到一个请求时,会改变真实 event ratio。clamp_min 可以防数值问题, 不能免费替代低流量政策。

counter reset

对 counter 使用 rate()/increase() 可以处理正常 reset,但仍要观察:

  • reset 是否过于频繁;
  • target identity 是否变化;
  • scrape window 是否跨越长缺口;
  • process restart 是否是事故的一部分;
  • 历史 recording rule 是否有断点。

PostgreSQL 统计也有 reset:

pg_stat_* stats_reset
pg_stat_statements_info.stats_reset
per-row stats_since
minmax_stats_since

数值突然变小,必须先问 reset,而不是直接说“负载下降”。

监控系统也要被监控

至少观察:

  • exporter/scrape freshness;
  • VictoriaMetrics query 与 ingestion;
  • VMAlert group/rule error;
  • missed evaluation;
  • Alertmanager notification failure;
  • 独立 canary 的 receipt;
  • external blackbox。

本章现场快照显示:

VMAlert rule errors                  0
VMAlert missed iterations nonzero   0
Alertmanager notification failures  0
current alerts                      0

这只能证明计数器当前没有记录失败。没有一条最近被 receiver 确认收到的 canary, 不能宣称真实通知链可达。

current lab cardinality snapshot

同一快照中:

VictoriaMetrics total series            44,842
total label-value pairs                 387,724
distinct metric names                   3,078
vmalert recording-rule series           698

这些是容量基线,不是目标上限。应当记录随时间的:

series growth rate
top metric by series
top label by values
unused/high-cost metric
query latency and cache pressure
retention impact

一个新 exporter 可能“工作正常”,但在一周内把 series 增长十倍。

信号引入评审

上线前用下面的表:

问题必须通过
question能写成一个可证伪问题
decision观察结果会改变具体决定
sourceproducer 和采集路径明确
typecounter/gauge/histogram/event/log/trace 明确
labels有界、必要、无 secret/PII
timescrape、window、delay、reset 明确
missing不会默认为健康
costseries、bytes、query、retention 有预算
access最小权限与脱敏明确
actionowner、首个安全动作、停止线明确
test正常、异常、缺失、恢复均可重放
retirementrename/deprecation 有迁移方案

本节验收

你应当能够对任意一个候选指标回答:

它服务于哪一个决定?
属于用户、入口、数据库还是主机层?
它是症状、原因、控制还是 metamonitoring?
数值是 current、counter、window、estimate 还是 progress?
哪些 label 有界,哪些绝对不能进入?
缺失、reset、NaN 与低流量分别怎么处理?
采集和保留成本是多少?
谁看到它后可以安全地做什么?
哪个独立事实可以复核?

如果只能回答“Grafana 上有这条线”,信号合同还没有建立。


返回本章目录 · 下一节:PostgreSQL 核心运行信号 · 查看全书目录 · 查看索引中心

最后更新于