摘要:微服务架构下排查一个慢请求需要翻 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_id 和 span_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/P99span_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 分钟。
踩过的坑
-
Span 泄露 :手动 Span 忘记
end()导致内存泄漏。解决:始终用 try-finally 或在 Spring 中用@WithSpan注解(需要额外切面配置)。 -
异步线程丢失上下文 :在
CompletableFuture里调远程服务,Trace 上下文丢失。需要手动传递:
java
Context context = Context.current();
CompletableFuture.supplyAsync(() -> {
try (Scope scope = context.makeCurrent()) {
// 执行异步操作
}
});
或者用 OpenTelemetry 的 @Async 集成。
- 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 个,还没有全链路追踪,这周就把它加上。下次线上故障时,你会感谢自己。
你的项目里是怎么做全链路追踪的?踩过哪些坑?欢迎评论区交流。