Zipkin vs Jaeger:Java程序员的链路追踪选型血泪史,我替你们把坑踩完了!
老铁们,先问你们一个问题:线上接口突然超时,用户投诉群里炸了锅,运维在钉钉里疯狂@你,产品经理端着咖啡站在你工位后面盯着屏幕——你打开日志一看,A服务调了B服务,B服务又调了C服务,C服务说“我没收到请求啊”,B服务说“我发了啊”。十几个微服务,到底是谁的锅?这种时候,你是不是恨不得有个X光机,能把每一次请求的完整路径拍下来?
这就是分布式链路追踪要干的活。今天咱们不扯虚的,就来扒一扒 Java 圈最常用的两个链路追踪工具——Zipkin和Jaeger——到底谁更适合你。我把架构差异、集成代码、性能对比、生产避坑全给你整明白,看完直接能抄到项目里用!
💡 金句预警:选链路追踪工具就像选对象——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。因为:
- OpenTelemetry是CNCF的统一标准,一次埋点,数据可以同时发给Jaeger、Zipkin、Prometheus
- Spring Cloud Sleuth已经停更,新项目必须走OTel路线
- 换后端只需要改配置,业务代码完全不用动
推荐技术栈:OpenTelemetry SDK(Java Agent或手动埋点) + Jaeger Collector(存储用Elasticsearch)+ Jaeger UI / Grafana
七、总结与互动
核心要点速记
| 选Zipkin | 选Jaeger |
|---|---|
| 部署快,5分钟搞定 | 弹性好,组件独立扩缩 |
| 内存占用小,约200MB | 功能全,自适应采样 |
| 适合Java/Spring团队 | 适合云原生/K8s环境 |
| 学习曲线平缓 | 高级特性丰富 |
🎤 评论区见!
老铁们,你们在生产环境用过哪个链路追踪工具?踩过什么坑?有没有更奇葩的死锁或者Trace断链的故事?
更多推荐


所有评论(0)