一、为什么我们需要分布式追踪?

说实话,我刚接触微服务那会儿,觉得这东西挺酷的。一个请求进来,经过网关、服务A、服务B、服务C,最后返回结果。看起来井井有条对吧?

但真正上线后,问题就来了。用户说「页面好慢」,我盯着日志看了半天,每个服务都说自己只花了50ms。那这500ms的延迟到底去哪儿了?

这就是分布式追踪要解决的问题。它能把一个请求经过的所有服务串起来,告诉你每一站花了多少时间。说白了,就是给分布式系统装个「行车记录仪」。

二、核心概念:Trace 与 Span

在讲具体工具之前,咱们得先搞懂两个最基础的概念。我当年就是被这两个词绕晕过。

2.1 Trace(追踪)

一个 Trace 代表一次完整的请求链路。比如你打开一个电商页面,这个请求可能涉及:

  • 前端请求网关
  • 网关调用用户服务
  • 用户服务调用订单服务
  • 订单服务调用库存服务
  • 最后返回页面数据

这一整条链路,就是一个 Trace。每个 Trace 有一个全局唯一的 Trace ID。

2.2 Span(跨度)

Span 是 Trace 的最小单位。每个服务调用、每个数据库查询、每个消息发送,都可以是一个 Span。

举个例子:

Trace: 用户下单
├── Span: 网关接收请求 (5ms)
├── Span: 调用用户服务 (20ms)
│   └── Span: 查询用户数据库 (10ms)
├── Span: 调用订单服务 (150ms)
│   └── Span: 写入订单数据库 (100ms)
└── Span: 返回响应 (2ms)

每个 Span 都包含:

  • Span ID:当前 Span 的唯一标识
  • Parent Span ID:父 Span 的 ID,用来构建父子关系
  • Trace ID:所属 Trace 的 ID
  • 开始时间、结束时间:计算耗时
  • Tags、Logs:附加信息,比如 HTTP 状态码、异常堆栈

关键点:Trace 是一棵树,Span 是树上的节点。根 Span 是整个请求的入口。

三、主流工具:Jaeger 与 Zipkin

概念搞懂了,接下来就是选工具。目前市面上用得最多的两个开源方案是 Jaeger 和 Zipkin。我两个都用过,说说我的感受。

3.1 Jaeger

Jaeger 是 Uber 开源的,后来捐给了 CNCF。我个人比较喜欢它的 UI,直观、好看。

它的核心组件:

  • Agent:部署在每个节点上,收集 Span 数据
  • Collector:接收 Agent 上报的数据,做校验和索引
  • Query:提供查询 API 和 UI 界面
  • Storage:支持 Cassandra、Elasticsearch、Kafka 等后端

3.2 Zipkin

Zipkin 是 Twitter 开源的,老牌项目。它的设计更轻量,部署起来也简单。

我记得有一次在客户现场,环境比较受限,我就用 Zipkin 搭了个单机版,一条命令搞定:

java -jar zipkin.jar

相比之下,Jaeger 的组件多一些,但功能也更丰富。

3.3 对比表格

特性 Jaeger Zipkin
开源方 Uber → CNCF Twitter → OpenZipkin
存储后端 Cassandra、ES、Kafka MySQL、ES、Cassandra
UI 体验 现代化,支持拓扑图 简洁,够用
采样策略 支持概率采样、速率限制 支持概率采样
部署复杂度 中等 简单

我的建议:如果团队刚起步,选 Zipkin 快速上手。如果链路复杂、需要高级分析,Jaeger 更合适。

四、OpenTelemetry:统一标准

说到这儿,你可能会问:「那我到底用 Jaeger 的 SDK 还是 Zipkin 的 SDK?」

嗯,这个问题曾经也困扰过我。以前每个工具都有自己的 SDK,换工具就得改代码,很痛苦。

后来 OpenTelemetry(简称 OTel)出现了。它想做的是「一次埋点,到处上报」。你只需要用 OTel 的 API 打点,至于数据发给 Jaeger 还是 Zipkin,那是配置的事。

4.1 OTel 的核心架构

应用代码
    ↓
OTel SDK(生成 Span、Trace)
    ↓
OTel Collector(接收、处理、导出)
    ↓
后端系统(Jaeger / Zipkin / Prometheus / ...)

说白了,OTel 就是中间层。它不替代 Jaeger 或 Zipkin,而是让它们能互相替换。

4.2 Kotlin 中使用 OTel

在 Kotlin 项目里集成 OTel 其实很简单。我拿一个 Spring Boot 项目举例:

// build.gradle.kts
implementation("io.opentelemetry:opentelemetry-api:1.31.0")
implementation("io.opentelemetry:opentelemetry-sdk:1.31.0")
implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.31.0")

然后在代码里创建 Span:

val tracer = OpenTelemetry.getGlobalTracer("my-service")

fun handleRequest(request: Request) {
    val span = tracer.spanBuilder("handleRequest")
        .setAttribute("request.id", request.id)
        .startSpan()
    
    try {
        // 业务逻辑
        processOrder(request)
    } catch (e: Exception) {
        span.recordException(e)
        span.setStatus(StatusCode.ERROR)
        throw e
    } finally {
        span.end()
    }
}

注意:Span 一定要在 finally 块里 end(),否则会造成内存泄漏。我曾经踩过这个坑,排查了好久才发现是 Span 没关闭。

五、实战:在 Kotlin 项目中集成 Jaeger

光说不练假把式。咱们来一个完整的集成示例。

5.1 启动 Jaeger

用 Docker 启动最简单:

docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
  -p 6831:6831/udp \
  -p 16686:16686 \
  jaegertracing/all-in-one:latest

启动后,访问 http://localhost:16686 就能看到 Jaeger UI。

5.2 Kotlin 代码集成

我习惯用 OpenTelemetry 的 Jaeger 导出器:

// 配置 OTel SDK
val openTelemetry = OpenTelemetrySdk.builder()
    .setTracerProvider(
        SdkTracerProvider.builder()
            .addSpanProcessor(BatchSpanProcessor.builder(
                JaegerGrpcSpanExporter.builder()
                    .setEndpoint("http://localhost:14250")
                    .build()
            ).build())
            .build()
    )
    .build()

// 获取 Tracer
val tracer = openTelemetry.getTracer("order-service")

然后在关键方法里埋点:

fun createOrder(userId: String, productId: String): Order {
    val span = tracer.spanBuilder("createOrder")
        .setAttribute("user.id", userId)
        .setAttribute("product.id", productId)
        .startSpan()
    
    return try {
        // 模拟业务逻辑
        val user = userService.getUser(userId)
        val product = productService.getProduct(productId)
        val order = orderRepository.save(Order(user, product))
        
        span.setAttribute("order.id", order.id)
        order
    } catch (e: Exception) {
        span.setStatus(StatusCode.ERROR, "创建订单失败")
        span.recordException(e)
        throw e
    } finally {
        span.end()
    }
}

5.3 查看追踪结果

请求跑完后,打开 Jaeger UI,搜索对应的 Service 和 Operation,就能看到完整的 Trace 图。每个 Span 的耗时、标签、日志都一目了然。

经验之谈:我一般会在每个 Span 里加上关键的业务属性,比如 userId、orderId。这样排查问题时,直接搜 ID 就能定位到对应的 Trace,非常方便。

六、分布式追踪知识体系

下面这张图帮你理清整个知识脉络:

分布式追踪知识体系 核心概念 Trace(追踪) Span(跨度) Span Context 工具与标准 Jaeger Zipkin OpenTelemetry 实践要点 埋点策略 采样配置 性能开销控制 目标:快速定位分布式系统中的性能瓶颈与异常

七、避坑指南

最后分享几个我踩过的坑,希望能帮你少走弯路。

7.1 采样率别设太高

我曾经在一个高并发项目里把采样率设成了 100%,结果 Jaeger Collector 直接被打爆了。后来改成 1% 的采样率,系统就稳了。

一般来说,生产环境用 1%~10% 的采样率就够了。排查问题时可以临时调高。

7.2 Span 一定要有结束时间

如果 Span 忘了 end(),Jaeger 里会显示这个 Span 一直在跑。我见过有人排查慢请求,结果发现是上一个请求的 Span 没关,数据串了。

7.3 不要埋太多点

每个 Span 都有开销。我建议只埋关键路径:外部调用、数据库操作、消息队列。像循环里的每次计算就没必要埋了,否则 Trace 图会变得密密麻麻,反而看不清。

小技巧:用 OTel 的 @WithSpan 注解可以自动埋点,省去手动创建 Span 的代码。但要注意,它只适用于简单的同步方法。

八、总结

分布式追踪不是什么高深的技术。说白了,就是给每个请求发一个「身份证号」(Trace ID),然后所有服务都带着这个号记录日志。最后通过 Jaeger 或 Zipkin 把这些日志串起来,你就能看到请求的完整旅程。

我个人建议新项目直接上 OpenTelemetry,因为它已经是业界标准了。以后想换后端工具,改个配置就行,代码不用动。

好了,这一章就到这里。希望你在自己的项目里也能用上分布式追踪,别再对着日志大海捞针了。


公众号:蓝海资料掘金营,微信deep3321