写在前面
单体里排查问题,打开一个日志文件翻一翻就行。微服务里,一个用户请求要穿越 5 个服务,出问题了你去哪翻日志?哪个服务慢?
可观测性(Observability) 就是微服务的神经系统——没有它,系统就是个黑盒,只能靠玄学排查。这一篇讲清三大支柱:日志、指标、链路追踪。
这篇和我写的《.NET Dump 诊断》《.NET 性能优化与 Profiling》呼应——那两篇是"单进程深度诊断",这篇是"跨服务的全局可观测"。
一、监控 vs 可观测性
1
2
3
4
5
6
7
8
9
10
11
| 监控(Monitoring):你知道要问什么,系统告诉你答案
→ 预设告警、仪表盘(CPU 高了?错误率涨了?)
→ 应对"已知的未知"
可观测性(Observability):你不一定知道要问什么,
但系统能让你探索出答案
→ 任意维度下钻、关联日志/指标/链路
→ 应对"未知的未知"
微服务复杂度高,新问题层出不穷,
单靠预设监控不够,必须建可观测性。
|
二、三大支柱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| ┌──────────────────────────────────────────────────┐
│ 可观测性三大支柱 │
│ │
│ 1. 日志 Logs —— 离散的事件记录 │
│ "14:03 用户 123 下单失败,余额不足" │
│ │
│ 2. 指标 Metrics —— 聚合的数值(时间序列) │
│ "过去 1 分钟订单服务 QPS=1200, p99=85ms" │
│ │
│ 3. 链路 Traces —— 一个请求跨服务的完整轨迹 │
│ "请求 → 网关 → 订单 → 库存(慢) → 账户" │
│ │
│ 用 traceId 把三者串起来 = 完整的可观测性 │
└──────────────────────────────────────────────────┘
|
三、日志(Logs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| 日志是离散的事件,记录"发生了什么"。
好日志的特征:
- 带上下文(谁、做了什么、结果)
- 结构化(JSON,方便检索聚合,别只写纯文本)
- 分级别(DEBUG/INFO/WARN/ERROR)
- 带关联 ID(traceId,见第五节)
反例:
log("error") ← 完全没用,错什么了?
log("user error: " + e) ← 没有上下文,哪个用户?
好例:
log.error({ userId: 123, orderId: 456, traceId }, "下单失败:余额不足")
微服务:日志分散在各服务,必须聚合到统一存储
ELK(Elasticsearch + Logstash + Kibana)/ Loki / Grafana
|
四、指标(Metrics)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| 指标是聚合的数值,回答"系统整体怎么样"。
四大黄金信号(Google SRE):
1. 延迟(Latency) —— p50/p99 响应时间
2. 流量(Traffic) —— QPS / 请求数
3. 错误(Errors) —— 错误率 / 5xx 比例
4. 饱和度(Saturation)—— CPU / 内存 / 连接数 / 队列堆积
特点:
- 聚合(不是每条请求一条,是按时间窗口聚合)
- 低成本(存的是数字,不是文本)
- 适合告警(阈值触发)
指标 vs 日志的分工:
指标 → 知道"出问题了 / 在哪里"(大盘、告警)
日志 → 知道"具体怎么回事"(下钻查具体那条)
常见:Prometheus(采集存储)+ Grafana(可视化)
.NET 自带 metrics API,可导出到 Prometheus。
|
1
2
3
4
5
6
| 为什么用 p99 不用平均:
100 个请求,99 个 10ms,1 个 10s
平均 = 110ms(看着还行)
p99 = 10s(真相:有 1% 的用户体验极差)
平均会掩盖长尾。线上要看分位数(p95/p99)。
|
五、链路追踪(Traces)—— 微服务最重要的支柱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 链路追踪:把一个请求经过的所有服务、每段的耗时,画成一条完整轨迹。
一个 trace = 一个请求的完整旅程
一个 span = 旅程中的一段(一次服务调用 / 一次 DB 查询)
示例 trace:
[下单请求] 总 120ms
├─ 网关 2ms
├─ 订单服务.createOrder 118ms
│ ├─ 库存服务.deduct 10ms
│ ├─ DB.insert_order 5ms
│ └─ 账户服务.debit 100ms ← 慢点在这!
└─ 响应 0ms
一眼看出:账户服务是瓶颈。
没有链路追踪,这种问题在微服务里几乎无从查起。
|
5.1 traceId 怎么传
1
2
3
4
5
6
7
8
9
10
11
12
| 关键机制:traceId 跨服务透传
网关生成 traceId → 放进 HTTP Header(W3C Trace Context: traceparent)
→ 每个服务收到请求,从 Header 取出 traceId
→ 调下游时再把 traceId 透传出去
→ 日志里都打上这个 traceId
效果:一条请求在所有服务、所有日志、所有指标里,共享一个 traceId。
排查时按 traceId 一过滤,整条链路的来龙去脉全出来。
标准协议:W3C Trace Context(traceparent / tracestate)
几乎所有语言/框架都支持。
|
六、OpenTelemetry:统一的采集标准
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 痛点:以前日志用 A 厂商 SDK,指标用 B 厂商,链路用 C 厂商
→ 换一套后端就要改一堆代码,被厂商绑架
OpenTelemetry(OTel):CNCF 的统一可观测性标准
- 一套 API/SDK,同时采集 日志 + 指标 + 链路
- 厂商无关:采集和后端解耦
换后端(Jaeger/Prometheus/Tempo/Datadog…)只改配置,不改代码
- 自动埋点:HTTP、DB driver、K8s 等自动注入 trace
- 已是事实标准,主流语言都支持(含 .NET)
.NET 接入:
OpenTelemetry.Extensions.Hosting
+ 各 Instrumentation 包(AspNetCore / HttpClient / SqlClient)
→ 几行代码,自动给所有 HTTP 调用和 DB 查询加上 span
|
1
2
3
4
5
6
7
8
| 典型架构:
应用(OTel SDK)─采集─→ OTel Collector ─路由─→ 后端
├─ Jaeger/Tempo(链路)
├─ Prometheus(指标)
└─ Loki/ES(日志)
Collector 是中间层:接收、处理、转发,解耦应用和后端。
|
七、告警:可观测性的出口
1
2
3
4
5
6
7
8
9
10
11
12
13
| 采集了一堆数据,最终要变成"主动通知"——告警。
好告警的原则:
1. 基于症状,不是原因
告"下单错误率 > 1%"(用户真受影响),别告"CPU 80%"(可能没事)
2. 可执行
每条告警都该有对应的处理动作;收到的告警没人知道怎么处理 = 噪音
3. 少而精
告警风暴会让所有人麻木;宁可少,要准
4. 分级
P0 电话叫醒 / P1 工作时间处理 / P2 记录即可
常见反模式:告 CPU/内存/磁盘每一个阈值 → 天天报警,全员麻木。
|
八、可观测性的落地优先级
1
2
3
4
5
6
7
8
9
| 从零搭建,按这个顺序:
1. 结构化日志 + 日志聚合(先有日志能查)
2. 基础指标 + 黄金信号仪表盘(知道整体健康度)
3. 关键链路追踪(核心请求的 traceId 透传)
4. 统一用 OpenTelemetry 重新规范(前面用临时方案也不要紧)
5. 基于症状的告警
不要一上来追求全套,先把"出问题能查"建起来。
|
九、小结
- 可观测性 > 监控:应对"未知的未知",能探索而不只是预设告警
- 三大支柱:日志(离散事件)、指标(聚合数值)、链路(请求轨迹),用 traceId 串联
- 指标看分位数(p99)不看平均,平均会掩盖长尾
- 链路追踪是微服务最重要的支柱:trace + span,traceId 跨服务透传
- OpenTelemetry:统一采集标准,厂商无关,已成事实标准
- 告警:基于症状、可执行、少而精、分级
- 落地顺序:日志聚合 → 基础指标 → 链路追踪 → OTel 规范化 → 告警
最后一篇,把前七篇的理论落到 ASP.NET Core 代码,搭一个微服务骨架。