微服务架构下的分布式追踪——OpenTelemetry 在 Spring 生态的落地
微服务架构下的分布式追踪——OpenTelemetry 在 Spring 生态的落地
一、分布式追踪的核心价值
在微服务架构中,单个用户请求可能跨越十几个服务节点,经历 MQ 异步处理、定时任务触发、缓存穿透等多条路径。当问题发生时,如果没有分布式追踪能力,定位根因常常需要逐一排查每个服务的日志,效率极低。
分布式追踪的核心价值在于:将一次完整请求的所有跨服务调用串联为一条 Trace,以 Span 为单位记录每个服务节点的处理耗时,最终通过 Trace ID 实现全链路回溯。
从技术演进来看,分布式追踪经历了 Zipkin(2012)→ Jaeger(2017)→ OpenTelemetry(2021)三个阶段。OpenTelemetry 作为 CNCF 的孵化项目,已经成为事实上的统一标准——它将 Trace、Metrics 和 Logs 三大观测信号统一到了一套 API 和 SDK 中。
二、Spring Boot 集成 OpenTelemetry
Spring Boot 从 3.x 版本开始原生支持 OpenTelemetry 的自动装配,但 2.x 版本仍然需要通过手动集成来接入。
/** * OpenTelemetry 配置——在 Spring Boot 2.x 环境下通过手动装配实现分布式追踪。 * * 为什么使用 SdkTracerProvider 而非 GlobalOpenTelemetry: * GlobalOpenTelemetry 是单例模式,不适合 Spring 容器的生命周期管理, * SdkTracerProvider 可以配合 Spring 的 @PreDestroy 实现优雅关闭。 */ @Configuration public class OpenTelemetryConfig { @Bean public OpenTelemetry openTelemetry() { // Span 处理器——将 Span 数据导出到 Jaeger Collector SpanExporter jaegerExporter = JaegerGrpcSpanExporter.builder() .setEndpoint("http://jaeger-collector:14250") // 为什么设置 10s 超时:避免 Span 导出失败时阻塞应用线程, // Span 导出是后台异步操作,失败后 BatchSpanProcessor 会自动重试 .setTimeout(Duration.ofSeconds(10)) .build(); // 为什么使用 BatchSpanProcessor 而非 SimpleSpanProcessor: // SimpleSpanProcessor 每个 Span 结束后立即导出,高并发下会产生大量网络IO; // BatchSpanProcessor 按批次(256条或5s间隔)批量导出,大幅减少网络开销 BatchSpanProcessor spanProcessor = BatchSpanProcessor.builder( jaegerExporter) .setMaxQueueSize(2048) .setMaxExportBatchSize(256) .setScheduleDelay(Duration.ofSeconds(5)) .build(); // 资源定义——标识服务的元信息 Resource resource = Resource.getDefault() .merge(Resource.create( Attributes.of( ResourceAttributes.SERVICE_NAME, "order-service", ResourceAttributes.SERVICE_VERSION, "1.5.0", ResourceAttributes.DEPLOYMENT_ENVIRONMENT, String.valueOf(getEnvironment()) ))); SdkTracerProvider tracerProvider = SdkTracerProvider.builder() .addSpanProcessor(spanProcessor) .setResource(resource) .build(); return OpenTelemetrySdk.builder() .setTracerProvider(tracerProvider) .buildAndRegisterGlobal(); } @Bean public Tracer tracer(OpenTelemetry openTelemetry) { return openTelemetry.getTracer("order-service", "1.5.0"); } private String getEnvironment() { String env = System.getenv("DEPLOY_ENV"); return env != null ? env : "development"; } }三、业务代码的追踪埋点
手动埋点是在自动装配无法覆盖的场景下的必要补充,尤其在异步执行、MQ 消费和自定义线程池等场景。
/** * 订单服务——展示业务代码中分布式追踪的埋点方式。 * * 为什么需要在异步线程中手动传递 Context: * OpenTelemetry 的 Context 默认存储在 ThreadLocal 中, * 线程切换(如 @Async、CompletableFuture)会导致 Context 丢失, * 需要显式传递 Context 以保持 Trace 连续性。 */ @Service public class OrderTraceService { private static final Logger log = LoggerFactory.getLogger( OrderTraceService.class); private final Tracer tracer; private final OrderRepository orderRepository; private final InventoryClient inventoryClient; private final NotificationProducer notificationProducer; public OrderTraceService(Tracer tracer, OrderRepository orderRepository, InventoryClient inventoryClient, NotificationProducer notificationProducer) { this.tracer = tracer; this.orderRepository = orderRepository; this.inventoryClient = inventoryClient; this.notificationProducer = notificationProducer; } /** * 创建订单——完整的分布式追踪覆盖。 * * @param orderRequest 订单请求 * @return 创建结果 */ public OrderResult createOrder(OrderRequest orderRequest) { // 创建主 Span Span span = tracer.spanBuilder("OrderService.createOrder") .setSpanKind(SpanKind.SERVER) .startSpan(); try (Scope scope = span.makeCurrent()) { // 为 Span 添加业务属性——用于后续检索和过滤 span.setAttribute("order.amount", orderRequest.getAmount()); span.setAttribute("order.userId", orderRequest.getUserId()); span.setAttribute("order.itemCount", orderRequest.getItems().size()); // 子 Span:保存订单数据 Span saveSpan = tracer.spanBuilder("OrderService.saveToDatabase") .setParent(Context.current().with(span)) .startSpan(); try (Scope saveScope = saveSpan.makeCurrent()) { Order order = orderRepository.save( convertToEntity(orderRequest)); saveSpan.setAttribute("order.id", order.getId()); saveSpan.setStatus(StatusCode.OK); } catch (Exception e) { saveSpan.setStatus(StatusCode.ERROR, "数据库保存失败"); saveSpan.recordException(e); throw e; } finally { saveSpan.end(); } // 子 Span:扣减库存(远程调用) Span inventorySpan = tracer.spanBuilder("OrderService.deductInventory") .setParent(Context.current().with(span)) .startSpan(); try (Scope invScope = inventorySpan.makeCurrent()) { boolean deducted = inventoryClient.deduct( orderRequest.getItems()); if (!deducted) { inventorySpan.setStatus(StatusCode.ERROR, "库存不足"); throw new InsufficientInventoryException( "库存扣减失败"); } inventorySpan.setStatus(StatusCode.OK); } catch (Exception e) { inventorySpan.setStatus(StatusCode.ERROR, e.getMessage()); inventorySpan.recordException(e); throw e; } finally { inventorySpan.end(); } // 子 Span:发送通知(异步——需要手动传递 Context) Span notifySpan = tracer.spanBuilder("OrderService.sendNotification") .setParent(Context.current().with(span)) .startSpan(); // 为什么用 Context.wrap 而非直接传 Span: // Context 是 OpenTelemetry 的上下文容器, // wrap 将当前 Context 转换为可跨线程传递的 Runnable Context notifyContext = Context.current().with(notifySpan); CompletableFuture.runAsync( Context.wrap(() -> { try (Scope notifyScope = notifyContext.makeCurrent()) { notificationProducer.send( buildNotification(orderRequest)); notifySpan.setStatus(StatusCode.OK); } catch (Exception e) { notifySpan.setStatus(StatusCode.ERROR, e.getMessage()); log.error("通知发送失败", e); } finally { notifySpan.end(); } })); span.setStatus(StatusCode.OK); return buildResult(); } catch (Exception e) { span.setStatus(StatusCode.ERROR, e.getMessage()); span.recordException(e); throw e; } finally { span.end(); } } }四、跨服务追踪的 Trace ID 传播
分布式追踪的前提是 Trace ID 能够在服务间一致传播。在 Spring Cloud 环境下,可以通过 Feign 拦截器自动注入 Trace 头信息:
/** * Feign 请求拦截器——在服务间调用时自动注入 OpenTelemetry 的 traceparent 头, * 实现 Trace ID 的跨服务传播。 * * 为什么使用 W3C Trace Context 标准而非自定义头: * W3C Trace Context(traceparent/tracestate)是业界通用标准, * 不同语言的 OpenTelemetry SDK 都能正确解析,避免厂商锁定。 */ @Component public class OpenTelemetryFeignInterceptor implements RequestInterceptor { private final Tracer tracer; public OpenTelemetryFeignInterceptor(Tracer tracer) { this.tracer = tracer; } @Override public void apply(RequestTemplate template) { Span currentSpan = Span.current(); if (currentSpan == null || !currentSpan.getSpanContext().isValid()) { return; } SpanContext spanContext = currentSpan.getSpanContext(); // 构建 W3C Trace Context 的 traceparent 头 // 格式:version-traceId-parentId-traceFlags // 为什么 version 固定为 00:当前 W3C 规范的唯一稳定版本 String traceparent = String.format("00-%s-%s-%s", spanContext.getTraceId(), spanContext.getSpanId(), spanContext.isSampled() ? "01" : "00"); template.header("traceparent", traceparent); // 传递 baggage——用于跨服务传递业务上下文 // 为什么限制 baggage 大小:HTTP 头有大小限制, // 过多的 baggage 可能导致请求被网关拒绝 Baggage baggage = Baggage.fromContext(Context.current()); if (baggage != null) { Map<String, String> entries = baggage.asMap(); if (!entries.isEmpty()) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : entries.entrySet()) { sb.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)) .append("=") .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)) .append(","); } template.header("baggage", sb.substring(0, sb.length() - 1)); } } } }五、MQ 异步场景的追踪
消息队列是分布式追踪中最容易断裂的环节。生产者发送消息和消费者接收消息之间没有 HTTP 头的自动传递,需要手动将 Trace Context 写入消息体:
/** * 消息生产者——将 Trace Context 注入消息属性, * 消费者可以从消息中恢复 Context 继续追踪。 */ public void sendWithTrace(OrderEvent event) { Span span = tracer.spanBuilder("OrderService.publishOrderCreated") .setSpanKind(SpanKind.PRODUCER) .startSpan(); try (Scope scope = span.makeCurrent()) { // 将 Trace Context 注入消息属性 Map<String, String> propagationHeaders = new HashMap<>(); // 为什么使用 propagator 而非手动拼接单条 traceparent: // TextMapPropagator 同时处理 traceparent 和 tracestate, // 确保多供应商环境下的追踪连续性 TextMapPropagator propagator = GlobalOpenTelemetry .getPropagators().getTextMapPropagator(); propagator.inject(Context.current(), propagationHeaders, (carrier, key, value) -> carrier.put(key, value)); event.setTraceHeaders(propagationHeaders); rabbitTemplate.convertAndSend("order.exchange", "order.created", event); span.setStatus(StatusCode.OK); } finally { span.end(); } }六、追踪数据的使用与分析
graph TD A[Spring Boot 应用] -->|OTLP/gRPC| B[OpenTelemetry Collector] B -->|导出 Traces| C[Jaeger/Tempo] B -->|导出 Metrics| D[Prometheus] B -->|导出 Logs| E[Loki/ELK] C -->|检索Trace| F[Grafana] D -->|指标看板| F E -->|日志关联| F G[问题发现] -->|Trace ID| F F -->|Span详情| H[定位瓶颈] H -->|关联日志| E style B fill:#bbf,stroke:#333,stroke-width:2px style F fill:#6f6,stroke:#333,stroke-width:2px style H fill:#f96,stroke:#333在实际排查中,一个典型的场景是:通过 Grafana 的延迟看板发现某个接口的 P99 延迟突增,利用 Trace ID 搜索对应的全链路追踪数据,在 Span 详情中发现某个下游服务的数据库查询耗时从 50ms 飙升至 2s,再通过关联日志确认是 SQL 执行计划变化导致。
七、总结
OpenTelemetry 在 Spring 生态的落地分为三个层面:基础设施部署(Collector + 后端存储)、SDK 集成(自动装配 + 手动埋点)和业务适配(MQ / 异步 / 自定义线程池的 Context 传递)。
在实际推进时,建议按照"先基础设施、再核心链路、最后全量覆盖"的顺序逐步实施。优先在请求入口和关键下游调用处埋点,覆盖 80% 以上的问题定位场景,再逐步补充异步和边缘链路的追踪能力。
Trace 采样策略的成本控制
全量 Trace(100% 采样率)在生产环境中会产生巨大的存储和网络开销。以我们的订单服务为例,日均 500 万次请求,每次请求平均 8 个 Span,全量采集每天产生 4000 万个 Span。按 Jaeger 的存储成本(约 30 字节/Span),每日需 1.2GB 存储空间,加上 Elasticsearch 的写入开销,月度存储成本约 $500。更严重的是,Collector 的 gRPC 批量导出在全量采集时可能成为瓶颈——Span 丢失率可高达 15%。
OpenTelemetry 支持多种采样策略:(1)parentbased_always_on——带 Trace ID 的请求(来自上游调用)全量采集;(2)traceidratio——按 Trace ID hash 进行百分比采样(10% 适合大多数场景);(3)tailsampling——基于延迟或错误状态的尾部采样(只保留慢请求和异常请求的完整 Trace)。推荐的生产策略是:入口处使用parentbased_always_on+traceidratio(0.1),同时在 Collector 配置 Tail Sampling Processor——保留所有延迟 >1s 或包含 ERROR 状态码的 Trace。这种策略在存储 35% 全量 Trace 数据的同时,覆盖了 95% 以上的问题排查需求。
Baggage 的正确使用边界
Baggage(跨服务传递的业务上下文)与 Span Attributes 的区别是:Baggage 在所有子 Span 中自动传播,而 Attributes 仅属于当前 Span。但 Baggage 的滥用会导致 HTTP Header 膨胀。一个常见错误是将用户请求的全部参数放入 Baggage(如userId=123&productId=456&amount=99.9&address=...)——在高频调用场景下,Header 大小从 200 字节增至 2KB+,网关层的 Header 解析和转发开销大幅增加。正确做法是:Baggage 仅存放必需的追踪上下文(如userId、tenantId、experimentGroup),业务细节通过日志关联(Trace ID + Span ID)而非全量传播。
