字数:约4500字 | 阅读时间:12分钟
“微服务架构下,一次请求跨越十几个服务,你凭什么快速定位问题?”


引言:可观测性为什么重要

在单体应用时代,定位问题很简单:看日志、查数据库、重启服务。三板斧下去,十有八九能找到根因。

但在微服务架构下,一次用户请求可能跨越十几个服务节点。一个接口超时,你得知道是哪个节点慢、哪个环节卡、哪个依赖在作妖。

传统的日志和监控已经不够用了。你需要的是全链路追踪——把一次请求在所有服务间的流转路径完整记录下来。

这就是 OpenTelemetry(以下简称 OTel)要解决的核心问题。

本文从实战出发,讲解如何在 Java 应用中接入 OTel,实现从代码埋点到 Jaeger 可视化的完整链路。

可观测性三支柱:Metrics、Logs、Traces

在说 OTel 之前,先理清三个概念:

1. Metrics(指标)
聚合后的数值,如CPU使用率、QPS、延迟P99。Prometheus 是这类数据的代表。

2. Logs(日志)
离散的事件记录,如”用户登录成功”、”订单创建失败”。结构化日志(JSON)是主流。

3. Traces(链路追踪)
一次请求的端到端路径,每个span记录了耗时和上下文。Jaeger、Zipkin 是这类数据的可视化工具。

这三个维度不是替代关系,而是互补的。OTel 的设计目标就是统一收集这三类数据,一次接入,全部搞定。

OpenTelemetry 架构

OTel 由四个核心组件构成:

1
2
3
4
5
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ SDK/Agent │────▶│ OTel │────▶│ Backend │
│ (埋点) │ │ Collector │ │ (Jaeger/ │
│ │ │ │ │ Tempo) │
└─────────────┘ └─────────────┘ └─────────────┘

SDK/Agent:应用端埋点库,Java 用 opentelemetry-java,Spring Boot 用 opentelemetry-spring-boot-starter

OTel Collector:接收器、处理器、导出器的组合。可以做过滤、聚合、格式转换。生产环境推荐独立部署。

Backend:存储和可视化,Jaeger(追踪)、Prometheus(指标)、Grafana(展示)。

方案一:Java Agent 无侵入接入

最快的方式是用 Java Agent,不需要改代码,只需要在启动时加参数:

1
2
3
4
5
6
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.metrics.exporter=otlp \
-Dotel.traces.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://collector:4317 \
-jar order-service.jar

这个 Agent 会自动为常见的 RPC 框架(Dubbo、gRPC、OkHttp)添加埋点,覆盖 Spring Boot、Spring Cloud 默认组件。

优点:零代码改动,5分钟接入。
缺点:无法埋入自定义业务逻辑,粒度较粗。

方案二:SDK 手动埋点(精准控制)

依赖引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.44.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.44.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<version>1.44.0</version>
</dependency>

SDK 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;

public class OtelConfig {
public static OpenTelemetry init() {
Resource resource = Resource.getDefault()
.merge(Resource.builder()
.put("service.name", "order-service")
.put("service.version", "1.0.0")
.build());

OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
.setEndpoint("http://collector:4317")
.build();

SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build())
.setResource(resource)
.build();

return OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.build();
}
}

业务代码埋点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;

@Service
public class OrderService {

private final Tracer tracer;

public OrderService() {
this.tracer = OtelConfig.init().getTracer("order-service", "1.0.0");
}

public Order createOrder(OrderDTO dto) {
Span span = tracer.spanBuilder("OrderService.createOrder")
.setAttribute("order.amount", dto.getAmount())
.setAttribute("order.userId", dto.getUserId())
.startSpan();

try (Scope ignored = span.makeCurrent()) {
// 业务逻辑
Order order = orderRepository.save(convertToEntity(dto));

span.setAttribute("order.id", order.getId());
span.setAttribute("order.status", order.getStatus());

return order;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.error, e.getMessage());
throw e;
} finally {
span.end();
}
}
}

关联ID传播:让请求串联起来

全链路追踪的核心是关联ID(TraceId)。每个请求入口生成一个 TraceId,后续所有服务调用都带着这个 ID,这样 Jaeger 才能把一次请求的所有span串联起来。

HTTP 入口处生成TraceId

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Component
public class TracingFilter extends OncePerRequestFilter {

@Autowired
private OpenTelemetry openTelemetry;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {

// 从请求头中提取 trace context(如果前端已传递)
Context extracted = openTelemetry.getPropagators()
.getTextMapPropagator()
.extract(Context.current(), request,
new TextMapGetter<HttpServletRequest>() {
@Override
public Iterable<String> keys(HttpServletRequest carrier) {
return () -> Collections.enumeration(request.getHeaderNames());
}
@Override
public String get(HttpServletRequest carrier, String key) {
return carrier.getHeader(key);
}
});

// 注入到当前 Context
try (Scope scope = extracted.makeCurrent()) {
chain.doFilter(request, response);
}

// 将 TraceId 写入响应头,方便前端排查
Span currentSpan = Span.current();
response.addHeader("X-Trace-Id", currentSpan.getSpanContext().getTraceId());
}
}

内部服务间传递(Feign Client)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class FeignConfig {

@Autowired
private OpenTelemetry openTelemetry;

@Bean
public RequestInterceptor tracingInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 将当前 TraceId 注入到下游请求头
openTelemetry.getPropagators().getTextMapPropagator()
.inject(Context.current(), template,
(t, k, v) -> t.header(k, v));
}
};
}
}

MQ 消息传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public class OrderMessageProducer {

@Autowired
private OpenTelemetry openTelemetry;

public void sendOrderCreatedEvent(Order order) {
Span span = tracer.spanBuilder("sendOrderCreatedEvent").startSpan();

try (Scope ignored = span.makeCurrent()) {
// 注入 trace context 到消息头
Map<String, String> headers = new HashMap<>();
openTelemetry.getPropagators().getTextMapPropagator()
.inject(Context.current(), headers, Map::put);

messageTemplate.convertAndSend("order.events",
order,
m -> {
headers.forEach(m::setHeader);
return m;
});
} finally {
span.end();
}
}
}

消费端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class OrderMessageConsumer {

@Autowired
private OpenTelemetry openTelemetry;

@KafkaListener(topics = "order.events")
public void onOrderCreated(Message<Order> message) {
// 从消息头提取 trace context
Context extracted = openTelemetry.getPropagators().getTextMapPropagator()
.extract(Context.current(), message.getHeaders(),
(carrier, key) -> {
Object val = message.getHeaders().get(key);
return val != null ? val.toString() : null;
});

try (Scope ignored = extracted.makeCurrent()) {
processOrder(message.getPayload());
}
}
}

Jaeger 可视化实战

部署 Jaeger(docker-compose):

1
2
3
4
5
6
7
8
9
10
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:1.57
ports:
- "16686:16686" # UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
environment:
COLLECTOR_OTLP_ENABLED: true

访问 http://localhost:16686,可以看到 Trace 列表。搜索条件可以用:

  • Service Name:服务名
  • Operation Name:方法名
  • Trace ID:精确查找
  • Tags:k=v 格式,如 http.status_code=500

火焰图(Trace Flame Graph)

点击一个 Trace,可以看到每个 span 的耗时占比。颜色块越大,耗时越长。这是最快速的瓶颈定位方式。

Span 详情

点击任意 span,可以查看:

  • Tags:业务属性(如 orderId、userId)
  • Events:关键事件(如”数据库查询”、”远程调用”)
  • Logs:异常堆栈
  • References:父子 span 关系

性能瓶颈分析方法

1. 找出耗时最长的 Span

在 Jaeger 中,按 Duration 排序,找到 P99 超标的请求。展开火焰图,看哪个颜色块最大。

2. 检查是否存在跨服务同步调用

如果一个 span 的子span全部是同步串行调用,说明存在不必要的等待。优化方向:并行化、异步化。

3. 发现热点数据库查询

在 span 的 Tags 中,搜索 db.statement,可以看到每次数据库查询的耗时。如果某个查询的耗时占比超过30%,考虑加索引或优化SQL。

4. 检查网络开销

RPC 调用的耗时如果超过50ms,且数据量不大(<1MB),说明可能存在网络路由问题或跨地域调用。检查服务部署拓扑,尽量同机房调用。

最佳实践

1. 采样策略
生产环境全量采集代价太高,推荐采样策略:

  • always_on:开发环境全量
  • traceidratio:生产环境按比例采样,如10%
  • rarest尾部关键请求采样,延迟超过阈值的请求强制保留
1
2
3
4
5
6
7
// 配置尾部采样
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(SpanProcessor.fromConfig(
SpanProcessorConfig.builder()
.addFilter(span -> span.getDuration().toMillis() > 1000) // >1s保留
.build()))
.build();

2. 结构化 Tag
埋点时注意 Tag 的键值规范,方便后续查询:

  • 服务名、版本、节点:用 resource 属性
  • 业务属性:用 span tag(如 orderId、userId)
  • 避免埋入敏感信息(密码、token、明文手机号)

3. Collector 独立部署
生产环境不要让应用直连 Jaeger,通过 OTel Collector 中转,可以做过滤、聚合、重试。建议用 Kubernetes 部署,配置自动扩缩容。

4. 关联日志
在 span 的 Tags 中写入 log.filelog.line,方便在 ELK 中直接跳转到对应日志行。

总结

本文从可观测性的三支柱出发,介绍了 OpenTelemetry 在 Java 应用中的接入方式:

  • Java Agent:零代码,5分钟接入,适合快速验证
  • SDK 手动埋点:精准控制,适合业务关键路径
  • 关联ID传播:HTTP/Feign/MQ 全覆盖,保证链路不断
  • Jaeger 可视化:火焰图、Span详情、尾部采样
  • 性能瓶颈分析:从耗时分布到数据库热点

微服务架构下,可观测性不是可选项,是生存技能。没有追踪,定位问题靠猜;有了追踪,定位问题靠数据。

你的下一个生产问题,应该在 Jaeger 里解决。


tags: [OpenTelemetry, 分布式追踪, 可观测性, Jaeger, 微服务]