25.1 从问题选择可观测信号
先选指标,再问它能说明什么,是监控系统膨胀的主要原因。
更可靠的顺序是:
例如,“副本 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:
这些定义直接决定分子和分母:
$$ \text{availability bad ratio}
\frac{\text{failed or unreconciled eligible attempts}} {\text{all eligible attempts}} $$
如果只收 HTTP 2xx/5xx,会漏掉:
- 返回 200,但事务后来失败;
- 客户端超时,但写入已经提交;
- 重试创建了两个订单;
- 响应成功,但订单属于错误租户;
- primary 写入成功,随后从副本读取不到;
- 应用在 admission 前正确拒绝无效请求。
所以用户层信号常常要组合两个测量点:
eligible event 必须先定义
没有 eligible 分母,错误率会随着流量分类任意变化。例如:
“用户断开”是否计入也不能临场决定:
正确性不是可用性的一部分
如果 10,000,000 个订单中有一行串租:
因此正确性使用 control signal:
它需要:
invariant是有界枚举;- reconciliation 有输入边界;
- 结果有 hash 或不可变运行记录;
- stale/missing 本身是控制失败;
- 修复后由独立核对确认,而不是把 gauge 手工设为零。
第二层:请求在哪个入口发生了什么
入口层回答的是路径问题:
每个入口有不同的拒绝、排队和重试语义:
| 入口 | 关键问题 | 典型证据 |
|---|---|---|
| application edge | 请求是否 admission、是否重试、最终 outcome | request counter、duration histogram、release id |
| HAProxy | 选择了哪个 backend、健康检查如何判断 | backend/session/queue、routing event |
| PgBouncer | 等待 server connection 还是正在执行 | client/server/pool state、wait duration |
| PostgreSQL | backend 在执行、等待还是 idle in transaction | activity、wait event、locks |
“连接数高”可能代表:
如果没有入口身份与状态,单个总数无法区分。
用 operation class,避免用 endpoint 爆炸
应用指标需要能分段,但不能把完整 URL 做标签:
operation class 是有界业务语义;原始 path 可能包含 tenant、order 和 token, 既造成 cardinality 爆炸,也造成数据泄露。
第三层:PostgreSQL 能否解释用户症状
数据库层分成当前状态与累计事实:
它们回答:
- 请求是否在 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 必须分开
下面两个事实可以同时成立:
前者是“现在没有”,后者是“窗口内曾经发生”。不能因为 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 语义交叉:
主机指标很适合 falsify 假设。例如,若 PostgreSQL 认为 I/O 时间增加,而设备 层没有对应变化,可能是 OS cache、采集时间窗、虚拟化层或统计口径不同,而不是 直接得出“磁盘坏了”。
四层不是固定下钻顺序
通常从用户症状向下,但也有例外:
正确原则不是“永远只看用户”,而是:
原因和容量信号默认进入 diagnostic 或 ticket。
建立问题卡
每个新信号先填一张问题卡:
若填不出 decision、missing 和 action,这个信号还不适合变成告警。
反例:从副本时间戳推用户新鲜度
常见快捷方式:
在一个空闲副本上,最近没有 WAL,它可能显示“很久以前”;但副本其实完全 追平。持续写入时,它又只能说明最后一次 replay 的时间,不知道用户关心的 commit 是否已经可见。
新鲜度需要:
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 |
| histogram | bucket counter + count/sum | request duration | bucket 不一致、聚合错误 |
| summary | 客户端计算 quantile | 某些应用延迟 | quantile 难以跨实例聚合 |
counter 要转成窗口
不要:
除非语义明确要求“生命周期内从未失败”。大多数运行告警关心的是新失败与当前 推进,而不是历史存在。
gauge 需要稳定条件
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 的最小字段
没有 type,就不知道能否 rate();没有 reset,就不知道突然下降是改善还是
重启;没有 missing,就可能把 absence 变成健康。
log:保存离散上下文
日志适合回答:
- 哪个错误类别发生;
- 状态何时迁移;
- lock wait 在
deadlock_timeout后是否被记录; - 哪个 temporary file 被创建;
- Patroni 何时改变角色;
- pgBackRest 哪一步失败;
- 配置 reload 或连接认证发生什么。
日志不适合作为默认总体统计:
因为:
- 日志行不等于 eligible event;
- multiline 和格式变化影响计数;
- sampling 会丢事件;
- rotation/retention 改变分母;
- 查询成本随数据量增长;
- 原始 SQL、参数和用户信息可能泄露。
structured log 仍然需要 schema
推荐将字段分为:
restricted payload 不应自动复制到 alert annotation、ticket 或公开 evidence。 “JSON 格式”只解决解析,不解决敏感性。
event:把变化放进时间线
发布、切换、配置和容量变化最好是结构化 event:
它能帮助回答:
event 本身仍不证明因果。一个发布接近事故,只是强候选,需要机制和修复验证。
event 的 clock 与 identity
事件至少记录:
- stable event id;
- actor role,而不是随意字符串;
- exact target;
- UTC 时间和时间源;
- request、approval、execution、result;
- rollback/roll-forward reference;
- source integrity。
若主机时钟相差两分钟,所谓“先发生”会被颠倒。诊断包要同时保存:
trace:跟随单次路径
trace 能展示:
它适合:
- 一次请求在哪里耗时;
- 重试和 fan-out 如何展开;
- 哪个 dependency 返回错误;
- sampled slow path 与正常 path 有何不同。
它不适合单独算完整 SLO:
- 采样意味着不是全部事件;
- tail sampling 会改变总体;
- trace backend 丢失不能解释为“无慢请求”;
- baggage 可能携带 tenant、token 或 PII;
- 数据库 span 未必包含真实执行等待。
不要把 SQL 文本塞进 span
推荐:
谨慎或禁止:
queryid 也不是绝对安全身份:它是 hash,可能冲突;相同文本在不同
search_path 下语义也可能不同。它的价值是聚合和关联,不是授权或数据分类。
四种信号如何组合
一个 latency fast burn 的调查:
这组证据支持“入口池排队”而不是“数据库执行变慢”。如果回滚池配置后:
- queue 恢复;
- trace pool wait 恢复;
- user latency windows 恢复;
- 没有 correctness mismatch;
因果证据才明显增强。
选择最便宜的充分信号
同一问题可能有多种来源:
如果只需要总体 rate,优先 counter;若要解释一类错误,再查询有界日志;若要 追单次跨服务路径,再使用 trace。不要因为存储便宜就永久收集一切。
25.1.3 标签基数、采样、保留与缺失数据
cardinality 是维度乘积
一个 metric 的 series 数近似为:
假设:
则:
如果再加:
系统不仅会爆炸,还会把业务标识复制到监控存储。
bounded label allowlist
本章允许:
禁止:
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 也不能放 秘密。一个安全模式:
不要:
先测 cardinality,再加维度
增加 label 前回答:
- 值域是否有硬上限?
- 谁控制它?
- 每个值保留多久?
- query 与 dashboard 是否真实使用?
- alert 是否需要它来路由?
- 是否能在日志/trace 中按需查,而不进入 metric?
- 该字段是否是 PII、secret 或业务主键?
若只是“以后可能有用”,默认不加。
sampling 必须改变措辞
日志或 trace 被采样后,允许说:
不允许说:
采样合同至少包括:
- head、tail 或 probabilistic;
- sample rate;
- error 是否强制保留;
- rate 随流量是否变化;
- dropped count;
- decision point;
- 是否能重建总体;
- 变更历史。
PostgreSQL log_min_duration_sample 与 log_statement_sample_rate 也是采样政策。
如果前者关闭,后者值为 1 并不代表所有语句都会被采样记录。
retention 决定能否回答窗口问题
若 SLO 是 rolling 28d,而原始 SLI 只保留 7d:
可以使用长期 recording rule,但必须记录:
- 原始数据保留多久;
- 聚合数据保留多久;
- rule expression 的版本;
- label 是否在聚合时丢失;
- reset/缺口如何进入结果;
- 回填是否发生。
诊断与治理保留期也不同:
| 数据 | 典型目的 | 关注点 |
|---|---|---|
| 高频 raw metric | 近期诊断 | 容量大、粒度高 |
| recording rule | 长期趋势/SLO | 语义版本 |
| log body | 事件上下文 | 敏感、访问、删除 |
| trace | sampled path | 成本与 PII |
| incident evidence | 决策与复盘 | 完整性、最小化 |
“永久保存以备万一”通常同时违反成本和隐私原则。
缺失数据至少有七种解释
还有 counter reset、staleness marker 和查询窗口不足。
absent() 不是万能答案
判断 SLI missing 需要一个独立期望:
如果服务夜间没有流量,request counter 没新样本不一定故障。可以用:
- 独立 synthetic probe;
- admission counter;
- deployment/inventory declaration;
- scrape target freshness;
- expected schedule。
关键是不能用被监控对象自身同时证明“应该有数据”和“数据存在”。
zero、empty、NaN、stale 与 error
| 结果 | 含义 |
|---|---|
0 |
series 存在,当前值或计算结果为零 |
| empty vector | selector 没匹配 series |
NaN |
运算未定义,例如 0/0 |
| stale | 时序被标记过期 |
| query error | 规则没有获得结果 |
把 empty vector 用 or vector(0) 填零可能很危险:
如果 bad ratio 消失是 exporter 故障,这会把“未知”变成“100% 健康”。只有在 零值语义、identity join 与独立 freshness 都被证明时,才能安全补零。
低流量与分母
本章记录规则没有用一个任意常数把分母抬高:
低流量时要显式决策:
- 使用更长窗口;
- 同时要求 minimum event count;
- 使用 synthetic probe;
- 保持 unknown;
- 用 ticket 而不是 page;
- 聚合到更稳定的 operation class。
若写:
每秒不到一个请求时,会改变真实 event ratio。clamp_min 可以防数值问题,
不能免费替代低流量政策。
counter reset
对 counter 使用 rate()/increase() 可以处理正常 reset,但仍要观察:
- reset 是否过于频繁;
- target identity 是否变化;
- scrape window 是否跨越长缺口;
- process restart 是否是事故的一部分;
- 历史 recording rule 是否有断点。
PostgreSQL 统计也有 reset:
数值突然变小,必须先问 reset,而不是直接说“负载下降”。
监控系统也要被监控
至少观察:
- exporter/scrape freshness;
- VictoriaMetrics query 与 ingestion;
- VMAlert group/rule error;
- missed evaluation;
- Alertmanager notification failure;
- 独立 canary 的 receipt;
- external blackbox。
本章现场快照显示:
这只能证明计数器当前没有记录失败。没有一条最近被 receiver 确认收到的 canary, 不能宣称真实通知链可达。
current lab cardinality snapshot
同一快照中:
这些是容量基线,不是目标上限。应当记录随时间的:
一个新 exporter 可能“工作正常”,但在一周内把 series 增长十倍。
信号引入评审
上线前用下面的表:
| 问题 | 必须通过 |
|---|---|
| question | 能写成一个可证伪问题 |
| decision | 观察结果会改变具体决定 |
| source | producer 和采集路径明确 |
| type | counter/gauge/histogram/event/log/trace 明确 |
| labels | 有界、必要、无 secret/PII |
| time | scrape、window、delay、reset 明确 |
| missing | 不会默认为健康 |
| cost | series、bytes、query、retention 有预算 |
| access | 最小权限与脱敏明确 |
| action | owner、首个安全动作、停止线明确 |
| test | 正常、异常、缺失、恢复均可重放 |
| retirement | rename/deprecation 有迁移方案 |
本节验收
你应当能够对任意一个候选指标回答:
如果只能回答“Grafana 上有这条线”,信号合同还没有建立。
返回本章目录 · 下一节:PostgreSQL 核心运行信号 · 查看全书目录 · 查看索引中心