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

端口说明:

端口用途
16686Jaeger UI
4317OTLP gRPC(Agent 发送数据到这里)
4318OTLP 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
RPCgRPC, Dubbo
日志Logback, Log4j2(自动注入 traceId)

一句话:你用的主流库基本都覆盖了。

查看自动注入的日志

Agent 会自动把 traceIdspanId 注入 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_idspan_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 AttributeBaggage
作用域仅当前 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 AgentMicrometer 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.xMicrometer Tracing(原生集成)
多语言架构OTel Agent(统一方案)
深度定制OTel SDK(手动配置)

互动话题:你的项目用什么做链路追踪?有没有遇到过 Trace 断链的问题?欢迎留言讨论!


参考资料


标题:10 分钟接入分布式链路追踪:OpenTelemetry Agent 零侵入 + Jaeger 可视化
作者:jiangyi
地址:http://jiangyi.space/articles/2026/08/04/1785576055463.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消