2、链路追踪基础:分布式追踪的核心概念与标准
好,咱们今天聊聊链路追踪的基础。说实话,我刚开始接触分布式系统时,最头疼的就是问题排查。一个请求发出去,经过七八个服务,到底卡在哪了?日志翻半天,头都大了。后来我才明白,这就是典型的「缺乏可观测性」——你根本不知道系统内部发生了什么。
链路追踪,说白了就是给每个请求发一张「通行证」,记录它走过的每一步。这张通行证上写着:你从哪来、到哪去、在每个服务里待了多久、有没有出问题。有了它,你就能把一次请求的完整路径还原出来。
2.1 核心概念:Trace、Span、SpanContext
这三个概念,是链路追踪的基石。我当年刚学的时候,觉得它们很抽象。后来在项目中亲手搭过一次,才真正理解。
2.1.1 Trace(追踪)
Trace 代表一次完整的请求链路。比如你在电商网站下单,这个操作会触发:前端 -> 网关 -> 订单服务 -> 库存服务 -> 支付服务 -> 通知服务。这一整条链路上的所有操作,都属于同一个 Trace。
每个 Trace 有一个全局唯一的 ID,叫 TraceID。我习惯用 UUID 或者 Snowflake 算法生成,保证分布式环境下不重复。
关键点:Trace 是树形结构,不是线性列表。一个 Trace 里可以有多个分支,比如并行调用多个服务。
2.1.2 Span(跨度)
Span 是 Trace 里的最小工作单元。每个 Span 代表一个服务内部的一次操作,比如「查询数据库」、「调用下游接口」、「执行一段业务逻辑」。
每个 Span 也有自己的 ID(SpanID),并且会记录它的父 Span ID(ParentSpanID)。这样就能把 Span 串起来,形成一棵调用树。
我记得有一次排查线上问题,发现某个接口响应特别慢。打开链路追踪一看,有个 Span 的耗时占了 80%。点进去发现是 Redis 查询超时了。嗯,这就是 Span 的价值——帮你精准定位瓶颈。
一个 Span 通常包含以下信息:
- SpanID:当前 Span 的唯一标识
- ParentSpanID:父 Span 的 ID,根 Span 没有父 ID
- TraceID:所属 Trace 的 ID
- OperationName:操作名称,比如 "query_user_info"
- StartTime / EndTime:开始和结束时间
- Tags:键值对,记录额外信息,比如 HTTP 状态码、数据库语句
- Logs:事件日志,比如异常堆栈
2.1.3 SpanContext(跨度上下文)
SpanContext 是 Span 的「身份证」。它包含了 TraceID、SpanID 以及一些需要跨服务传递的 Baggage(行李)数据。
为什么需要 SpanContext?因为服务之间调用时,必须把当前 Trace 的信息传递过去,下游服务才能知道「我是哪个 Trace 的一部分」。传递方式通常是通过 HTTP 头(比如 uber-trace-id)或者消息队列的元数据。
避坑指南:我曾经遇到过一个坑——Baggage 数据传得太多,导致 HTTP 头过大,影响了性能。后来我严格控制 Baggage 的大小,只传必要的业务上下文,比如用户 ID、请求来源。
2.2 分布式追踪标准:OpenTracing 与 OpenTelemetry
早期链路追踪工具各搞各的,比如 Jaeger 有自己的 API,Zipkin 也有自己的。这就导致一个问题:你用了 Jaeger,想换成 SkyWalking,得改代码。说白了,就是缺乏统一标准。
后来社区开始推动标准化,出现了两个重要的项目:OpenTracing 和 OpenTelemetry。
2.2.1 OpenTracing
OpenTracing 是一个 API 规范,定义了如何创建 Span、如何传递 SpanContext、如何记录日志。它不关心底层实现,只提供接口。
举个例子,用 OpenTracing 的 API 创建一个 Span:
// 创建一个 tracer
Tracer tracer = new JaegerTracer.Builder("my-service").build();
// 创建一个 Span
Span span = tracer.buildSpan("query_database")
.withTag("db.type", "mysql")
.start();
// 执行操作
try {
// 你的业务逻辑
} catch (Exception e) {
span.log("error: " + e.getMessage());
} finally {
span.finish();
}
你想想看,如果所有工具都遵循 OpenTracing,那么切换实现时只需要改一行配置,代码几乎不用动。这就是标准化的好处。
不过 OpenTracing 也有局限——它只定义了追踪的 API,没有涵盖指标和日志。而且它已经停止更新了,被 OpenTelemetry 取代。
2.2.2 OpenTelemetry
OpenTelemetry(简称 OTel)是 OpenTracing 和 OpenCensus 合并后的产物。它不只是做追踪,还涵盖了指标和日志,目标是成为可观测性的统一标准。
我个人觉得,OTel 最大的优势是「一次埋点,到处使用」。你只需要用 OTel 的 SDK 埋好点,数据可以导出到 Jaeger、Zipkin、Prometheus、Datadog 等任意后端。
用 OTel 创建一个 Span 的代码也很简洁:
// 获取 tracer
Tracer tracer = GlobalOpenTelemetry.getTracer("my-service");
// 创建 Span
Span span = tracer.spanBuilder("query_database")
.setAttribute("db.type", "mysql")
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 你的业务逻辑
} catch (Exception e) {
span.recordException(e);
} finally {
span.end();
}
你看,和 OpenTracing 的写法很像,但 OTel 更强大——它内置了上下文传播、采样策略、导出器配置等。
2.2.3 两者的对比
| 特性 | OpenTracing | OpenTelemetry |
|---|---|---|
| 状态 | 已停止更新 | 活跃开发中 |
| 覆盖范围 | 仅追踪 | 追踪 + 指标 + 日志 |
| 上下文传播 | 手动实现 | 内置支持 |
| 采样策略 | 需自行集成 | 内置多种策略 |
| 后端兼容性 | Jaeger、Zipkin 等 | Jaeger、Zipkin、Prometheus、Datadog 等 |
注意:如果你现在要新建项目,我建议直接上 OpenTelemetry。OpenTracing 虽然还能用,但社区已经不维护了,未来可能会遇到兼容性问题。
2.3 核心逻辑:Trace 与 Span 的关系
为了让你更直观地理解 Trace 和 Span 的关系,我画了一张图。这张图展示了一次下单请求的完整链路:
从这张图你能看到:
- 一个 Trace 包含多个 Span,Span 之间通过 ParentSpanID 形成树形结构
- 每个 Span 可以有自己的子 Span,比如订单服务内部有「查数据库」和「调用库存」两个子操作
- 叶子 Span(库存服务)没有子 Span,它是最底层的调用
嗯,这里要注意:Span 的父子关系不一定是严格的「请求-响应」顺序。比如并行调用时,多个子 Span 可以同时存在,它们的 ParentSpanID 指向同一个父 Span。
2.4 实际项目中的经验
我在做风控系统时,链路追踪帮了大忙。有一次发现某个风控规则执行特别慢,影响了整个下单流程。通过追踪,我发现是规则引擎里调了一个外部黑名单接口,那个接口偶尔会超时。
怎么解决的?我做了两件事:
- 加超时熔断:给外部调用设置超时时间,超时后走降级逻辑
- 加采样策略:全量追踪太耗资源,我改成「错误链路全量采样 + 正常链路 1% 采样」
说白了,链路追踪不只是用来排查问题的,它还能帮你做性能优化和容量规划。你想想看,如果你知道每个接口的 P99 延迟,是不是就能更精准地做限流和扩容?
个人建议:刚开始做链路追踪时,不要追求「全量覆盖」。先给核心链路(比如下单、支付)埋点,跑通了再逐步扩展。我见过不少团队一上来就想把所有服务都接入,结果埋点不规范,数据质量差,最后不了了之。
好了,这一章的内容就到这里。链路追踪的基础概念和标准,说白了就是三件事:Trace 标识一次请求、Span 记录每一步、SpanContext 传递上下文。标准方面,OpenTelemetry 是未来的方向,建议直接上手。