我用 OpenTelemetry 搭了一套全链路追踪,再也不用猜哪个服务慢了

摘要:微服务架构下排查一个慢请求需要翻 N 个服务的日志。本文记录用 OpenTelemetry + Jaeger 搭建全链路追踪的完整过程,包含自动埋点、自定义 Span、日志关联和告警规则,附可落地的配置和代码。

一次线上慢请求的排查噩梦

"用户反馈订单列表加载了十几秒。" 运营在群里发了个截图,然后@了后端。

我们的系统已经拆成了 8 个微服务,一个 /api/orders 请求的路径是:网关 → 订单服务 → 用户服务 → 库存服务 → 优惠券服务 → 数据库。没有任何调用链追踪,我只能逐个服务翻日志,凭时间戳手动对齐。那次排查花了将近两个小时,最后发现是优惠券服务调用库存的一个 SQL 没有走索引。

事后我在团队群里说了一句话:"再不做全链路追踪,下次线上故障我们就等着背锅吧。"

于是花了一个礼拜,用 OpenTelemetry + Jaeger 把这套体系搭了起来。现在排查一个慢请求,只需要在 Jaeger UI 里点一下,哪个 Span 慢、哪个调用出错,一目了然。


选型:为什么用 OpenTelemetry

市面上做分布式追踪的框架很多,但 2026 年的趋势已经非常明确:OpenTelemetry 是事实标准。它是 CNCF 孵化项目,由 OpenTracing 和 OpenCensus 合并而来,各大云厂商和 APM 厂商都支持。

我选的组合:

  • OpenTelemetry SDK(自动埋点 + 手动埋点)
  • Jaeger(存储和展示 Trace)
  • Spring Boot 3(我们的微服务框架)

第一步:部署 Jaeger

最简单的部署方式是 Docker:

yaml 复制代码
# docker-compose.yml
version: '3.8'
services:
  jaeger:
    image: jaegertracing/all-in-one:1.58
    ports:
      - "16686:16686"   # Jaeger UI
      - "4317:4317"     # OTLP gRPC 接收端口
    environment:
      - COLLECTOR_OTLP_ENABLED=true
      - SPAN_STORAGE_TYPE=badger
      - BADGER_EPHEMERAL=false
      - BADGER_DIRECTORY_VALUE=/badger/data
      - BADGER_DIRECTORY_KEY=/badger/key
    volumes:
      - jaeger_data:/badger
    restart: unless-stopped

volumes:
  jaeger_data:

SPAN_STORAGE_TYPE=badger 用本地嵌入式存储,适合中小规模。如果每天产生上亿 Span,需要换成 Elasticsearch 或 ClickHouse。

启动后访问 http://localhost:16686 就能看到 Jaeger UI。


第二步:Spring Boot 接入 OpenTelemetry

Spring Boot 3 对 OpenTelemetry 有很好的支持。我们不需要手动写 SDK 代码,只需引入依赖和配置。

添加依赖

xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-api</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-spring-boot-starter</artifactId>
    <version>2.5.0</version>
</dependency>

Spring Boot 的自动配置会处理好 Bean 的创建。

配置文件

yaml 复制代码
# application.yml
spring:
  application:
    name: order-service   # 每个服务用自己的名字

otel:
  traces:
    exporter: otlp
  exporter:
    otlp:
      endpoint: http://localhost:4317   # Jaeger OTLP 接收端
  resource:
    attributes:
      service.name: ${spring.application.name}
      deployment.environment: dev
  instrumentation:
    spring-webmvc:
      enabled: true
    spring-data:
      enabled: true

重启服务后,任何 HTTP 请求和数据库操作都会自动产生 Span,无需一行代码。访问 http://localhost:8080/api/orders/123,然后打开 Jaeger,选择 order-service,已经能看到一条完整的调用链。

自动埋点覆盖的范围

  • HTTP 请求(Spring MVC Controller)
  • 数据库查询(Spring Data JPA)
  • RestTemplate / WebClient 远程调用
  • Kafka 消息发送和接收
  • Scheduled 定时任务

第三步:手动埋点------记录关键业务逻辑

自动埋点只覆盖了框架层面,业务逻辑内部的耗时细节需要手动 Span。

注入 Tracer

java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private Tracer tracer;
    
    public OrderDetail queryOrderWithDetails(String orderId) {
        Span span = tracer.spanBuilder("queryOrderWithDetails")
                .setAttribute("order.id", orderId)
                .startSpan();
        try (Scope scope = span.makeCurrent()) {
            
            // 查订单基本信息
            Order order = getOrder(orderId);
            
            // 查用户信息(手动Span包裹远程调用)
            User user = getUserWithTrace(order.getUserId());
            
            // 查优惠信息
            List<Coupon> coupons = getCoupons(orderId);
            
            return assembleResult(order, user, coupons);
            
        } catch (Exception e) {
            span.recordException(e);
            span.setStatus(StatusCode.ERROR, "查询订单详情失败");
            throw e;
        } finally {
            span.end();
        }
    }
    
    private User getUserWithTrace(Long userId) {
        Span span = tracer.spanBuilder("getUserInfo")
                .setAttribute("user.id", userId)
                .startSpan();
        try (Scope scope = span.makeCurrent()) {
            return userClient.getUser(userId);  // Feign 调用用户服务
        } finally {
            span.end();
        }
    }
}

在 Jaeger 中,queryOrderWithDetails 这个 Span 会展示其内部的子 Span,每个子 Span 的耗时清清楚楚。如果 getUserInfo 花了 800ms,一眼就能看到。


第四步:跨服务传递 Trace 上下文

微服务之间的调用(如订单服务 → 用户服务),需要自动传递 traceparent 头。OpenTelemetry 对 Feign 和 RestTemplate 有自动集成。

Feign 集成

xml 复制代码
<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-spring-cloud-feign-client</artifactId>
    <version>2.5.0</version>
</dependency>

无需额外配置,Feign 发出的请求会自动带上 traceparent 头,下游服务接收到后继续使用同一个 Trace ID。

验证方法:在 Jaeger 中搜索同一个 Trace ID,能看到跨越多个服务的完整调用链。


第五步:把日志和 Trace 关联起来

排查问题时,既要看 Span 的耗时,也要看那段时间的日志。如果日志里也带上 Trace ID 和 Span ID,就能在 Jaeger 里直接跳转到日志平台(我们用 ELK)。

在日志里注入 Trace 信息

OpenTelemetry 自动往 MDC(Mapped Diagnostic Context)里放了 trace_idspan_id,在 logback-spring.xml 里配置即可:

xml 复制代码
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>
            %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} 
            traceId=%mdc{trace_id} spanId=%mdc{span_id} 
            - %msg%n
        </pattern>
    </encoder>
</appender>

输出效果:

ini 复制代码
14:32:15.123 [http-nio-8080-exec-1] INFO  OrderService 
traceId=abc123def456 spanId=789ghi 
- 开始查询订单详情, orderId=1001

现在在 ELK 里搜 traceId=abc123def456,就能拿到这个请求的所有日志,和 Jaeger 里的 Span 一一对应。


第六步:设置采样策略

全量采集在生产环境成本很高,需要设置采样率。OpenTelemetry 支持多种采样策略:

yaml 复制代码
otel:
  traces:
    sampler:
      probability: 0.1   # 10% 采样率(开发环境 1.0,生产环境 0.1)

也可以基于规则动态采样------对错误请求全采,正常请求按比例:

java 复制代码
@Configuration
public class TraceConfig {
    @Bean
    public Sampler sampler() {
        return Sampler.parentBased(Sampler.traceIdRatioBased(0.1));
    }
}

更高级的做法是"尾部采样"(Tail-Based Sampling),在 Jaeger Collector 端根据请求是否出错或延迟超过阈值决定保留哪些 Trace,但需要额外配置 Jaeger 的 --sampling.strategies-file


第七步:监控和告警

有了 Trace 数据,可以基于 Span 的耗时或错误率设置告警。我们用 Prometheus 的 opentelemetry-metrics 暴露指标,然后在 Grafana 配置告警。

关键指标:

  • http_server_duration_milliseconds:HTTP 请求延迟的 P95/P99
  • span_count_by_status:Span 成功/失败计数

示例 Grafana 告警:某个接口的 P99 延迟超过 2 秒持续 5 分钟,发 Slack 通知。


实际效果

接入后,日常排查慢请求的时间从平均 30 分钟降到 3 分钟。一个真实的例子:

某天中午,库存服务的 /checkStock 接口延迟飙到 5 秒。打开 Jaeger,搜索该服务的请求,按延迟排序,发现一个 Trace 里 checkStock 内部调用了 getWarehouseInfo 花了 4.8 秒。点进去看 Span 属性,发现 warehouseId 是 0(异常值)。原来前端有个 bug 会传空的仓库 ID,后端没有做参数校验,导致全表扫描。定位 + 修复只用了 10 分钟。


踩过的坑

  1. Span 泄露 :手动 Span 忘记 end() 导致内存泄漏。解决:始终用 try-finally 或在 Spring 中用 @WithSpan 注解(需要额外切面配置)。

  2. 异步线程丢失上下文 :在 CompletableFuture 里调远程服务,Trace 上下文丢失。需要手动传递:

java 复制代码
Context context = Context.current();
CompletableFuture.supplyAsync(() -> {
    try (Scope scope = context.makeCurrent()) {
        // 执行异步操作
    }
});

或者用 OpenTelemetry 的 @Async 集成。

  1. Jaeger 存储膨胀 :Badger 存储几天就上百 GB。生产环境一定要设置 TTL(--badger.span-store-ttl=72h)或换 Elasticsearch。

搭建检查清单

markdown 复制代码
## OpenTelemetry + Jaeger 接入清单

### 基础设施
- [ ] 部署 Jaeger(Docker / K8s)
- [ ] 配置存储后端(Badger / ES)
- [ ] 确认 OTLP 端口可访问

### 微服务接入
- [ ] 引入 opentelemetry-spring-boot-starter 依赖
- [ ] 配置 otel exporter endpoint
- [ ] 设置 service.name
- [ ] 验证自动埋点(HTTP / 数据库)

### 手动埋点
- [ ] 为关键业务逻辑创建 Span
- [ ] 为远程调用添加子 Span
- [ ] 在日志 MDC 中输出 traceId/spanId

### 跨服务传递
- [ ] Feign / RestTemplate 自动传递 traceparent
- [ ] 在 Jaeger 中验证完整调用链

### 运维
- [ ] 设置采样率(生产 0.1)
- [ ] 配置 Grafana 告警(P99 延迟 / 错误率)
- [ ] 设置 Jaeger 数据 TTL

总结

分布式追踪不是"锦上添花"的工具,而是微服务架构下必备的基础设施。它的价值在于:当系统出问题时,你不需要靠经验猜,而是直接看数据。 OpenTelemetry + Jaeger 这个组合,用最低的成本实现了最大化的可观测性。

如果你的服务超过 3 个,还没有全链路追踪,这周就把它加上。下次线上故障时,你会感谢自己。


你的项目里是怎么做全链路追踪的?踩过哪些坑?欢迎评论区交流。

相关推荐
DreamLife☼1 小时前
OpenBCI-机器学习入门:从脑电信号到模式识别
人工智能·机器学习·开源硬件·脑机接口·eeg·openbci·神经科技
钓了猫的鱼儿1 小时前
基于深度学习+AI的葡萄目标检测与预警系统(Python源码+数据集+UI可视化界面+YOLOv11训练结果)
人工智能·深度学习·目标检测
木雷坞1 小时前
Open WebUI 连不上 Ollama:Docker Compose 排查记录
人工智能·docker·ai编程
AndrewHZ1 小时前
【LLM技术全景】Transformer架构深度解析:Encoder-Decoder全理解
人工智能·深度学习·语言模型·大模型·llm·transformer·编解码技术
Stick_ZYZ1 小时前
从项目启动到 Milvus 向量检索,我把 RAG 项目链路又打通了一层
java·人工智能·经验分享·ai·milvus
沫儿笙1 小时前
新能源汽车焊接智能节气装置
人工智能·机器人·汽车
阿昌喜欢吃黄桃1 小时前
大模型常见参数学习笔记
人工智能·ai·llm·prompt·token
程序员cxuan1 小时前
太顶了,ChatGPT 要和 Codex 搞一起了。
人工智能·后端·程序员
weixin_446260851 小时前
ClinEnv:面向Agent的交互式多阶段电子健康记录(EHR)环境
人工智能