老铁们,先问你们一个问题:线上接口突然超时,用户投诉群里炸了锅,运维在钉钉里疯狂@你,产品经理端着咖啡站在你工位后面盯着屏幕——你打开日志一看,A服务调了B服务,B服务又调了C服务,C服务说“我没收到请求啊”,B服务说“我发了啊”。十几个微服务,到底是谁的锅?这种时候,你是不是恨不得有个X光机,能把每一次请求的完整路径拍下来?

这就是分布式链路追踪要干的活。今天咱们不扯虚的,就来扒一扒 Java 圈最常用的两个链路追踪工具——ZipkinJaeger——到底谁更适合你。我把架构差异、集成代码、性能对比、生产避坑全给你整明白,看完直接能抄到项目里用!

💡 金句预警:选链路追踪工具就像选对象——Zipkin是那个简单省心的初恋,Jaeger是那个能陪你打硬仗的战友。关键是,你得知道自己的项目是“谈恋爱”还是“过日子”。

一、先别急着选,搞清楚这俩货到底是什么来头

1.1 Zipkin:微服务追踪界的“开山鼻祖”

Zipkin 2012年由Twitter开源,直接受Google那篇著名的Dapper论文启发,可以说是分布式追踪领域的“老大哥”了。它的设计哲学就四个字——简单务实。用Java写的,天生跟Spring生态亲如一家,部署一个JAR包就能跑起来。

当年我刚入行的时候,团队里的链路追踪就是用Zipkin搭的。部署那叫一个省心——docker run一下,Spring Boot里加个依赖,齐活。UI虽然朴素了点,但够用。

但老铁们注意一个关键信号:Zipkin的维护状态相对“佛系”,功能演进比较平稳,社区活跃度不如当年那么生猛了。

1.2 Jaeger:云原生时代的“后起之秀”

Jaeger 2017年由Uber开源,一出生就捐给了CNCF(云原生计算基金会)。Uber那业务规模你们懂的,日均处理千亿级Span,所以Jaeger从基因里就刻着“高并发”和“云原生”两个标签。用Go语言写的,天生对并发友好,部署形态也更贴近Kubernetes生态。

⚠️ 重点:Jaeger v1版本将于2026年1月废弃,2025年12月发布最后一个v1版本。新项目直接上Jaeger v2或走OpenTelemetry路线,别踩坑!

1.3 快速对比:一眼看穿谁是谁

维度 Zipkin Jaeger
诞生时间 2012年(Twitter) 2017年(Uber)
主要语言 Java Go
所属基金会 OpenZipkin(独立) CNCF(云原生计算基金会)
架构模式 一体化(一个进程搞定所有) 模块化(组件可独立部署扩展)
部署复杂度 低,5分钟跑起来 中等,需要理解组件关系
采样策略 概率采样、速率限制 概率采样、速率限制、自适应采样、远程采样
内存占用 ~200MB ~300MB
启动时间 <30秒 ~60秒
最适合 中小规模、Java/Spring团队、快速验证 大规模、云原生、需要高级采样策略

一句话总结:Zipkin追求“开箱即用”,Jaeger追求“弹性伸缩”。

二、架构对撞:一体化 vs 模块化,谁更抗造?

2.1 Zipkin的“All-in-One”模式

Zipkin采用的是一体化架构,所有功能——数据收集、存储、查询、UI——全塞在一个进程里。

┌─────────────────────────────────────────────────┐
│                  Zipkin Server                    │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────┐ │
│  │Collector │→│ Storage  │←│  Query   │→│ UI  │ │
│  └──────────┘ └──────────┘ └──────────┘ └─────┘ │
└─────────────────────────────────────────────────┘

优点:部署简单到令人发指,一个Docker命令搞定,Spring Boot加个依赖就能上报数据。

缺点:所有功能绑在一起,没法独立扩容。数据量上来之后,收集和查询会互相抢资源,一个慢全盘慢。

2.2 Jaeger的“模块化”设计

Jaeger把功能拆成了四个独立组件:

┌────────────┐    ┌────────────┐    ┌────────────┐    ┌────────────┐
│   Agent    │───→│ Collector  │───→│   Storage  │←───│   Query    │
│ (sidecar)  │    │  (接收端)   │    │ (ES/Cass)  │    │  (查询端)   │
└────────────┘    └────────────┘    └────────────┘    └────────────┘
                                                              │
                                                              ↓
                                                       ┌────────────┐
                                                       │    UI      │
                                                       └────────────┘
  • Agent:作为Sidecar或Daemon部署,缓冲上报数据
  • Collector:接收Trace数据,做验证和转换
  • Storage:支持Elasticsearch、Cassandra、Kafka等多种后端
  • Query + UI:查询服务和可视化界面

优点:每个组件可以独立扩展。Collector扛不住了加Collector,Query慢了加Query,互不影响。

缺点:部署和运维复杂度明显更高,新手容易在组件连接上翻车。

💡 魔性比喻:Zipkin像街边的煎饼摊,一个人搞定所有事,简单实惠;Jaeger像中央厨房,和面、烙饼、打包各司其职,量大管饱还能根据订单量随时加人。

三、Java集成实战:手把手保姆级代码(复制就能用!)

光说架构不写代码都是耍流氓。下面我把两种方案的Java集成代码全扒出来,注释写到你能直接拿去跟产品经理炫耀的程度。

3.1 Zipkin + Spring Boot 集成(Sleuth退役后的正确姿势)

🚫 避坑警告:Spring Cloud Sleuth 已经停止维护,官方推荐迁移到Micrometer Tracing + Brave!新项目千万别再用Sleuth了,我上次在公司项目里用了被CTO Code Review喷了一下午……

步骤1:添加Maven依赖
<!-- pom.xml - Zipkin集成所需的核心依赖 -->
<dependencies>
    <!-- 
        ⚠️ 重点:micrometer-tracing-bridge-brave 是关键!
        它是Micrometer Tracing与Brave(Zipkin的Java客户端)的桥接器
        没有它,Trace信息根本传不到Zipkin,我上次就栽这了!
    -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-tracing-bridge-brave</artifactId>
    </dependency>
    
    <!-- 
        💡 技巧:这个依赖负责把Trace数据上报到Zipkin Server
        支持HTTP、Kafka、RabbitMQ三种上报方式
        生产环境强烈建议用Kafka,HTTP同步上报在高并发下是性能杀手!
    -->
    <dependency>
        <groupId>io.zipkin.reporter2</groupId>
        <artifactId>zipkin-reporter-brave</artifactId>
    </dependency>
    
    <!-- 
        Spring Boot Actuator:提供/actuator端点,便于健康检查
        顺便能看Trace信息是否正常注册
    -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
    <!-- 
        Web依赖:提供RestController能力,用于测试链路追踪
    -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

<!-- ⚠️ 重点:Spring Boot版本管理 -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.2.0</version>  <!-- 必须用Spring Boot 3.x,2.x的Sleuth已废弃 -->
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
步骤2:application.yml 配置(每条配置都有故事)
# application.yml - Zipkin配置详解
# 注意:这个配置我踩过3个坑,每条注释都是我半夜被叫起来修Bug的血泪史

spring:
  application:
    name: order-service  # 💡 服务名会出现在Zipkin UI的服务列表中,取个好认的名字!
    
management:
  tracing:
    sampling:
      # ⚠️ 重点:采样率配置!
      # 1.0 = 100%采样,开发环境可以用,生产环境千万别这么玩!
      # 我上次开1.0跑压测,Zipkin直接被Trace数据撑爆,ES磁盘写满,运维差点杀了我
      # 生产环境建议 0.1(10%)或更低,够排查问题就行
      probability: 0.1
      
  endpoints:
    web:
      exposure:
        include: health,info,prometheus  # 暴露actuator端点,便于监控
        
  # 🚫 避坑:Sleuth已死,别再用 spring.sleuth 配置了!
  # 以下配置在新版本中完全无效,会让人怀疑人生
  # spring.sleuth.sampler.probability=1.0  ← 这种写法已经过时,别抄!

# ==================== Zipkin 上报配置 ====================
# ⚠️ 重点:以下配置决定了Trace数据怎么传到Zipkin
# 我有一次因为base-url写错了端口,3天没看到任何Trace数据,最后发现Zipkin跑在9412端口……

zipkin:
  # 💡 Zipkin Server地址,默认localhost:9411
  # 注意:9411是Zipkin的默认端口,不是8080也不是8081!
  base-url: http://localhost:9411
  
  # 🚫 避坑:sender.type 配置项在不同版本中名称不同
  # 有些版本是 zipkin.sender.type,有些是 zipkin.reporter.sender
  # 建议不配置,默认走HTTP,生产环境用Kafka时再配
  # sender:
  #   type: web  # web/http/kafka/rabbit,默认web

# ==================== 日志配置(配合TraceId追踪) ====================
logging:
  pattern:
    # 💡 技巧:在日志格式中加入 traceId 和 spanId
    # 这样你在ELK里搜日志时,能直接用TraceId串联整个调用链!
    # %X{traceId:-} 表示获取MDC中的traceId,没有就显示空
    # %X{spanId:-} 同理
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - [traceId=%X{traceId:-}, spanId=%X{spanId:-}] - %msg%n"
步骤3:启动Zipkin Server(Docker一把梭)
# 🚀 启动Zipkin Server - 最简单的方式
# 这个命令我每次新项目都要敲一遍,已经刻进肌肉记忆了

# 方式一:内存存储(仅限开发/测试,重启数据全丢!)
docker run -d \
  --name zipkin \
  -p 9411:9411 \
  openzipkin/zipkin

# ⚠️ 重点:生产环境必须挂持久化存储!
# 我上次用内存存储跑了3天,容器重启后所有Trace数据灰飞烟灭,
# 老板要看上周的调用链分析,我啥也拿不出来,绩效直接被打折
docker run -d \
  --name zipkin \
  -p 9411:9411 \
  -e STORAGE_TYPE=elasticsearch \
  -e ES_HOSTS=http://elasticsearch:9200 \
  -e ES_INDEX=zipkin \
  openzipkin/zipkin

# 💡 技巧:ES_HOSTS 支持多个节点,用逗号分隔
# -e ES_HOSTS=http://es-node1:9200,http://es-node2:9200,http://es-node3:9200
步骤4:业务代码示例(带完整追踪)
package com.example.orderservice.controller;

import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.Span;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

/**
 * 订单服务Controller
 * 
 * 💡 说明:Micrometer Tracing会自动为每个HTTP请求创建Span
 * 你不需要写任何额外代码,TraceId和SpanId会自动生成并传递!
 * 
 * 当年用Sleuth的时候还要手动搞,现在爽多了
 */
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    private static final Logger log = LoggerFactory.getLogger(OrderController.class);
    
    // ⚠️ 重点:Tracer是手动创建Span的入口,大多数情况不需要手动用
    // 但有些场景(比如异步处理、自定义埋点)必须手动创建
    @Autowired
    private Tracer tracer;
    
    @Autowired
    private RestTemplate restTemplate;
    
    /**
     * 创建订单 - 演示完整的链路追踪
     * 
     * 调用链路:前端 -> order-service -> user-service -> inventory-service
     * 每个服务调用都会自动记录Span
     */
    @PostMapping("/create")
    public String createOrder(@RequestBody OrderRequest request) {
        // 📝 日志会自动带上traceId,这就是前面配置 logging.pattern.console 的功劳
        log.info("收到创建订单请求,用户ID:{},商品ID:{}", 
                 request.getUserId(), request.getProductId());
        
        // 🚫 避坑:RestTemplate必须用@Bean注入,不能用new!
        // 只有通过Spring容器管理的RestTemplate才会被自动拦截添加Trace头
        // 我上次用 new RestTemplate(),Trace链在跨服务时直接断了,查了2天才发现
        
        // 步骤1:调用用户服务校验用户状态
        // 注意:请求头中会自动携带 traceparent 等追踪信息
        String userInfo = restTemplate.getForObject(
            "http://user-service/api/users/" + request.getUserId(), 
            String.class
        );
        log.info("用户服务返回:{}", userInfo);
        
        // 步骤2:调用库存服务扣减库存
        // 💡 技巧:可以手动创建一个子Span,记录更细粒度的操作
        // 这个Span会显示在Zipkin的调用链中,便于分析每一步的耗时
        Span inventorySpan = tracer.nextSpan()
            .name("inventory-check")  // Span名称,会显示在UI中
            .tag("product.id", request.getProductId())  // 添加自定义Tag
            .tag("quantity", String.valueOf(request.getQuantity()))
            .start();
        
        try (var scope = tracer.withSpan(inventorySpan)) {
            String inventoryResult = restTemplate.postForObject(
                "http://inventory-service/api/inventory/deduct",
                request,
                String.class
            );
            log.info("库存服务返回:{}", inventoryResult);
            
            // 💡 技巧:可以添加事件记录关键节点
            inventorySpan.event("inventory-deduct-success");
            
        } catch (Exception e) {
            // ⚠️ 重点:异常时标记Span状态,Zipkin中会高亮显示
            inventorySpan.error(e);
            log.error("库存扣减失败", e);
            throw e;
        } finally {
            // 🚫 避坑:Span必须显式结束,否则会一直挂在内存里!
            // 我见过有人忘了end(),导致内存泄漏,服务跑了3天就OOM了
            inventorySpan.end();
        }
        
        log.info("订单创建成功,订单号:ORD-{}", System.currentTimeMillis());
        return "订单创建成功,订单号:ORD-" + System.currentTimeMillis();
    }
    
    /**
     * 查询订单详情
     */
    @GetMapping("/{orderId}")
    public String getOrder(@PathVariable String orderId) {
        log.info("查询订单详情,订单号:{}", orderId);
        
        // 💡 技巧:获取当前TraceId和SpanId
        // 可以把它们返回给前端,用户报Bug时直接提供,定位问题快10倍
        Span currentSpan = tracer.currentSpan();
        String traceId = currentSpan != null ? currentSpan.context().traceId() : "N/A";
        String spanId = currentSpan != null ? currentSpan.context().spanId() : "N/A";
        
        return String.format("订单详情 - 订单号:%s, TraceId:%s, SpanId:%s", 
                             orderId, traceId, spanId);
    }
}

/**
 * 订单请求DTO
 */
class OrderRequest {
    private String userId;
    private String productId;
    private int quantity;
    
    // getter/setter省略,实际代码记得加上
    public String getUserId() { return userId; }
    public String getProductId() { return productId; }
    public int getQuantity() { return quantity; }
}
步骤5:RestTemplate配置(这个不配,链路必断!)
package com.example.orderservice.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.boot.web.client.RestTemplateBuilder;

/**
 * RestTemplate配置类
 * 
 * 🚫 避坑警告:这个配置是链路追踪能否跨服务传递的关键!
 * 不配置这个,TraceId在跨服务调用时会断,你的调用链就成一节一节的碎链子了
 * 
 * 我当年第一次用Zipkin的时候,就是因为这个问题,调了整整一天
 * 最后发现是因为自己 new 了 RestTemplate,没走Spring容器
 * 被老同事笑了整整一周……
 */
@Configuration
public class RestTemplateConfig {
    
    /**
     * ⚠️ 重点:必须通过@Bean注入RestTemplate!
     * 
     * 原因:Micrometer Tracing的自动配置会拦截所有Spring容器管理的RestTemplate,
     * 自动添加Trace相关的HTTP头(如 traceparent)。
     * 如果你自己 new RestTemplate(),拦截器不会生效,TraceId就传不到下游服务。
     * 
     * 底层原理:Spring Boot自动配置会注入一个 RestTemplateCustomizer,
     * 这个Customizer会给RestTemplate加上 TracingClientHttpRequestInterceptor
     */
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder
                // 设置连接超时,防止上游服务挂了把整个调用链拖死
                .setConnectTimeout(java.time.Duration.ofSeconds(3))
                // 设置读取超时,防止慢SQL把调用链卡住
                .setReadTimeout(java.time.Duration.ofSeconds(10))
                .build();
        // 💡 技巧:builder.build() 会自动应用所有已注册的 RestTemplateCustomizer
        // 包括 TracingClientHttpRequestInterceptor,无需手动添加
    }
}

3.2 Jaeger + OpenTelemetry + Spring Boot 集成(2026年新标准)

💡 重点:现在推荐用OpenTelemetry(OTel)接入Jaeger,而不是Jaeger原生SDK。因为OTel是CNCF统一标准,一次埋点,数据可以同时发给Jaeger、Zipkin、Prometheus等多个后端,避免被单一厂商锁定。

步骤1:添加Maven依赖
<!-- pom.xml - OpenTelemetry + Jaeger 集成所需依赖 -->
<dependencies>
    <!-- 
        ⚠️ 重点:OpenTelemetry Spring Boot Starter
        这是OTel官方提供的Spring Boot自动配置包
        导入后自动完成TracerProvider、SpanExporter等核心组件的配置
    -->
    <dependency>
        <groupId>io.opentelemetry.instrumentation</groupId>
        <artifactId>opentelemetry-spring-boot-starter</artifactId>
        <version>2.17.0</version>
    </dependency>
    
    <!-- 
        💡 技巧:OTLP Exporter - 通过gRPC协议将Trace数据发送到Jaeger Collector
        默认端口4317(gRPC)或4318(HTTP)
        注意:Jaeger v1.35+ 原生支持OTLP协议,不需要额外转换!
    -->
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-otlp</artifactId>
    </dependency>
    
    <!-- 
        如果Jaeger版本较老,不支持OTLP,可以用这个Jaeger专用Exporter
        但新项目强烈建议走OTLP,这是未来标准
    -->
    <!--
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-jaeger</artifactId>
    </dependency>
    -->
    
    <!-- 
        Spring Boot Actuator:必不可少,OTel会自动给Actuator端点加埋点
    -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
步骤2:application.yml 配置(Jaeger版)
# application.yml - OpenTelemetry + Jaeger 配置详解
# 这个配置我参照了官方文档,在生产环境跑了半年,稳如老狗

spring:
  application:
    name: order-service
    
# ==================== OpenTelemetry 核心配置 ====================
# ⚠️ 重点:以下配置决定了OTel SDK的行为
otel:
  # 服务名称,会显示在Jaeger UI中
  service:
    name: ${spring.application.name}
    
  # 🚫 避坑:采样器配置!
  # always_on = 100%采样,生产环境千万别用!
  # 我上次开了always_on,Jaeger的ES存储一天写了几百G,运维直接杀到我工位
  traces:
    sampler:
      # 可选值:always_on / always_off / traceidratio / parentbased_always_on
      # parentbased_traceidratio:如果上游有采样决策则沿用,否则按比例采样
      type: parentbased_traceidratio
      # 采样比例:0.1 = 10%,够用了
      argument: 0.1
      
    # 💡 技巧:导出器配置 - 数据发给谁
    exporter:
      # 使用OTLP协议,这是CNCF标准
      otlp:
        # Jaeger Collector的OTLP gRPC端点
        # 注意:是4317端口,不是14250!
        # 14250是Jaeger原生gRPC端口,4317是OTLP gRPC端口
        endpoint: http://localhost:4317
        # 🚫 避坑:生产环境必须开TLS
        # 否则Trace数据明文传输,包含敏感信息的Tag(比如用户ID)可能泄露
        # insecure: true  # 仅开发环境用!
        
    # 💡 技巧:传播器配置 - 决定Trace信息怎么在服务间传递
    # tracecontext:W3C标准格式(推荐)
    # baggage:跨服务传递自定义键值对
    # b3:兼容Zipkin的老格式,用于混合部署场景
    propagators:
      - tracecontext
      - baggage
      - b3  # 如果你同时用了Zipkin,保留这个可以兼容
        
# ==================== 日志配置(关联TraceId) ====================
logging:
  pattern:
    # 💡 技巧:OTel会自动把trace_id和span_id注入MDC
    # 变量名是 trace_id 和 span_id(注意是下划线,不是Zipkin的驼峰!)
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - [trace_id=%X{trace_id}, span_id=%X{span_id}] - %msg%n"
步骤3:启动Jaeger全套组件(Docker Compose)
# docker-compose.yml - Jaeger全家桶一键启动
# ⚠️ 重点:这个文件我调了3天才稳定,直接抄,别自己瞎改!
version: '3.8'

services:
  # ==================== Jaeger Collector ====================
  # 作用:接收来自Agent或SDK的Trace数据,做验证、转换后写入存储
  jaeger-collector:
    image: jaegertracing/jaeger-collector:1.66
    container_name: jaeger-collector
    restart: unless-stopped
    ports:
      - "14250:14250"   # gRPC端口(Jaeger原生协议)
      - "4317:4317"     # OTLP gRPC端口(OpenTelemetry标准)
      - "4318:4318"     # OTLP HTTP端口
      - "14268:14268"   # HTTP端口(Jaeger原生协议)
    environment:
      # 存储后端:Elasticsearch
      # 🚫 避坑:生产环境必须用ES或Cassandra,内存存储会丢数据!
      - SPAN_STORAGE_TYPE=elasticsearch
      - ES_SERVER_URLS=http://elasticsearch:9200
      # 创建ES索引的配置
      - ES_CREATE_INDEX_TEMPLATES=true
      # 💡 技巧:日志级别设为info,debug太吵了
      - LOG_LEVEL=info
    depends_on:
      - elasticsearch
    networks:
      - jaeger-net

  # ==================== Jaeger Query + UI ====================
  # 作用:查询Trace数据,提供Web UI界面
  jaeger-query:
    image: jaegertracing/jaeger-query:1.66
    container_name: jaeger-query
    restart: unless-stopped
    ports:
      - "16686:16686"   # Jaeger UI默认端口,浏览器访问 http://localhost:16686
      - "16687:16687"   # 管理端口
    environment:
      - SPAN_STORAGE_TYPE=elasticsearch
      - ES_SERVER_URLS=http://elasticsearch:9200
      - LOG_LEVEL=info
      # 💡 技巧:配置UI的默认查询时间范围
      - QUERY_MAX_LOOKBACK=72h
    depends_on:
      - elasticsearch
      - jaeger-collector
    networks:
      - jaeger-net

  # ==================== Elasticsearch(存储后端) ====================
  # 🚫 避坑:Jaeger对ES版本有要求,建议用7.x或8.x
  # 我上次用ES 6.x,索引模板不兼容,Collector疯狂报错
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    container_name: elasticsearch
    restart: unless-stopped
    environment:
      - discovery.type=single-node  # 单节点模式,仅测试用
      - xpack.security.enabled=false  # 关闭安全认证,生产环境必须开!
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g"  # 内存限制,防止把宿主机吃光
    ports:
      - "9200:9200"
    volumes:
      # 💡 技巧:挂载数据卷,防止容器重启丢数据
      - es-data:/usr/share/elasticsearch/data
    networks:
      - jaeger-net

  # ==================== Jaeger Agent(可选) ====================
  # 作用:作为Sidecar部署在应用旁边,缓冲上报数据
  # 如果用OTLP SDK直接上报,这个组件可以省略
  # jaeger-agent:
  #   image: jaegertracing/jaeger-agent:1.66
  #   container_name: jaeger-agent
  #   restart: unless-stopped
  #   ports:
  #     - "5775:5775/udp"
  #     - "6831:6831/udp"
  #     - "6832:6832/udp"
  #     - "5778:5778"
  #   environment:
  #     - REPORTER_GRPC_HOST_PORT=jaeger-collector:14250
  #   depends_on:
  #     - jaeger-collector
  #   networks:
  #     - jaeger-net

networks:
  jaeger-net:
    driver: bridge

volumes:
  es-data:
    driver: local

启动命令:

# 一键启动Jaeger全家桶
docker-compose up -d

# 查看日志确认启动成功
docker-compose logs -f jaeger-collector

# 验证:访问Jaeger UI
# http://localhost:16686
步骤4:业务代码示例(Jaeger版)
package com.example.orderservice.controller;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

/**
 * 订单服务Controller - Jaeger + OpenTelemetry版本
 * 
 * 💡 说明:与Zipkin版本的核心逻辑几乎一样
 * 这就是OpenTelemetry的强大之处——换后端只需要改配置,业务代码不用动!
 */
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    private static final Logger log = LoggerFactory.getLogger(OrderController.class);
    
    // ⚠️ 重点:OpenTelemetry的Tracer(注意包名是 io.opentelemetry.api.trace.Tracer)
    // 不是Micrometer的Tracer!
    @Autowired
    private Tracer tracer;
    
    @Autowired
    private RestTemplate restTemplate;
    
    @PostMapping("/create")
    public String createOrder(@RequestBody OrderRequest request) {
        log.info("收到创建订单请求,用户ID:{},商品ID:{}", 
                 request.getUserId(), request.getProductId());
        
        // 💡 技巧:获取当前Span
        Span currentSpan = Span.current();
        
        // 添加自定义属性(Tag),会在Jaeger UI中显示
        currentSpan.setAttribute("user.id", request.getUserId());
        currentSpan.setAttribute("product.id", request.getProductId());
        currentSpan.setAttribute("order.quantity", request.getQuantity());
        
        // 步骤1:调用用户服务
        String userInfo = restTemplate.getForObject(
            "http://user-service/api/users/" + request.getUserId(), 
            String.class
        );
        log.info("用户服务返回:{}", userInfo);
        
        // 步骤2:创建子Span记录库存操作
        // 💡 技巧:用SpanBuilder创建子Span,更灵活
        Span inventorySpan = tracer.spanBuilder("inventory-check")
                .setParent(io.opentelemetry.context.Context.current())
                .setAttribute("product.id", request.getProductId())
                .setAttribute("quantity", request.getQuantity())
                .startSpan();
        
        // 🚫 避坑:必须用try-with-resources或显式close Scope
        // 否则Span上下文不会正确恢复,后续操作会挂到错误的Span上
        try (Scope scope = inventorySpan.makeCurrent()) {
            String inventoryResult = restTemplate.postForObject(
                "http://inventory-service/api/inventory/deduct",
                request,
                String.class
            );
            log.info("库存服务返回:{}", inventoryResult);
            
            // 💡 技巧:添加事件,记录关键时间点
            inventorySpan.addEvent("inventory.deduct.completed");
            
        } catch (Exception e) {
            // ⚠️ 重点:记录异常,Jaeger中会高亮显示
            inventorySpan.recordException(e);
            inventorySpan.setAttribute("error", true);
            inventorySpan.setAttribute("error.message", e.getMessage());
            log.error("库存扣减失败", e);
            throw e;
        } finally {
            inventorySpan.end();
        }
        
        // 获取TraceId返回给前端
        String traceId = Span.current().getSpanContext().getTraceId();
        log.info("订单创建成功,订单号:ORD-{},TraceId:{}", 
                 System.currentTimeMillis(), traceId);
        
        return String.format("订单创建成功,订单号:ORD-%d,TraceId:%s", 
                             System.currentTimeMillis(), traceId);
    }
    
    @GetMapping("/{orderId}")
    public String getOrder(@PathVariable String orderId) {
        log.info("查询订单详情,订单号:{}", orderId);
        
        Span span = Span.current();
        span.setAttribute("order.id", orderId);
        
        // 💡 技巧:返回TraceId,方便前端定位问题
        String traceId = span.getSpanContext().getTraceId();
        return String.format("订单详情 - 订单号:%s, TraceId:%s", orderId, traceId);
    }
}

class OrderRequest {
    private String userId;
    private String productId;
    private int quantity;
    
    public String getUserId() { return userId; }
    public String getProductId() { return productId; }
    public int getQuantity() { return quantity; }
}

四、性能对撞:谁在高并发下更抗造?

4.1 基准数据对比

根据多个来源的测试数据,整理出以下对比:

性能指标 Zipkin Jaeger
内存占用(空载) ~200MB ~300MB
CPU占用(1000 TPS) 较高 较低(低约15%)
处理能力(Span/秒) 中等
查询性能 良好 优秀
扩展性 中等

实测数据显示,相同配置下,Jaeger处理每秒10万span时CPU占用率比Zipkin低15%左右,但内存消耗高出约20%。这是因为Jaeger的Go语言实现天然对并发更友好,而Zipkin的JVM本身就有一定的内存开销。

4.2 存储瓶颈

Zipkin的存储坑:Zipkin默认内存存储不适合生产环境,使用Elasticsearch集群存储时需避免写入热点,建议调整索引分片数并开启ILM策略。另外Zipkin内置了一个信号量来限制并发写入ES,高并发时可能会丢弃部分数据。

Jaeger的存储坑:Jaeger的可扩展性受限于后端存储的性能,在高分布式、高流量系统中可能成为瓶颈。Kafka作为缓冲层的方案虽然复杂,但能有效缓解存储压力。

五、避坑清单:生产环境踩过的血泪坑

🚫 坑1:采样率开100%,存储直接爆炸

我当年的血泪史:为了调试方便,把采样率设成了100%,结果ES磁盘一天写了800G,运维凌晨3点打电话骂我,最后扣了500块绩效。

解决方案:生产环境采样率控制在5%~10%就够用了。Jaeger还支持自适应采样(Adaptive Sampling),流量低时多采、高时少采,自动平衡。

🚫 坑2:自己new RestTemplate,链路全断

新人最容易犯的错误:图方便直接 new RestTemplate() 调用下游服务,结果TraceId传不过去,调用链碎成渣。

解决方案:必须通过Spring容器注入RestTemplate,让Tracing拦截器自动生效。

🚫 坑3:Jaeger版本混乱,协议不兼容

Jaeger v1即将在2026年1月废弃,但很多老教程还在教用Jaeger原生的Thrift协议。新项目直接用OpenTelemetry + OTLP协议,少走弯路。

解决方案:统一用OpenTelemetry SDK,通过OTLP协议上报Jaeger Collector。

🚫 坑4:Span忘了end(),内存泄漏到OOM

手动创建的Span必须显式调用end(),否则Span对象一直挂在那里,GC回收不掉,时间长了直接OOM。

解决方案:用try-with-resources或finally块确保Span一定被关闭。

🚫 坑5:日志里没有TraceId,排查问题抓瞎

光有链路追踪,日志里没带TraceId,出了问题还是得手动关联,效率极低。

解决方案:配置日志格式,把%X{trace_id}%X{span_id}加进去,ELK里一搜一个准。

六、选型决策:到底该用谁?

选Zipkin,如果你:

  • ✅ 团队规模小,运维能力有限,想要5分钟跑起来的方案
  • ✅ 技术栈是纯Java/Spring,希望最小化学习成本
  • ✅ 流量不大(每天百万级请求以内),单体式部署就够了
  • ✅ 只想要基础的可视化,不追求高级分析功能

总结:Zipkin适合“快速验证”和“中小规模”,是分布式追踪入门的绝佳选择。

选Jaeger,如果你:

  • ✅ 微服务数量多(10+),调用链路复杂
  • ✅ 流量大(每天千万级以上),需要弹性伸缩能力
  • ✅ 团队拥抱云原生,跑在Kubernetes上
  • ✅ 需要高级采样策略和丰富的可视化(服务依赖图、火焰图)
  • ✅ 未来考虑拥抱OpenTelemetry标准

总结:Jaeger适合“生产级”和“云原生”,是大规模微服务追踪的首选。

2026年的最佳实践

💡 金句:选链路追踪工具不是选终身伴侣,而是选交通工具——短途骑单车(Zipkin),长途开汽车(Jaeger),别拿单车跑高速!

无论你选哪个,强烈建议直接上OpenTelemetry。因为:

  1. OpenTelemetry是CNCF的统一标准,一次埋点,数据可以同时发给Jaeger、Zipkin、Prometheus
  2. Spring Cloud Sleuth已经停更,新项目必须走OTel路线
  3. 换后端只需要改配置,业务代码完全不用动

推荐技术栈:OpenTelemetry SDK(Java Agent或手动埋点) + Jaeger Collector(存储用Elasticsearch)+ Jaeger UI / Grafana

七、总结与互动

核心要点速记

选Zipkin 选Jaeger
部署快,5分钟搞定 弹性好,组件独立扩缩
内存占用小,约200MB 功能全,自适应采样
适合Java/Spring团队 适合云原生/K8s环境
学习曲线平缓 高级特性丰富

🎤 评论区见!

老铁们,你们在生产环境用过哪个链路追踪工具?踩过什么坑?有没有更奇葩的死锁或者Trace断链的故事?


更多推荐