第二章 全链路监控基础:分布式追踪原理、Trace/Span概念、OpenTracing标准

好,咱们正式开始聊全链路监控的核心——分布式追踪。说实话,我刚接触这个领域时,也被一堆新概念砸得头晕。但搞懂了最基础的几个东西,后面就顺了。

2.1 为什么需要分布式追踪?

先问个问题:你负责的系统,一个请求从用户点击到最终返回,中间经过了多少个服务?

我见过不少团队,服务拆了十几个,但排查问题还是靠「猜」。A服务说「我调了B」,B说「我调了C和D」,D又说「我超时了」……一圈下来,谁都不知道根因在哪。

这就是典型的「分布式困境」——请求像一串珍珠,但珍珠散落在各个服务里,没人能把它们串起来。

分布式追踪要解决的,就是这个串珍珠的问题。

2.2 Trace 和 Span:最核心的两个概念

说白了,一个 Trace 就是一次完整的请求旅程。从用户点击,到网关、微服务、数据库、缓存,最后返回结果——整条链路就是一个 Trace。

而 Span,是 Trace 里的一个片段。每个服务、每个操作,都可以是一个 Span。

举个例子:

你下单买了个东西。整个下单过程是一个 Trace。其中「调用库存服务」、「扣减余额」、「生成订单」这三个步骤,各自是一个 Span。

每个 Span 都记录着:

  • Span ID:自己的唯一标识
  • Parent Span ID:父 Span 是谁(形成树状结构)
  • Trace ID:属于哪个 Trace
  • 开始时间、结束时间:算耗时用的
  • Tags、Logs:附加信息,比如 HTTP 状态码、异常堆栈

我在项目中遇到过最典型的坑:有人把 Span 的 Parent ID 传错了,结果整个链路图变成了一团乱麻。嗯,调试这种问题比写代码还痛苦。

2.3 分布式追踪的工作原理

原理其实不复杂,就三步:

  1. 注入:请求进来时,生成或传递 Trace ID 和 Span ID
  2. 传播:通过 HTTP 头、RPC 元数据等方式,把上下文传到下游服务
  3. 收集:每个服务把 Span 数据上报到后端,后端按 Trace ID 组装成完整链路

你想想看,这就像快递包裹上的运单号。每个中转站都扫一下码,记录时间和状态。最后你查物流时,就能看到完整的运输路径。

我的习惯:在网关层就生成 Trace ID,然后通过 HTTP Header 传递。推荐用 uber-trace-idX-B3-TraceId 这样的标准头,别自己发明格式。

2.4 OpenTracing 标准:让不同系统能对话

早些年,各家 APM 厂商各玩各的。Jaeger 的格式、Zipkin 的格式、SkyWalking 的格式……互不兼容。你换一个工具,埋点代码就得重写。

OpenTracing 就是来解决这个问题的。它定义了一套标准接口,不管你底层用 Jaeger 还是 Zipkin,埋点代码几乎不用改。

核心接口就几个:

接口 作用
Tracer 创建 Span,管理上下文
Span 代表一个操作单元
SpanContext 传递 Trace ID、Span ID 等上下文
Scope 管理 Span 的生命周期(自动关闭)

用代码看更直观。这是一个典型的 OpenTracing 埋点:

// 创建 Tracer(以 Jaeger 为例)
Tracer tracer = new Configuration("my-service")
    .withSampler(Configuration.SamplerConfiguration.fromEnv())
    .withReporter(Configuration.ReporterConfiguration.fromEnv())
    .getTracer();

// 开始一个 Span
Span span = tracer.buildSpan("handleRequest")
    .withTag("http.method", "GET")
    .withTag("http.url", "/api/order")
    .start();

// 模拟业务逻辑
try {
    // 调用下游服务
    Span childSpan = tracer.buildSpan("callInventoryService")
        .asChildOf(span)
        .start();
    // ... 调用代码
    childSpan.finish();
} catch (Exception e) {
    span.setTag("error", true);
    span.log(Map.of("event", "error", "error.object", e));
} finally {
    span.finish();
}

我曾经踩过的坑:忘记在 finally 里调用 span.finish()。结果 Span 一直不关闭,后端看到的链路全是「进行中」状态。排查了半天才发现是资源泄漏。

2.5 采样:别什么都往上报

高并发系统里,每秒几万个请求。如果每个请求都生成 Trace 并上报,后端存储扛不住,网络带宽也扛不住。

所以必须采样。常见的策略有:

  • 固定采样:比如只采样 1% 的请求
  • 动态采样:根据请求延迟、错误率动态调整采样率
  • 头部采样:只采样前 N 个 Span(不推荐,容易丢失关键信息)

我个人习惯用动态采样。正常时采样 1%,一旦发现某个接口错误率飙升,自动提升到 100%。这样既省钱,又不漏问题。

2.6 从理论到实践:你需要知道的几件事

讲完原理,说点实际的。搭建全链路监控,有几个关键点绕不开:

  1. 统一 Trace ID 生成规则:所有服务用同一套算法,别各搞各的
  2. 上下文传递不能丢:异步调用、消息队列、线程池——这些地方最容易丢上下文
  3. 存储选型要提前想好:Elasticsearch 适合全文检索,Cassandra 适合高写入,各有优劣
  4. UI 展示要直观:甘特图、火焰图、调用拓扑——用户一眼能看出问题在哪

一个小技巧:在日志里也带上 Trace ID。这样你查日志时,直接搜 Trace ID 就能把整个请求的所有日志串起来。排查效率翻倍。

2.7 本章小结

分布式追踪的核心,说白了就是三个东西:Trace(完整链路)、Span(操作片段)、上下文传递(把 ID 传下去)。OpenTracing 让不同系统能互相理解,采样让系统不至于被数据淹死。

下一章,咱们会深入讲 APM 系统的整体架构设计。到时候你会看到,这些基础概念是怎么搭建成一个完整系统的。

嗯,先消化这些。有问题随时翻回来看看。