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 生态。
我的小建议: 别在选型上花太多时间。两个都跑个 demo,哪个顺手用哪个。我见过太多团队「选型选半年,上线两三天」。

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 设计原则

我总结了几条经验,你直接拿去用:

  1. 一个 Span 只做一件事。别把「查数据库 + 调外部 API + 写缓存」塞进一个 Span。拆开,每个操作一个 Span。
  2. Span 的粒度不要太细。比如循环 1000 次调用,别生成 1000 个 Span。用 batch 模式合并。
  3. 关键路径加 Tag。比如数据库查询的 SQL 语句、HTTP 请求的 URL。排查时一眼就能看出问题。
  4. 异常一定要记录。在 Span 的 logs 里记录错误堆栈,别只打个 error=true 的 Tag。
我曾经踩过的坑: 有个服务每次调用都生成几十个 Span,结果 Jaeger 的存储直接爆了。后来加了采样策略,又把不必要的 Span 合并了,才稳住。

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 集群踩坑实录。嗯,那又是另一个故事了。