3、链路追踪实战:Jaeger 与 Zipkin 选型、Span 设计、采样策略
说到分布式系统的性能调优,链路追踪绝对是个绕不开的话题。我刚开始做微服务的时候,线上出了个慢请求,十几个服务绕来绕去,查了半天愣是没找到瓶颈在哪。后来上了链路追踪,才真正体会到什么叫「一目了然」。
今天咱们就聊聊链路追踪的实战选型。Jaeger 和 Zipkin 这两个老牌选手,到底该怎么选?Span 怎么设计才合理?采样策略怎么配才能既省钱又有效?
3.1 Jaeger 与 Zipkin:选型对比
先说说我的个人习惯。我一般把这两个工具放在一起对比,看场景选。
| 对比维度 | Jaeger | Zipkin |
|---|---|---|
| 存储后端 | Elasticsearch、Cassandra、Kafka | Elasticsearch、Cassandra、MySQL |
| UI 体验 | 更现代,支持 DAG 图 | 简洁,但功能够用 |
| 采样策略 | 支持概率采样、速率限制、远程配置 | 支持概率采样、速率限制 |
| 社区活跃度 | CNCF 毕业项目,更新频繁 | Apache 项目,相对稳定 |
| 部署复杂度 | 组件较多,稍复杂 | 轻量,单 jar 可跑 |
我遇到过不少团队,一上来就纠结选哪个。其实没那么复杂——
- 如果你团队小、链路少:Zipkin 就够了。部署简单,一个 jar 包搞定,存储用 MySQL 也能凑合。
- 如果你服务多、流量大:我建议上 Jaeger。它的远程采样配置和 DAG 图,排查复杂调用链时真的省心。
- 如果你已经用了 Prometheus + Grafana:Jaeger 的集成更自然,毕竟都是 CNCF 生态。
3.2 Span 设计:别小看这个环节
Span 是链路追踪的最小单位。说白了,一个 Span 就是一个操作单元。比如一次数据库查询、一次 RPC 调用、一次消息发送。
Span 设计得好不好,直接影响排查问题的效率。我见过最夸张的案例——有人把整个 HTTP 请求都塞进一个 Span 里,结果出了慢请求根本不知道是哪个环节慢。
3.2.1 Span 的核心字段
每个 Span 至少要有这些信息:
- traceId:全局唯一,串联整条链路
- spanId:当前 Span 的唯一标识
- parentSpanId:父 Span 的 ID,形成树形结构
- operationName:操作名称,比如
GET /api/users - startTime / duration:开始时间和耗时
- tags:自定义标签,比如
http.status_code=200 - logs:事件日志,比如异常堆栈
3.2.2 实战中的 Span 设计原则
我总结了几条经验,你直接拿去用:
- 一个 Span 只做一件事。别把「查数据库 + 调外部 API + 写缓存」塞进一个 Span。拆开,每个操作一个 Span。
- Span 的粒度不要太细。比如循环 1000 次调用,别生成 1000 个 Span。用
batch模式合并。 - 关键路径加 Tag。比如数据库查询的 SQL 语句、HTTP 请求的 URL。排查时一眼就能看出问题。
- 异常一定要记录。在 Span 的 logs 里记录错误堆栈,别只打个
error=true的 Tag。
3.2.3 代码示例:手动创建 Span
以 Jaeger 的 Java 客户端为例:
// 创建 Tracer
Tracer tracer = Configuration.fromEnv().getTracer();
// 创建一个 Span
Span span = tracer.buildSpan("getUserById")
.withTag("user.id", userId)
.withTag("http.method", "GET")
.start();
try {
// 模拟业务逻辑
User user = userService.findById(userId);
span.setTag("user.found", user != null);
} catch (Exception e) {
// 记录异常
span.log(Map.of("event", "error", "error.object", e));
throw e;
} finally {
span.finish();
}
嗯,这里要注意:span.finish() 一定要在 finally 里调用。不然出了异常,Span 永远不会结束,链路就断了。
3.3 采样策略:省钱又有效的关键
链路追踪最怕什么?存储爆炸。你想想看,一个高并发系统每秒几万个请求,每个请求生成几十个 Span,一天下来数据量能到 TB 级。
所以采样策略必须上。我一般分三层来配置:
3.3.1 概率采样
最简单的策略。比如设置采样率为 0.1,那就只采样 10% 的请求。
# Jaeger 配置
sampler:
type: probabilistic
param: 0.1
适合流量均匀的场景。但有个问题——低流量接口可能永远采不到。
3.3.2 速率限制采样
每秒最多采样多少个 Span。比如每秒只采 100 个。
# Jaeger 配置
sampler:
type: ratelimiting
param: 100
适合流量波动大的场景。高峰期自动降采样,低峰期全量采集。
3.3.3 远程采样(Jaeger 独有)
这个是我最喜欢的。可以在 Jaeger UI 上动态调整采样率,不用重启服务。
# Jaeger 配置
sampler:
type: remote
param:
samplingServerURL: http://jaeger-agent:5778
我遇到过线上问题,临时把采样率从 0.01 调到 1.0,排查完再调回去。整个过程零重启,太爽了。
3.3.4 自定义采样策略
有时候业务场景特殊,比如只采样「错误请求」或「慢请求」。这时候可以自己写采样器。
// 自定义采样器:只采样耗时超过 1s 的请求
public class SlowRequestSampler implements Sampler {
@Override
public SamplingStatus sample(String operation, TraceId id) {
// 实际项目中,这里需要从上下文中获取耗时
if (isSlowRequest()) {
return SamplingStatus.sampled(Map.of("reason", "slow_request"));
}
return SamplingStatus.notSampled();
}
}
3.4 实战总结
链路追踪这东西,说白了就是「花小钱办大事」。选型别纠结,Span 设计别偷懒,采样策略别省着。我见过太多团队,链路追踪上了半年,结果采样率设成 1.0,存储爆了,最后不得不关掉。
记住一句话:链路追踪不是为了采集所有数据,而是为了在需要的时候能找到数据。
下一章咱们聊聊「分布式日志聚合」,到时候我会分享一个我亲手搭建的 ELK 集群踩坑实录。嗯,那又是另一个故事了。