字数:约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 { 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); } }); try (Scope scope = extracted.makeCurrent()) { chain.doFilter(request, response); } 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) { 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()) { 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) { 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" - "4317:4317" - "4318:4318" 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) .build())) .build();
|
2. 结构化 Tag
埋点时注意 Tag 的键值规范,方便后续查询:
- 服务名、版本、节点:用 resource 属性
- 业务属性:用 span tag(如 orderId、userId)
- 避免埋入敏感信息(密码、token、明文手机号)
3. Collector 独立部署
生产环境不要让应用直连 Jaeger,通过 OTel Collector 中转,可以做过滤、聚合、重试。建议用 Kubernetes 部署,配置自动扩缩容。
4. 关联日志
在 span 的 Tags 中写入 log.file、log.line,方便在 ELK 中直接跳转到对应日志行。
总结
本文从可观测性的三支柱出发,介绍了 OpenTelemetry 在 Java 应用中的接入方式:
- Java Agent:零代码,5分钟接入,适合快速验证
- SDK 手动埋点:精准控制,适合业务关键路径
- 关联ID传播:HTTP/Feign/MQ 全覆盖,保证链路不断
- Jaeger 可视化:火焰图、Span详情、尾部采样
- 性能瓶颈分析:从耗时分布到数据库热点
微服务架构下,可观测性不是可选项,是生存技能。没有追踪,定位问题靠猜;有了追踪,定位问题靠数据。
你的下一个生产问题,应该在 Jaeger 里解决。
tags: [OpenTelemetry, 分布式追踪, 可观测性, Jaeger, 微服务]