指标、日志、追踪与连续剖析¶
可观测性不是“装了监控”,而是能否从系统输出的证据中回答内部状态问题。一个成熟系统既要能看到已知故障,也要能调查事先没有写进告警规则的问题;后者要求信号共享身份、时间与语义,而不只是堆积四套互不相通的数据。
本文以 OpenTelemetry Specification 1.59.0、OTLP 1.11.0、Semantic Conventions 1.43.0 和 W3C Trace Context Recommendation 为边界。OpenTelemetry 各语言 SDK、Collector component 与 semantic convention 的稳定性并不相同,使用前应锁定依赖版本并检查对应 stability 标记。
从问题选择信号¶
| 信号 | 最自然的问题 | 典型粒度 | 不擅长回答 |
|---|---|---|---|
| metric | 发生了多少、趋势是否异常 | 时间窗内聚合 | 某个请求经过了什么 |
| log | 某个离散事件携带了哪些字段 | event | 全局分布与调用因果 |
| trace | 一次事务跨组件如何传播 | request/transaction | 函数内部 CPU 热点 |
| profile | 资源消耗落在哪些调用栈 | stack sample | 业务语义与远端因果 |
它们的连接点比各自的数量更重要:
- metric exemplar 指向一个代表性 trace;
- span 带稳定的 service、operation 与 status 语义;
- log 带 trace/span ID,但不复制整份 span;
- profile sample 能关联 process、service、deployment,条件允许时再关联 trace;
- deployment、feature flag 与配置变更进入同一时间线。
只保留“CPU 80%”不足以调查:要知道哪个 workload、哪个 cgroup、哪版 binary、哪类 request,以及时间窗内是否发生频率、GC、队列或下游变化。
统一的数据身份¶
OpenTelemetry 的 telemetry entity 通常由三层描述:
- Resource:产生数据的实体,如 service、process、container、host;
- Instrumentation Scope:哪个 library/name/version 产生数据;
- Signal record:metric point、log record、span 或 profile sample。
Resource attribute 应在进程生命周期内相对稳定。高变化字段如 user.id、URL query、随机 job ID 不应混入 resource;否则同一服务被切成海量时间序列与存储分区。
命名要区分:
- 身份:
service.name、deployment environment; - 工作类别:route、RPC method、queue name;
- 单次实例:trace ID、request ID;
- 结果:status、error type;
- 测量值:duration、bytes、queue depth。
单次实例适合 log/trace,不适合 metric label。把 request ID 放进 counter attribute,会让每次请求创建一条新 time series。
Trace:跨边界保存因果¶
trace 由 span 构成。span 至少需要:
- trace ID 与 span ID;
- operation name;
- start/end timestamp;
- parent 或 link;
- attributes、events 与 status;
- resource 和 instrumentation scope。
Parent 与 Link¶
parent-child 表示一个主要因果树,例如 HTTP server span 调用 database client span。link 表示不能自然归入一棵树的关系:
- batch 从多个 message 聚合;
- fan-in job 依赖多个上游;
- queue consumer 与 producer 跨越较长时间;
- retry attempt 与原始 attempt;
- trace 被异步任务继续,但生命周期独立。
不要为了画成漂亮的树而伪造 parent。错误的因果结构会让 critical path、service graph 与 latency attribution 都失真。
W3C Trace Context¶
HTTP traceparent 的核心字段为:
接收方必须校验长度、十六进制格式、全零 ID 和版本规则,再创建本地 span。tracestate 承载 vendor-specific 状态;它不是任意 metadata 通道。
跨 trust boundary 时,传播上下文属于安全输入:
- 外部 trace ID 可保留连续性,但不能因此信任对方的采样或身份声明;
- baggage 可能被最终用户构造,不应装 secret、令牌或高敏个人信息;
- 限制 header 数量与长度,避免转发放大;
- 不把 trace attribute 自动复制到 metric label。
Span 边界¶
每个函数一个 span 会产生噪声和成本。优先为这些边界建 span:
- network/database/message broker 调用;
- queue 等待与异步 hand-off;
- 用户可感知的高层 operation;
- 独立 retry attempt;
- 跨 runtime、process 或权限域的工作。
进程内短函数的 CPU 分解通常交给 profile。span event 适合记录 operation 内少量关键转折,不适合逐行日志。
Metric:在聚合中保存语义¶
metric instrument 要匹配被测量的数学性质:
| Instrument/聚合 | 适合 | 常见错误 |
|---|---|---|
| counter/sum | requests、bytes、errors 的累计增量 | 表示可下降的 queue depth |
| up-down counter | active requests、allocated units | 当成跨进程全局唯一 gauge |
| gauge | 当前温度、队列深度、利用率快照 | 对每次事件都发一个随机 ID |
| histogram | duration、size 的分布 | 只保存平均数 |
Temporality¶
累计 temporality 从起点给总量;增量 temporality 给相邻 collection interval 的变化。转换时必须处理:
- process restart/reset;
- collector 丢失 interval;
- counter wrap 或错误下降;
- multiple writer;
- start timestamp。
rate 是对 sum 在时间上的派生,不是另一个随意累加的 counter。
Histogram 与尾部¶
平均延迟
无法区分“全部 100 ms”和“99% 10 ms、1% 9 s”。histogram 保存 bucket/count/sum,后端才能估 quantile。估计精度受 bucket boundary 影响;不同 boundary 的 histogram 不能在不定义重桶规则时直接合并。
percentile 也不可相加:
通常只有在强条件下才能用 convolution 或原始联合分布推导。端到端 trace 的 critical path 与服务侧 histogram 应互相校验。
Cardinality¶
若 metric 有 \(k\) 个 attributes,第 \(i\) 个有 \(c_i\) 个活跃值,最坏 time-series 数量近似:
实际组合未必全出现,但乘法增长足以制造事故。route template 比 raw URL 好;error class 比完整 message 好;tenant 只有在容量与访问控制允许时才适合作为维度。
Exemplar¶
exemplar 在一个聚合数据点旁保留代表性测量和 trace/span context。它让“p99 bucket 上升”可跳到一个真实慢请求,同时不把每个 trace ID 变成 label。exemplar 是抽样桥梁,不代表该 bucket 的所有事件。
Log:把事件写成可查询记录¶
结构化 log 的价值不只是 JSON,而是稳定 schema:
{
"timestamp": "2026-07-28T08:00:00.123Z",
"severity": "ERROR",
"service": "checkout",
"event": "payment_attempt_failed",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"error_type": "deadline_exceeded",
"attempt": 2,
"elapsed_ms": 803
}
应稳定的是 field 语义,不是 message 文案。建议:
event用低基数机器可读名字;- error 保留 type/code/stack,避免只写
failed; - time 用统一绝对时间,并在 duration 字段明确单位;
- request body、token、cookie、credential 默认不记录;
- 大 payload 写摘要、尺寸与受控引用,不全文复制;
- retry 每个 attempt 可记录,但最终 operation 只计算一次业务结果。
同一异常被每层 catch 后重复记成 ERROR 会造成告警放大。选择一个拥有处理责任的层记录主要错误,下层把细节附在 span event 或向上传递的 error chain。
标准库级关联示例¶
下面的 Go 代码只演示 context 中的 trace identity 如何进入结构化日志;真正的 span 创建与传播应由选定的 OpenTelemetry SDK 完成:
package main
import (
"context"
"encoding/json"
"os"
)
type traceKey struct{}
type event struct {
Name string `json:"event"`
TraceID string `json:"trace_id,omitempty"`
Value int `json:"value"`
}
func writeEvent(ctx context.Context, name string, value int) error {
e := event{Name: name, Value: value}
if id, ok := ctx.Value(traceKey{}).(string); ok {
e.TraceID = id
}
return json.NewEncoder(os.Stdout).Encode(e)
}
func main() {
ctx := context.WithValue(context.Background(), traceKey{}, "4bf92f3577b34da6a3ce929d0e0e4736")
_ = writeEvent(ctx, "items_processed", 7)
}
不要把 context 当成任意参数袋。这里只放 request-scoped identity;业务参数仍显式传递。
Profile:从请求回到调用栈¶
连续剖析定期采集 CPU、allocation、wall-clock 或 lock 等 stack profile。它补上 trace span 内部的黑盒:
- 同一个 RPC span 变慢,是 JSON decode、GC、lock 还是 kernel path;
- 新 deployment 的 CPU 增量落在哪个 frame;
- allocation rate 上升由哪条调用链造成;
- on-CPU 正常时,off-CPU stack 在等什么。
profile 的维度必须受控。按 service/version/region 聚合通常有价值;把每个 request 都变成独立 profile 既昂贵又破坏统计。trace-profile correlation 应在采样预算内使用,并明确 profile sample 与某个 trace 的关联是统计还是精确。
截至 2026-07-28,OpenTelemetry Profiles signal 的规范稳定性仍为 Alpha;数据格式、SDK 和 Collector 支持都不能当成稳定的跨版本契约。原生 runtime profiler、Linux perf 与 eBPF 的机制见采样、perf 与 eBPF。
Sampling:看见什么,也决定看不见什么¶
Head sampling¶
请求开始时决定是否保留。优点是成本可预测、整条调用链能遵循同一 sampled flag;缺点是当时还不知道请求最终是否慢或失败。
概率为 \(q\) 的独立采样中,样本事件的无偏计数估计可用 Horvitz–Thompson 直觉:
前提是 inclusion probability 已知且采样实现符合模型。parent-based、rate limit、collector loss 与 tail rules 会改变这个前提。
Tail sampling¶
trace 完成或积累足够 span 后,根据 latency、error、attribute 决定。它能保留稀有失败,但需要暂存数据、处理迟到 span,并承担 collector 内存与一致性成本。
“保留全部错误、慢请求和 1% 正常请求”的数据集不能直接计算总体 error rate 或 latency distribution,除非记录并应用各类 inclusion probability。tail sampling 适合调查,不自动等于无偏统计。
一致性¶
同一 trace 的 sampling decision 应尽量一致传播。否则上游保留而下游丢弃会形成 broken trace;下游独立提高采样也可能产生 orphan span。跨组织传播时仍要执行本地成本和安全策略。
Collector 与传输¶
常见数据路径:
Collector 可做 batching、retry、memory limiting、filter、attribute transform、tail sampling 与多后端 export。每一步也会成为故障源:
- exporter queue 满;
- retry 放大下游故障;
- memory limiter 丢数据;
- transform 删除 identity;
- clock skew 与 timestamp 修正;
- collector 自己没有 telemetry;
- OTLP 接收成功但后端写入失败。
遥测管道要有自己的 loss、queue、retry、export latency 与 rejection 指标。应用不应因远端 telemetry backend 变慢而无限阻塞主请求;本地队列也必须有界。
从遥测到服务目标¶
先定义用户结果,再选择 telemetry。一个 availability SLI 可写成:
关键在于 good/valid 的边界:
- client cancel 是否计入;
- health check、synthetic traffic 是否排除;
- 重试按 attempt 还是最终 operation;
- partial success 是否 good;
- dependency failure 由谁承担。
dashboard 应能沿同一语义下钻:
- SLO/window 是否异常;
- 哪个 operation、region、version 贡献;
- 对应 trace 的 critical path;
- 相关 log/error chain;
- span 内 profile 或 host/container 饱和证据;
- deployment/config/event 时间线。
告警以可行动的用户影响为主,资源信号用于诊断。CPU 高但 SLO 正常可能只是有效利用;SLO burn 高而 CPU 低可能是 dependency、queue、lock 或流量被拒绝。
成本、可靠性与隐私预算¶
遥测成本可粗略拆为:
实际还包括 index cardinality、replication、compression、query scan 与 egress。优化顺序通常是:
- 删除无用途字段与重复事件;
- 规范低基数 attributes;
- metric 聚合高频事件;
- 对 trace/profile 采样;
- 按调查价值设置 retention tier;
- 在靠近数据源处过滤敏感内容。
隐私与安全不是 export 后再补:
- 建立字段 allowlist 与 data classification;
- secret、token、认证 header 不进入 telemetry;
- 用户输入和 URL query 做明确 redaction;
- tenant boundary 与 query authorization 一致;
- 限制 baggage、log payload 与 stack/local variable capture;
- 定义 retention、删除与审计策略;
- 将 observability backend 视为生产数据系统。
一套可复验的落地顺序¶
- 写下服务边界、用户 operation 与 SLI;
- 定义 resource identity 和 semantic convention;
- 用 metric 覆盖 rate/error/duration/saturation;
- 在 network、queue、database 边界建立 trace;
- 用结构化 log 保存不可从 span/metric 重建的事件;
- 接入 runtime/host profile;
- 设计 head/tail sampling 与丢失预算;
- 校验跨服务 context、clock、retry 与 async link;
- 做故障注入,确认从告警能下钻到证据;
- 监控 telemetry pipeline 自身并定期删去无用数据。
最终验收不是“每个页面都有图”,而是拿一个真实问题,从用户影响出发,能在有限时间内找到可复验的机制解释。
常见失败¶
- metric label 使用 raw URL、request ID 或完整错误文本;
- trace 每个函数都建 span,却漏掉 queue wait;
- 把 sampling 后的 trace count 当总体 traffic;
- 用 span duration 之和替代 critical path;
- percentile 相加或在不同 bucket schema 间直接合并;
- log message 可读但字段语义随版本漂移;
- propagation 信任外部 baggage;
- telemetry exporter 无界 retry;
- collector 丢数据却没有自监控;
- trace 有 ID,log/profile/deployment 却无法关联;
- 只按平均值告警,忽略尾部与 burn rate;
- 为排障记录 credential 或完整用户 payload。
继续阅读¶
- CPU、off-CPU、PMU 与内核事件:见采样、perf 与 eBPF。
- 把遥测转成负载边界与排障决策:见容量与系统化排障。
- 先保证测量对象可比:见基准设计。
Reference¶
- OpenTelemetry Specification 1.59.0
- OpenTelemetry Protocol 1.11.0
- OpenTelemetry Semantic Conventions 1.43.0
- OpenTelemetry Trace API
- OpenTelemetry Metrics Data Model
- OpenTelemetry Logs Data Model
- OpenTelemetry Profiles specification and Alpha status
- W3C Trace Context Recommendation
- W3C Baggage Recommendation
- Google SRE Workbook: Implementing SLOs