一、引言
分布式链路追踪是微服务架构中不可或缺的监控手段。本文将带你从零开始,用 OpenTelemetry 为你的 Java 微服务添加分布式链路追踪能力。
核心目标:10 分钟内完成从零到可视化的完整链路追踪搭建。
二、5 分钟快速入门
2.1 启动 Jaeger
创建 docker-compose.yml:
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:1.51
ports:
- "16686:16686"
- "4317:4317"
- "4318:4318"
environment:
- COLLECTOR_OTLP_ENABLED=true
启动:
docker-compose up -d
打开浏览器访问 http://localhost:16686,你会看到 Jaeger 界面。
2.2 启动你的应用(Java Agent 方式)
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.traces.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://localhost:4317 \
-Dotel.metrics.exporter=none \
-jar your-app.jar
效果:无需修改任何代码,自动追踪所有 HTTP 请求、数据库调用、消息队列等。
三、10 分钟深入:手动埋点
3.1 添加依赖
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.32.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
<version>1.32.0</version>
<scope>provided</scope>
</dependency>
3.2 @WithSpan 注解
@Slf4j
@Service
public class OrderService {
@WithSpan("createOrder")
public OrderDTO createOrder(OrderCreateRequest request) {
// 业务逻辑
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
// 调用其他服务
paymentService.pay(order.getId(), order.getAmount());
return OrderDTO.from(order);
}
@WithSpan("calculateAmount")
public BigDecimal calculateAmount(Long productId, Integer quantity) {
Product product = productService.getById(productId);
return product.getPrice().multiply(BigDecimal.valueOf(quantity));
}
}
3.3 自定义属性
@Slf4j
@Service
public class OrderService {
@Autowired
private Tracer tracer;
@WithSpan("createOrder")
public OrderDTO createOrder(OrderCreateRequest request) {
Span span = Span.current();
span.setAttribute("userId", request.getUserId());
span.setAttribute("productId", request.getProductId());
span.setAttribute("quantity", request.getQuantity());
// 业务逻辑
Order order = orderRepository.save(buildOrder(request));
span.addEvent("订单创建成功", Attributes.of(
AttributeKey.stringKey("orderId"), order.getId().toString()
));
return OrderDTO.from(order);
}
}
3.4 手动创建 Span
@Slf4j
@Service
public class OrderService {
@Autowired
private Tracer tracer;
public OrderDTO createOrder(OrderCreateRequest request) {
Span span = tracer.spanBuilder("createOrder")
.setAttribute("userId", request.getUserId())
.setAttribute("productId", request.getProductId())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 业务逻辑
Order order = orderRepository.save(buildOrder(request));
Span childSpan = tracer.spanBuilder("sendNotification")
.setParent(Context.current().with(span))
.startSpan();
try (Scope childScope = childSpan.makeCurrent()) {
notificationService.send(order.getId());
} finally {
childSpan.end();
}
return OrderDTO.from(order);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
}
四、跨服务传递 TraceId
4.1 HTTP 调用自动传递
使用 Java Agent 后,HTTP 客户端(RestTemplate、WebClient、HttpClient)会自动传递 TraceId:
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
@WithSpan("callPaymentService")
public void callPaymentService(Long orderId, BigDecimal amount) {
PaymentRequest request = new PaymentRequest(orderId, amount);
// TraceId 会自动通过 HTTP 头传递
restTemplate.postForObject(
"http://payment-service/api/pay",
request,
PaymentResponse.class
);
}
}
自动传递的 HTTP 头:
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
tracestate: vendor1=value1,vendor2=value2
4.2 手动传递(非 HTTP 场景)
@Service
public class OrderService {
@Autowired
private Tracer tracer;
@Autowired
private RabbitTemplate rabbitTemplate;
@WithSpan("sendOrderCreatedEvent")
public void sendOrderCreatedEvent(Long orderId) {
Span span = Span.current();
// 获取当前 TraceContext
SpanContext context = span.getSpanContext();
String traceId = context.getTraceId();
String spanId = context.getSpanId();
boolean isSampled = context.isSampled();
// 手动传递到消息体
OrderEvent event = new OrderEvent();
event.setOrderId(orderId);
event.setTraceId(traceId);
event.setSpanId(spanId);
event.setSampled(isSampled);
rabbitTemplate.convertAndSend("order-exchange", "order.created", event);
}
}
@Service
public class OrderEventListener {
@Autowired
private Tracer tracer;
@RabbitListener(queues = "order-queue")
public void handleOrderEvent(OrderEvent event) {
// 重建 SpanContext
SpanContext parentContext = SpanContext.createFromRemoteParent(
event.getTraceId(),
event.getSpanId(),
event.isSampled() ? TraceFlags.getSampled() : TraceFlags.getDefault(),
TraceState.getDefault()
);
// 创建子 Span
Span span = tracer.spanBuilder("handleOrderEvent")
.setParent(Context.current().with(parentContext))
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 业务逻辑
orderService.processOrderEvent(event);
} finally {
span.end();
}
}
}
五、采样策略配置
5.1 环境变量配置
# 采样率 100%
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.traces.sampler=always_on \
-jar your-app.jar
# 采样率 10%
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.traces.sampler=traceidratio \
-Dotel.traces.sampler.arg=0.1 \
-jar your-app.jar
# 从不采样(生产环境默认)
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.traces.sampler=always_off \
-jar your-app.jar
5.2 基于属性的采样
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.sdk.trace.samplers.SamplingDecision;
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
import java.util.List;
public class CustomSampler implements Sampler {
@Override
public SamplingResult shouldSample(
Context context,
String traceId,
String name,
SpanKind spanKind,
Attributes attributes,
List<io.opentelemetry.api.trace.Link> parentLinks) {
// 对关键业务的请求进行全量采样
if (name.contains("createOrder") || name.contains("payment")) {
return SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE);
}
// 对错误请求进行全量采样
String statusCode = attributes.get(AttributeKey.stringKey("http.status_code"));
if ("500".equals(statusCode)) {
return SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE);
}
// 默认采样 10%
return Math.random() < 0.1
? SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE)
: SamplingResult.create(SamplingDecision.DROP);
}
@Override
public String getDescription() {
return "CustomSampler";
}
}
六、完整示例:Spring Boot 应用
6.1 pom.xml
<project>
<groupId>com.example</groupId>
<artifactId>order-service</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.32.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
<version>1.32.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
6.2 application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/example_db
username: admin
password: password
jpa:
hibernate:
ddl-auto: update
logging:
pattern:
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
6.3 启动脚本
#!/bin/bash
AGENT_VERSION="1.32.0"
AGENT_JAR="opentelemetry-javaagent.jar"
# 下载 agent(如果不存在)
if [ ! -f "$AGENT_JAR" ]; then
curl -L -O "https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v$AGENT_VERSION/$AGENT_JAR"
fi
# 启动应用
java -javaagent:$AGENT_JAR \
-Dotel.service.name=order-service \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=none \
-Dotel.exporter.otlp.endpoint=http://localhost:4317 \
-Dotel.traces.sampler=always_on \
-jar target/order-service-1.0.0.jar
七、附录:Spring Boot 3.x + Micrometer Tracing 对比
7.1 什么是 Micrometer Tracing
Micrometer Tracing 是 Spring Boot 3.x 中引入的分布式追踪抽象层,它可以使用 OpenTelemetry、Zipkin、Jaeger 等作为后端实现。
7.2 快速接入(Spring Boot 3.x)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
spring:
application:
name: order-service
tracing:
sampling:
probability: 1.0
management:
tracing:
sampling:
probability: 1.0
metrics:
export:
prometheus:
enabled: true
7.3 特性对比
| 特性 | OpenTelemetry Java Agent | Micrometer Tracing |
|---|---|---|
| 自动插桩深度 | 深(数据库、MQ、HTTP 等) | 中等(主要是 Spring 生态) |
| 配置复杂度 | 高(环境变量/配置文件) | 低(Spring Boot 自动配置) |
| 自定义能力 | 强(API 丰富) | 中等(通过 Tracer 接口) |
| 厂商锁定 | 无(标准协议) | 无(可切换后端) |
| 迁移路径 | 从 Java Agent 到手动埋点 | 从 Micrometer 切换后端 |
| 学习曲线 | 较陡 | 较平缓(Spring 生态) |
7.4 如何选择
选择建议:
┌─────────────────────────────────────────────────────┐
│ │
│ 场景: │
│ │
│ 1. 快速接入,零代码修改 → OpenTelemetry Java Agent │
│ ├── 优点:无需修改代码,自动追踪 │
│ └── 缺点:配置复杂,灵活性有限 │
│ │
│ 2. Spring Boot 3.x 项目 → Micrometer Tracing │
│ ├── 优点:与 Spring 生态无缝集成,配置简单 │
│ └── 缺点:自动插桩深度有限 │
│ │
│ 3. 需要深度自定义 → 手动埋点 + OpenTelemetry API │
│ ├── 优点:灵活性最高,完全可控 │
│ └── 缺点:需要手动编写代码 │
│ │
└─────────────────────────────────────────────────────┘
7.5 混合使用
混合使用方案:
┌─────────────────────────────────────────────────────┐
│ │
│ Java Agent(自动追踪基础链路) │
│ + │
│ Micrometer Tracing(Spring 生态集成) │
│ + │
│ 手动埋点(关键业务逻辑) │
│ │
│ 优点:自动 + 手动相结合,兼顾效率和灵活性 │
│ │
└─────────────────────────────────────────────────────┘
八、总结
8.1 关键知识点
关键知识点:
1. Java Agent 方式:零代码修改,自动追踪
2. @WithSpan 注解:手动标记关键方法
3. Span 自定义属性:添加业务上下文(userId、orderId)
4. 跨服务传递:HTTP 自动传递,非 HTTP 手动传递
5. 采样策略:控制追踪数据量,平衡性能和可观测性
8.2 最佳实践
最佳实践:
┌─────────────────────────────────────────────────────┐
│ │
│ 1. 生产环境使用采样策略(避免数据量过大) │
│ 2. 关键业务路径添加自定义属性(便于排查问题) │
│ 3. 使用 @WithSpan 代替手动创建 Span(代码更简洁) │
│ 4. 错误场景进行全量采样(便于分析问题) │
│ 5. 配合日志使用 TraceId(MDC 自动注入) │
│ │
└─────────────────────────────────────────────────────┘
8.3 下一步
下一步:
1. 在你的应用中尝试 Java Agent 方式
2. 添加几个 @WithSpan 注解
3. 在关键方法中添加自定义属性
4. 配置采样策略
5. 查看 Jaeger 可视化效果
💡 互动话题:你在项目中使用过分布式链路追踪吗?遇到过什么问题?欢迎在评论区分享!
