10 分钟接入分布式链路追踪:OpenTelemetry Agent 零侵入 + Jaeger 可视化
引言
"这个请求慢了,具体慢在哪一步?"
如果你的回答是"看日志猜",那说明你的系统还没有链路追踪。
分布式链路追踪是可观测性三支柱中最快见效的一个——不改动一行业务代码,加上一个 JVM 参数,立刻就能在 UI 上看到请求在各个服务之间的调用链路。
本文纯实战,10 分钟,三步搞定。然后逐步深入自定义埋点、跨服务传递、采样策略。
一、三步接入:零侵入方案
1.1 架构预览
┌──────────────┐ ┌──────────────┐
│ order-service│ │ user-service │
│ (8080) │ │ (8081) │
│ │ HTTP │ │
│ GET /orders │────────→│ GET /users │
│ ↓ │ │ ↓ │
│ OTel Agent │ │ OTel Agent │
│ (自动埋点) │ │ (自动埋点) │
└──────┬───────┘ └──────┬───────┘
│ │
│ OTLP (gRPC) │
└────────┬───────────────┘
│
▼
┌───────────────┐
│ OTel Collector│
│ (4317) │
└───────┬───────┘
│
▼
┌───────────────┐
│ Jaeger │
│ UI (16686) │
└───────────────┘
1.2 第一步:启动 Jaeger + Collector
用 Docker 一键启动 Jaeger all-in-one(内置 Collector + 存储 + UI):
docker run -d --name jaeger \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
-e COLLECTOR_OTLP_ENABLED=true \
jaegertracing/all-in-one:1.60
端口说明:
| 端口 | 用途 |
|---|---|
| 16686 | Jaeger UI |
| 4317 | OTLP gRPC(Agent 发送数据到这里) |
| 4318 | OTLP HTTP |
验证:打开 http://localhost:16686,看到 Jaeger UI 即成功。
1.3 第二步:下载 OpenTelemetry Java Agent
# 下载最新版 Agent
curl -L -o opentelemetry-javaagent.jar \
https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
# 验证下载
ls -lh opentelemetry-javaagent.jar
# -rw-r--r-- 1 user staff 32M opentelemetry-javaagent.jar
一个 32MB 的 jar 包,这就是全部。不用改 pom.xml,不用加依赖,不用改代码。
1.4 第三步:加 JVM 参数启动
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.exporter.otlp.endpoint=http://localhost:4317 \
-Dotel.traces.exporter=otlp \
-jar order-service.jar
三个参数就够了:
| 参数 | 说明 |
|---|---|
-javaagent:opentelemetry-javaagent.jar | 挂载 Agent |
-Dotel.service.name=order-service | 服务名(显示在 Jaeger 中) |
-Dotel.exporter.otlp.endpoint=http://localhost:4317 | 数据发到哪 |
启动另一个服务:
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=user-service \
-Dotel.exporter.otlp.endpoint=http://localhost:4317 \
-Dotel.traces.exporter=otlp \
-jar user-service.jar
1.5 看!调用链来了
发一个请求:
curl http://localhost:8080/orders/1
打开 Jaeger UI → Service 下拉选 order-service → Find Traces:
Trace: GET /orders/1 (总耗时 85ms)
order-service GET /orders/1 85ms
├─ order-service SELECT order_db 12ms
└─ user-service GET /users/1 45ms
└─ user-service SELECT user_db 8ms
没有改一行代码,HTTP 调用、数据库查询全部自动埋点。
二、Agent 自动埋点了什么
OpenTelemetry Agent 基于 ByteBuddy 字节码增强,自动拦截以下组件:
| 类别 | 自动埋点的库 |
|---|---|
| HTTP 服务端 | Spring MVC, JAX-RS, Servlet, Spring WebFlux |
| HTTP 客户端 | OkHttp, Apache HttpClient, RestTemplate, WebClient, Feign |
| 数据库 | JDBC, Hibernate, JPA, MongoDB, Redis, MyBatis |
| 消息队列 | Kafka, RabbitMQ, RocketMQ, Pulsar |
| RPC | gRPC, Dubbo |
| 日志 | Logback, Log4j2(自动注入 traceId) |
一句话:你用的主流库基本都覆盖了。
查看自动注入的日志
Agent 会自动把 traceId 和 spanId 注入 MDC:
<!-- logback-spring.xml -->
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{trace_id}] [%X{span_id}] %-5level %logger - %msg%n</pattern>
日志输出效果:
2026-08-01 14:23:15 [http-nio-8080-exec-1] [4a3b2c1d8e9f0a1b] [2c3d4e5f6a7b8c9d] INFO OrderController - 查询订单: id=1
trace_id 和 span_id 可以直接在 Jaeger UI 中搜索,实现日志和链路的关联。
三、自定义 Span 埋点
自动埋点覆盖了框架层,但业务逻辑需要手动埋点。
3.1 @WithSpan 注解
最简单的方式,加一个注解就有 Span:
import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.instrumentation.annotations.SpanAttribute;
@Service
public class OrderService {
@WithSpan("order-service.createOrder")
public Order createOrder(
@SpanAttribute("order.userId") String userId,
@SpanAttribute("order.productName") String productName,
@SpanAttribute("order.amount") BigDecimal amount) {
// 这个方法会自动成为一个 Span
// 参数值会作为 Span 属性显示在 Jaeger 中
validateOrder(userId, productName);
Order order = saveOrder(userId, productName, amount);
publishEvent(order);
return order;
}
@WithSpan("order-service.validateOrder")
private void validateOrder(
@SpanAttribute("userId") String userId,
@SpanAttribute("productName") String productName) {
// 校验逻辑
}
}
Jaeger 中看到的效果:
Trace: POST /orders (总耗时 120ms)
order-service POST /orders 120ms
├─ order-service order-service.createOrder 95ms
│ ├─ order-service order-service.validateOrder 3ms
│ ├─ order-service SELECT order_db 15ms
│ └─ order-service Kafka send 20ms
└─ user-service GET /users/1 25ms
点击 createOrder Span,能看到属性:
Span Attributes:
order.userId = "user001"
order.productName = "MacBook Pro"
order.amount = 12999.00
3.2 手动创建 Span
需要更精细控制时,手动 API:
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Scope;
@Service
public class PaymentService {
private final Tracer tracer = GlobalOpenTelemetry.getTracer("payment-service");
public PaymentResult processPayment(Order order) {
// 手动创建 Span
Span span = tracer.spanBuilder("payment.processPayment")
.setAttribute("payment.orderNo", order.getOrderNo())
.setAttribute("payment.amount", order.getAmount().doubleValue())
.setAttribute("payment.method", order.getPaymentMethod())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 调用支付网关
PaymentResult result = callPaymentGateway(order);
span.setAttribute("payment.result", result.getStatus());
return result;
} catch (Exception e) {
// 记录异常到 Span
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
}
3.3 @WithSpan vs 手动 API
| 维度 | @WithSpan | 手动 API |
|---|---|---|
| 简洁度 | 一个注解搞定 | 5-10 行代码 |
| 灵活性 | 仅方法级别 | 任意代码块 |
| 异常处理 | 自动记录异常 | 手动 recordException |
| 属性设置 | @SpanAttribute 注解 | setAttribute 方法 |
| 适用场景 | Service 层方法 | 复杂业务逻辑块 |
建议:优先用 @WithSpan,不够灵活时再用手动 API。
四、跨服务传递 Baggage
4.1 什么是 Baggage
Trace:记录"请求在哪些服务中经过了"
Baggage:记录"请求附带的业务上下文"
典型场景:用户 ID、租户 ID 在请求入口提取,后续所有服务都能读到。
用户请求(Header: X-User-Id=user001, X-Tenant-Id=tenantA)
↓
order-service → 提取 userId/tenantId 放入 Baggage
↓
user-service → 从 Baggage 读取 userId/tenantId
↓
payment-service → 从 Baggage 读取 userId/tenantId
4.2 代码实现
入口服务设置 Baggage:
import io.opentelemetry.api.baggage.Baggage;
import io.opentelemetry.api.baggage.BaggageEntry;
import io.opentelemetry.context.Context;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/orders")
@WithSpan("order-controller.createOrder")
public Result<Order> createOrder(
@RequestHeader("X-User-Id") String userId,
@RequestHeader("X-Tenant-Id") String tenantId,
@RequestBody CreateOrderRequest request) {
// 将业务上下文存入 Baggage,后续服务自动传递
Baggage.current()
.toBuilder()
.put("user.id", userId)
.put("tenant.id", tenantId)
.build()
.makeCurrent();
return Result.success(orderService.createOrder(userId, request.getProductName(), request.getAmount()));
}
}
下游服务读取 Baggage:
import io.opentelemetry.api.baggage.Baggage;
@Service
public class UserService {
@WithSpan("user-service.getUserInfo")
public UserInfo getUserInfo(@SpanAttribute("userId") String userId) {
// 从 Baggage 读取上游传递的上下文
String tenantId = Baggage.current().getEntryValue("tenant.id");
String currentUserId = Baggage.current().getEntryValue("user.id");
log.info("处理用户请求: userId={}, tenantId={}", currentUserId, tenantId);
// 根据 tenantId 做多租户隔离
return userRepository.findByIdAndTenantId(userId, tenantId);
}
}
4.3 Baggage 在 Jaeger 中查看
Jaeger UI 中,每个 Span 都能看到关联的 Baggage:
Span: user-service.getUserInfo (15ms)
Span Attributes:
userId = "user001"
Baggage:
user.id = "user001"
tenant.id = "tenantA"
4.4 Baggage vs Span Attribute
| 维度 | Span Attribute | Baggage |
|---|---|---|
| 作用域 | 仅当前 Span | 跨服务传递 |
| 传递方式 | 不传递 | 随 Trace Context 自动传递 |
| 数据量 | 无限制 | 有限制(HTTP Header 大小) |
| 适用场景 | Span 级别的属性 | 全链路共享的业务上下文 |
注意:Baggage 会随每个 HTTP 请求传递,不要放大数据。
五、采样策略配置
5.1 为什么要采样
10000 QPS × 每个请求 10 个 Span = 100,000 Span/秒
全量采集的话,Jaeger 存储扛不住,网络带宽也是问题。采样是必须的。
5.2 三种采样策略
策略一:概率采样(默认)
按比例采样,适合大部分场景:
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.exporter.otlp.endpoint=http://localhost:4317 \
-Dotel.traces.sampler=parentbased_traceidratio \
-Dotel.traces.sampler.arg=0.1 \
-jar order-service.jar
| 参数 | 说明 |
|---|---|
parentbased_traceidratio | 基于父 Span 决定是否采样,子 Span 跟随父 Span |
0.1 | 采样率 10%,10 个请求采 1 个 |
原理:如果父 Span 被采样了,子 Span 一定被采样,保证链路完整。
策略二:强制采样
特定接口强制采样(不依赖概率):
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.samplers.Sampler;
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
// 自定义采样器
public class CustomSampler implements Sampler {
private final Sampler baseSampler;
public CustomSampler(double ratio) {
this.baseSampler = Sampler.traceIdRatioBased(ratio);
}
@Override
public SamplingResult shouldSample(
Context parentContext,
String traceId,
String name,
SpanKind spanKind,
Attributes attributes,
List<LinkData parentLinks) {
// 支付接口强制采样
if (name.contains("payment") || name.contains("Payment")) {
return SamplingResult.create(true);
}
// 其他接口走概率采样
return baseSampler.shouldSample(
parentContext, traceId, name, spanKind, attributes, parentLinks);
}
@Override
public String getDescription() {
return "CustomSampler";
}
}
策略三:错误全采
正常请求采样 10%,错误请求 100% 采集:
// 结合自定义采样器 + 全局异常处理
@RestControllerAdvice
public class TraceExceptionAdvice {
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
// 获取当前 Span,标记为采样
Span currentSpan = Span.current();
// 记录异常
currentSpan.recordException(e);
currentSpan.setStatus(StatusCode.ERROR, e.getMessage());
// 设置属性:标记为错误请求
currentSpan.setAttribute("error", true);
currentSpan.setAttribute("error.message", e.getMessage());
return Result.fail(500, "系统错误");
}
}
更优雅的方案:通过 OTel SDK 配置 tail-based sampling(尾部采样),在 Span 结束后再决定是否采样:
# otel-collector-config.yml(Collector 端配置)
processors:
tail_sampling:
decision_wait: 30s
num_traces: 50000
policies:
# 错误请求全采
- name: errors
type: status_code
status_code:
status_codes: [ERROR]
# 慢请求全采(>500ms)
- name: slow
type: latency
latency:
threshold_ms: 500
# 其他请求 10% 采样
- name: baseline
type: probabilistic
probabilistic:
sampling_percentage: 10
5.3 采样策略对比
| 策略 | 采样率 | 存储 | 适用场景 |
|---|---|---|---|
| 概率采样 10% | 10% | 低 | 日常监控 |
| 强制采样 | 指定接口 100% | 中 | 支付/关键接口 |
| 错误全采 | 错误 100% + 正常 10% | 中 | 排障优先 |
| 慢请求全采 | 慢 100% + 正常 10% | 中 | 性能优化 |
| 全量采集 | 100% | 高 | 低流量/调试 |
建议:生产环境用"错误全采 + 慢请求全采 + 正常概率采样"组合,既能排障又控制成本。
六、OTel Agent vs Spring Boot Micrometer Tracing
Spring Boot 3.x 提供了 Micrometer Tracing 原生方案,和 OTel Agent 怎么选?
6.1 接入方式对比
OTel Agent 方案(零侵入):
# 不改代码,不改 pom.xml
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-jar app.jar
Micrometer Tracing 方案(原生集成):
<!-- pom.xml 加依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</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>
# application.yml
management:
tracing:
sampling:
probability: 0.1
otlp:
tracing:
endpoint: http://localhost:4317
6.2 全面对比
| 维度 | OTel Agent | Micrometer Tracing |
|---|---|---|
| 代码侵入 | 零侵入 | 需加依赖 + 配置 |
| 自动埋点范围 | 广(所有主流库) | 中(Spring 生态为主) |
| 自定义埋点 | @WithSpan 注解 | @Observed + MDC |
| 配置灵活度 | JVM 参数控制 | application.yml 控制 |
| 性能开销 | 中(ByteBuddy 增强) | 低(原生集成) |
| 调试难度 | 高(字节码增强黑盒) | 低(源码可调试) |
| 多语言支持 | 是(Agent 支持 11+ 语言) | 否(仅 Java) |
| 升级维护 | 换 Agent jar 即可 | 改 pom.xml 版本 |
| 生产稳定性 | 成熟,大厂验证 | Spring 官方维护 |
6.3 怎么选
选 OTel Agent 如果:
✅ 已有项目,不想改代码
✅ 多语言微服务架构
✅ 需要快速接入,快速见效
✅ 团队对 OTel 生态有了解
选 Micrometer Tracing 如果:
✅ 新项目,Spring Boot 3.x
✅ 只有 Java 技术栈
✅ 追求原生集成,不想依赖 Agent
✅ 需要 Micrometer 生态(Metrics + Tracing 统一)
6.4 两种方案可以共存
# 用 Micrometer Tracing 做业务层埋点
# 同时挂 OTel Agent 做框架层自动埋点
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.exporter.otlp.endpoint=http://localhost:4317 \
-jar app.jar
但要注意:需要在 Agent 配置中排除 Spring 相关的自动埋点,避免重复:
-Dotel.instrumentation.spring-web.enabled=false
-Dotel.instrumentation.spring-boot-actuator.enabled=false
七、生产环境最佳实践
7.1 Agent 参数配置模板
java -javaagent:opentelemetry-javaagent.jar \
\
# ========== 基础配置 ==========
-Dotel.service.name=order-service \
-Dotel.service.version=1.0.0 \
-Dotel.resource.attributes=deployment.environment=production,host.name=prod-node-01 \
\
# ========== 导出配置 ==========
-Dotel.traces.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
-Dotel.exporter.otlp.timeout=5000 \
\
# ========== 采样配置 ==========
-Dotel.traces.sampler=parentbased_traceidratio \
-Dotel.traces.sampler.arg=0.1 \
\
# ========== 性能配置 ==========
-Dotel.bsp.schedule.delay=5000 \
-Dotel.bsp.max.queue.size=2048 \
-Dotel.bsp.max.export.batch.size=512 \
\
# ========== 日志关联 ==========
-Dotel.instrumentation.common.mdc.resource-keys=service.name,deployment.environment \
-Dotel.instrumentation.common.mdc.span-attributes=userId,tenantId \
\
-jar order-service.jar
参数说明:
| 参数 | 说明 |
|---|---|
bsp.schedule.delay | 批量发送间隔,5 秒 |
bsp.max.queue.size | 队列大小,防止 OOM |
bsp.max.export.batch.size | 每批发送 512 条 |
mdc.span-attributes | 自动注入 MDC 的 Span 属性 |
7.2 生产架构
应用 (Agent) → OTel Collector → Jaeger / Tempo
│
├── 采样过滤(尾部采样)
├── 数据增强(添加 K8s 信息)
└── 限流保护
生产环境不要让 Agent 直接发数据到 Jaeger,中间加 Collector 做缓冲和过滤。
7.3 性能影响
| 指标 | 无 Agent | 有 Agent(10% 采样) | 影响 |
|---|---|---|---|
| CPU | 基准 | +2-3% | 低 |
| 内存 | 基准 | +30-50MB | 低 |
| 请求延迟 | 基准 | +1-2ms | 可忽略 |
| GC 频率 | 基准 | 略增 | 可忽略 |
结论:10% 采样率下,Agent 对性能的影响在可接受范围内。
八、常见问题
Q1:Jaeger 里看不到 Trace?
排查清单:
# 1. 确认 Agent 挂载成功
java -javaagent:opentelemetry-javaagent.jar -version
# 应该输出 Agent 版本信息
# 2. 确认 Collector 地址正确
curl http://localhost:4317 # 应该有响应
# 3. 确认服务名设置了
# Jaeger UI 的 Service 下拉框里有没有你的服务名
# 4. 确认采样率不是 0
-Dotel.traces.sampler.arg=1.0 # 临时设为 100% 测试
# 5. 查看 Agent 日志
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.javaagent.debug=true \
-jar app.jar 2>&1 | grep -i "export\|error"
Q2:跨服务 Trace 链断了?
期望:
order-service → user-service(同一 Trace)
实际:
order-service(Trace A)
user-service(Trace B) ← 断了
原因:HTTP 客户端没被 Agent 拦截,或自定义客户端没传递 Trace Context。
解决:确保 HTTP 客户端在 Agent 支持列表中。如果用自定义客户端,手动注入 Header:
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapSetter;
// 注入 Trace Context 到 HTTP Header
TextMapSetter<HttpURLConnection> setter = (carrier, key, value) -> {
carrier.setRequestProperty(key, value);
};
Context currentContext = Context.current();
GlobalOpenTelemetry.getPropagators()
.getTextMapPropagator()
.inject(currentContext, connection, setter);
Q3:Agent 日志太多?
# 关闭 Agent 调试日志
-Dotel.javaagent.debug=false
# 只保留错误日志
-Dio.opentelemetry.context.enableStrictContext=false
九、总结
接入步骤速查
第一步:启动 Jaeger
docker run -d -p 16686:16686 -p 4317:4317 jaegertracing/all-in-one:1.60
第二步:下载 Agent
curl -L -o opentelemetry-javaagent.jar https://...
第三步:加参数启动
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=xxx \
-Dotel.exporter.otlp.endpoint=http://localhost:4317 \
-jar app.jar
进阶路线
Level 1:零侵入接入(本文第一步)
→ Agent 自动埋点,Jaeger 看调用链
Level 2:自定义埋点(本文第三节)
→ @WithSpan + @SpanAttribute 标注业务方法
Level 3:跨服务传递(本文第四节)
→ Baggage 传递 userId/tenantId
Level 4:采样策略(本文第五节)
→ 错误全采 + 慢请求全采 + 正常概率采样
Level 5:生产部署(本文第七节)
→ Agent → Collector → Jaeger,尾部采样 + 限流
方案选型
| 场景 | 推荐方案 |
|---|---|
| 快速接入/已有项目 | OTel Agent(零侵入) |
| 新项目/Spring Boot 3.x | Micrometer Tracing(原生集成) |
| 多语言架构 | OTel Agent(统一方案) |
| 深度定制 | OTel SDK(手动配置) |
互动话题:你的项目用什么做链路追踪?有没有遇到过 Trace 断链的问题?欢迎留言讨论!
参考资料
标题:10 分钟接入分布式链路追踪:OpenTelemetry Agent 零侵入 + Jaeger 可视化
作者:jiangyi
地址:http://jiangyi.space/articles/2026/08/04/1785576055463.html
公众号:服务端技术精选
- 引言
- 一、三步接入:零侵入方案
- 1.1 架构预览
- 1.2 第一步:启动 Jaeger + Collector
- 1.3 第二步:下载 OpenTelemetry Java Agent
- 1.4 第三步:加 JVM 参数启动
- 1.5 看!调用链来了
- 二、Agent 自动埋点了什么
- 查看自动注入的日志
- 三、自定义 Span 埋点
- 3.1 @WithSpan 注解
- 3.2 手动创建 Span
- 3.3 @WithSpan vs 手动 API
- 四、跨服务传递 Baggage
- 4.1 什么是 Baggage
- 4.2 代码实现
- 4.3 Baggage 在 Jaeger 中查看
- 4.4 Baggage vs Span Attribute
- 五、采样策略配置
- 5.1 为什么要采样
- 5.2 三种采样策略
- 策略一:概率采样(默认)
- 策略二:强制采样
- 策略三:错误全采
- 5.3 采样策略对比
- 六、OTel Agent vs Spring Boot Micrometer Tracing
- 6.1 接入方式对比
- 6.2 全面对比
- 6.3 怎么选
- 6.4 两种方案可以共存
- 七、生产环境最佳实践
- 7.1 Agent 参数配置模板
- 7.2 生产架构
- 7.3 性能影响
- 八、常见问题
- Q1:Jaeger 里看不到 Trace?
- Q2:跨服务 Trace 链断了?
- Q3:Agent 日志太多?
- 九、总结
- 接入步骤速查
- 进阶路线
- 方案选型
- 参考资料
评论