可观测三件套:指标、日志与链路追踪
排查能力的天花板由可观测性决定:没有指标只能靠猜,没有日志只能靠复现,没有链路只能靠翻代码。本章讲清 Metrics、Logs、Trace 三者的分工、各自的判读方法,以及三者如何互相印证——最后给出没有 APM 时的兜底方案。
三者的分工
| 维度 | Metrics 指标 | Logs 日志 | Trace 链路 |
|---|---|---|---|
| 回答什么 | 有没有问题、范围多大 | 错在哪一步、什么异常 | 慢在哪一跳、谁的责任 |
| 数据形态 | 时间序列,聚合值 | 离散事件,全量文本 | 调用树,带父子关系 |
| 成本 | 低(可长期保存) | 高(需采样与轮转) | 中(必须采样) |
| 强项 | 趋势与告警 | 细节与上下文 | 跨服务耗时归因 |
| 弱项 | 无细节、无个体 | 难以聚合统计 | 采样后可能漏掉长尾 |
| 典型入口 | 大盘与告警面板 | journalctl、日志平台 | Trace 查询与耗时 TopN |
只用一件工具排查,等于只用一只眼睛看世界。三个一起上,才能形成“现象 → 证据 → 结论”的闭环。
Metrics:USE 与 RED
两种方法各管一半:USE 看资源,RED 看服务。
| 方法 | 维度 | 指标 | 判读要点 |
|---|---|---|---|
| USE | 资源利用率 Utilization | CPU、内存、带宽使用率 | 逼近 100% 才算异常,先找最先接近的那个 |
| USE | 饱和度 Saturation | load、队列长度、连接池等待数 | 有排队就说明资源不够,即使利用率不高 |
| USE | 错误 Errors | 网卡丢包、磁盘重试、OOM 次数 | 这类错误不会体现在利用率上 |
| RED | 速率 Rate | QPS/TPS | 突增可能是重试放大,突降可能是上游断了 |
| RED | 错误 Errors | 5xx 比例、超时率 | 按接口与状态码拆开看 |
| RED | 时长 Duration | P50/P95/P99 | 涨在哪个分位决定影响多少人 |
延迟必须看分位,不同分位对应不同故事:
| 分位 | 含义 | 常见根因 |
|---|---|---|
| P50 | 一半请求快于它 | 整体变慢:依赖变慢、数据量增长 |
| P95 | 5% 的请求慢于它 | 局部热点、索引失效、单机故障 |
| P99 | 1% 的请求慢于它 | GC 停顿、锁竞争、慢查询、重试 |
| Max | 最慢的那一个 | 超时与长尾,直接决定用户投诉 |
告警阈值怎么定
- 阈值要基于基线:先观察两周同一个时间段的正常值,再取 3 倍标准差或静态倍数。
- 优先报警 P99 与错误率,不建议对平均延迟报警,响应太迟钝。
- 每个告警必须能被一条命令验证,否则运维会习惯性忽略它。
- 分级:P0 打电话(核心接口不可用),P1 群里通知(错误率超阈值),P2 只进工单(水位趋势)。
坏告警:CPU > 80% 持续 5 分钟 → 不一定影响用户,频繁误报
好告警:下单接口 5xx 比例 > 1% 持续 1 分钟,且请求量 > 100/s
好告警:下单接口 P99 > 2s 持续 3 分钟
Logs:结构化才可用
日志的价值取决于字段是否统一。排查时要按“时间 + traceId + 接口 + 错误码”聚合,纯文本自由格式做不到。
| 必须有的字段 | 作用 |
|---|---|
| 时间戳(毫秒,带时区) | 与变更时间线对齐 |
| TraceID / SpanID | 跨服务串联一次请求 |
| 用户 ID / 租户 ID | 评估影响面、复现个案 |
| 接口名 / 方法名 | 按接口聚合错误率 |
| 耗时(自身耗时 + 下游耗时) | 区分自身慢还是下游慢 |
| 错误码 / 异常类名 | 分类统计,避免只看总数 |
# 用 TraceID 串起一次请求在所有服务里的日志
grep -h "<trace-id>" /var/log/myapp/*.log | sort -k1,2 | head -50
# 按分钟统计错误数,定位拐点
grep "ERROR" app.log | awk '{print substr($1,1,16)}' | uniq -c | tail -30
# 结构化日志(JSON)按字段过滤
grep '"level":"ERROR"' app.log | head -20
日志落盘必须配套轮转,否则日志本身就是下一个故障源:磁盘写满或 inode 耗尽都会导致全站不可用。
Trace:把耗时拆开
一次慢请求的耗时永远能拆成四段:自身计算、等待下游、等待网络、排队等待。
| 耗时构成 | 表现 | 优化方向 |
|---|---|---|
| 自身计算 | 本服务 Span 内耗时长,无子 Span | 优化代码、算法、序列化 |
| 等待下游 | 子 Span 本身慢 | 推动下游优化、加超时与降级 |
| 网络 | 子 Span 开始时间明显晚于父 Span | 重试、DNS、跨机房调用 |
| 排队等待 | 线程池、连接池等待 Span | 扩池、隔离、限流 |
读 Trace 的三个要点:先看最底层的慢 Span(那里才是真凶),再看是否所有请求都慢(全局问题还是个别请求),最后看 Span 数量是否异常膨胀(N+1 调用)。
三者互相印证
| 症状 | 先用谁 | 再用谁确认 |
|---|---|---|
| 大盘延迟抬头,不知哪台机器 | Metrics 分实例曲线 | Logs 看实例日志,Trace 看该实例慢 Span |
| 错误率上升 | Metrics 按接口拆分 | Logs 找首个异常栈 |
| 偶发超时、均值正常 | Trace 找长尾请求 | Logs 看该 TraceID 完整链路 |
| 单机 CPU 高、整体正常 | Metrics 分实例 | 登机器用 top -Hp 定位线程 |
| 用户说“很慢”但监控正常 | Logs 按用户 ID 检索 | Trace 看该用户请求,常见是数据量差异 |
没有 APM 时的兜底
没有链路追踪也能做到 80% 的定位能力,前提是把 TraceID 落到日志里。
请求进入 Nginx:生成/透传 X-Request-ID → 应用日志打印该 ID
↓
Nginx access.log 记录 $request_time(整体)与 $upstream_response_time(后端)
↓
两者之差 ≈ 网络与排队耗时;upstream 耗时大 → 问题在后端
↓
应用日志按 X-Request-ID 捞出该请求的全部日志 → 看到具体慢在哪一步
Nginx 侧至少加上这几个变量:
log_format main '$remote_addr - $request_id [$time_local] "$request" '
'$status $body_bytes_sent $request_time $upstream_response_time '
'"$http_referer" "$http_user_agent" $upstream_addr';
判读规则:$request_time 大而 $upstream_response_time 小,问题在 Nginx 自身或客户端;两者都大,问题在后端;$upstream_addr 指向哪台就查哪台。
预防与巡检
- 核心接口必须有指标、日志、TraceID 三项,缺一项就算可观测性欠债。
- 定期验证告警真的能触发,静默的告警比没有告警更危险。
- 日志字段变更要走评审,字段一旦改名,历史检索链路全部失效。
- 采样率要留够长尾:错误请求应该 100% 采样,正常请求可以低比例采样。
小结:Metrics 告诉你有没有问题与范围,Logs 告诉你错在哪一步,Trace 告诉你慢在哪一跳;延迟看 P95/P99 而非均值,告警按接口与分位设置,日志必须结构化并带 TraceID;没有 APM 时用 Nginx 的 $request_time 与 $upstream_response_time 差值加 TraceID 兜底,同样能定位到具体服务与具体步骤。