一、为什么我们需要分布式追踪?
说实话,我刚接触微服务那会儿,觉得这东西挺酷的。一个请求进来,经过网关、服务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,非常方便。
六、分布式追踪知识体系
下面这张图帮你理清整个知识脉络:
七、避坑指南
最后分享几个我踩过的坑,希望能帮你少走弯路。
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