场景:线上慢了,但慢在哪?

一个用户反馈下单接口偶尔很慢。你打开监控,看到平均延迟 80ms,P99 却高达 3 秒。问题是:这 3 秒花在了哪一环?是数据库?是某个下游 RPC?是 GC 停顿?还是线程池排队?光看一条平均曲线,你只知道"有问题",却定位不到"问题在哪"。

可观测性(observability)的目标,就是让你能从系统外部的输出推断出内部状态,而不必每次都去改代码加日志重新发布。它由三根支柱构成,常被称为 MELT 中的三个:日志(Logs)、指标(Metrics)、链路追踪(Traces)。它们不是三选一,而是各管一段、互相补位。

三支柱:各自回答什么问题

理解可观测性,先得分清三者的定位,否则容易用错工具:

  • 指标:回答"系统现在健康吗、趋势如何"。它是聚合后的数值(QPS、错误率、P99 延迟、内存占用),存储成本低、查询快,适合做告警和大盘。但它丢失了细节——你知道错误率涨到 5%,但不知道是哪些请求错了。
  • 链路追踪:回答"这一个慢请求,时间花在哪个环节"。它把一次请求经过的所有服务/方法串成一棵 span 树,能精确定位瓶颈节点。代价是数据量大,通常要采样。
  • 日志:回答"这个具体环节到底发生了什么"。它是最细粒度的事实记录,但海量、非结构化时极难检索。

类比:指标是体检报告的各项数值(血压偏高),链路追踪是 CT 扫描(定位到哪个器官),日志是病理切片(具体病灶细节)。三者层层下钻。

机制:把三者串起来的 traceId

可观测性真正的威力,不在于分别拥有三种数据,而在于用一个公共标识把它们关联起来。这个标识就是 traceId

一次请求进入系统,在入口处生成一个全局唯一的 traceId,并随着调用链(HTTP header、消息属性)一路传播到所有下游服务。每个服务内部的每一段操作是一个 span,有自己的 spanId 和指向上游的 parentSpanId。同时,这个 traceId 被写进该请求产生的每一条日志

1
2
3
4
5
请求 ──> 网关(traceId=abc, span=1)
├──> 订单服务(traceId=abc, span=2, parent=1)
│ ├──> DB 查询(span=3, parent=2)
│ └──> 库存服务(traceId=abc, span=4, parent=2)
└──> 日志: [traceId=abc] ... 全链路所有日志都带这个 id

于是定位流程变成:大盘指标告警 → 找到慢的 trace → 看 span 树定位到"库存服务那一跳耗时 2.8s" → 用 traceId 去日志系统精确捞出库存服务这次调用的所有日志 → 看到具体是哪条 SQL 慢。这条下钻路径,才是可观测性的核心价值。

源码:Spring Boot 里怎么落地

现代 Spring Boot 用 Micrometer 做指标,用 Micrometer Tracing(整合 OpenTelemetry / Brave)做链路,日志侧靠 MDC 注入 traceId。引入依赖后,大量工作是自动的——Web 请求、RestTemplate/WebClient 调用、JDBC 都会被自动埋点。

指标埋点示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class OrderService {
private final Counter orderCounter;
private final Timer orderTimer;

public OrderService(MeterRegistry registry) {
this.orderCounter = registry.counter("orders.created", "channel", "app");
this.orderTimer = registry.timer("orders.process.time");
}

public Order create(OrderRequest req) {
return orderTimer.record(() -> { // 记录耗时分布
Order o = doCreate(req);
orderCounter.increment(); // 计数
return o;
});
}
}

让日志自动带上 traceId,只需在日志 pattern 里引用 MDC——Micrometer Tracing 会自动把当前 span 的 id 放进 MDC:

1
2
# logback pattern
%d{HH:mm:ss} [%thread] [%X{traceId:-},%X{spanId:-}] %-5level %logger - %msg%n

跨进程传播则依赖 W3C Trace Context 标准的 traceparent header,框架在 WebClient/RestTemplate 出口自动注入、在入口自动解析,你无需手写。

工程权衡:可观测性不是免费的

第一,采样率是性能与可见性的拉锯。 全量采集 trace,在高 QPS 下会产生海量数据,既冲击应用性能(埋点、序列化、上报开销),又撑爆存储和后端处理能力。所以生产环境普遍采样,比如只留 1%~10%。但采样带来矛盾:你最想看的那个偶发慢请求,恰恰可能没被采到。解法是 尾部采样(tail-based sampling)——先缓存整条 trace,等它结束后,根据"是否报错、是否超过延迟阈值"再决定留不留。代价是 collector 需要更多内存来缓存进行中的 trace。

第二,指标基数(cardinality)是隐形杀手。 每个指标的每一种 tag 取值组合,都是一条独立的时间序列。如果你给指标打了一个 userId 的 tag,而你有百万用户,就会瞬间生成百万条时间序列,把监控后端(尤其是 Prometheus)的内存打爆。铁律:tag 的取值必须是有限且低基数的(如状态码、接口名、机房),绝不能用 userId、订单号、请求参数这类高基数维度。这是新手最常踩的线上事故。

第三,日志的成本与噪音。 全量 DEBUG 日志在生产是灾难:磁盘 IO、网络上报、存储费用都会失控,关键信息还会淹没在噪音里。合理做法是默认 INFO,结合 traceId 做"按需下钻"——平时不打详细日志,需要排查时通过 traceId 把同一请求的少量关键日志串起来,而不是无差别地疯狂打日志。

常见误区

  • 把三支柱割裂使用:有指标无 traceId 关联,告警响了还得人肉去各服务翻日志,完全没发挥下钻能力。务必让日志带上 traceId。
  • 用日志算指标:从日志里 grep 统计 QPS、错误率,既慢又脆弱。计数和分布该交给 Micrometer 这类专门的指标系统,它在内存里聚合,代价远低于事后扫日志。
  • 追求"采集一切":可观测性的目标是"能在需要时定位问题",不是"存下所有数据"。无脑全采全存,换来的往往是失控的成本和被噪音淹没的信噪比。

小结

日志、指标、链路追踪不是三个独立工具,而是一套层层下钻的定位体系:指标告诉你"有问题",链路追踪告诉你"问题在哪一跳",日志告诉你"那一跳具体发生了什么",而把它们焊接成整体的是贯穿全链路的 traceId。在 Spring Boot 里,Micrometer 加 Micrometer Tracing 已经把绝大部分埋点自动化,真正需要你操心的是工程权衡——采样率怎么平衡可见性与开销、指标 tag 如何避免高基数爆炸、日志如何按需下钻而非无脑全量。把这几条边界守住,下次再遇到那个 P99 飙到 3 秒的请求,你就能顺着 traceId 一路追到那条慢 SQL,而不是对着一条平均曲线干瞪眼。