后端架构实战(七):可观测性——微服务的神经系统

写在前面

单体里排查问题,打开一个日志文件翻一翻就行。微服务里,一个用户请求要穿越 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 代码,搭一个微服务骨架。