微服务拆完之后最烦人的事情不是服务多了部署麻烦而是出了问题根本不知道去哪里看。一条用户请求从网关进来经过订单服务、库存服务、支付服务最后落库任何一环慢上几百毫秒用户感知就是“卡了”但你想定位是哪一环慢了得上服务器翻日志、对着时间戳猜链路运气不好还得一个个服务手动查一遍。这个痛点就是分布式追踪要解决的。而把分布式追踪落地到Spring Boot项目里目前最值得投入的方案就是OpenTelemetry简称OTel。这篇文章我会结合自己在几个Spring Boot微服务项目里的实际落地经验把分布式追踪的基础概念、OpenTelemetry的选型理由、Spring Boot Starter的接入步骤、关键参数调优和常见坑一次讲清楚。内容偏向实操适合正在做微服务改造、或者已经在排查链路问题上浪费过时间的Java开发者和架构师。1. 为什么微服务架构必须做分布式追踪1.1 服务拆分后问题定位从“看日志”变成“拼图”单体应用时代排查性能问题很简单请求进来Controller处理Service调DAO一条调用链就在同一个进程里日志按时间排序就能还原整个过程。微服务化之后一次业务请求会跨越多个进程、多台机器每个服务只记录自己那一段日志而且各服务的时间戳还不一定严格同步光靠“翻日志比对时间”基本是在做拼图游戏。更难受的是哪怕你定位到某个服务响应慢也很难快速判断是这个服务自身逻辑慢、数据库查询慢、还是它调的下游服务拖累了它。没有全链路视图就只能靠猜猜完还得让多个团队配合查证一次线上故障折腾几个小时很正常。我自己经历过一次典型的线上事故排查用户反馈下单提交后要等十几秒才出结果实际是订单服务在等待库存服务的HTTP回调超时而库存服务在处理一个历史慢SQL问题链跨越了三个服务、两组日志文件。那次之后我就意识到微服务架构里链路数据不是“可选项”而是基础设施和日志、监控一个级别。1.2 核心概念先对齐Trace、Span、SpanContext在动手集成OpenTelemetry之前建议先理解分布式追踪领域中三个最关键的概念——Trace、Span、SpanContext后面所有配置和代码都围绕它们展开。Trace追踪一次完整业务请求的整个过程比如“用户提交订单”从网关到订单服务、库存服务、支付服务、最终写库的完整调用路径就是一条Trace。Span跨度Trace里的一个最小工作单元每个服务处理请求、调用外部接口、执行SQL都可以是一个Span。一个Span有开始时间、结束时间、名称、状态、属性等。多个Span通过父子关系串成树状结构就是Trace。SpanContext跨度上下文用于标识一个Span在分布式环境中的身份信息包括TraceId、SpanId、采样标记、追踪系统附加信息等。跨服务传递链路信息本质上就是在传递SpanContext。用一个生活化类比一条Trace就是从“出发地”到“目的地”的完整行程Span是行程中的每一段航班或大巴SpanContext相当于每段行程的车票车票上印着同一个订单号TraceId你凭这个订单号能把所有分散的行程片段重新拼回完整路线。1.3 分布式追踪与Metrics、Logs的关系这里顺便把可观测性的整体框架说一下。业界常提“三大支柱”Metrics指标、Logs日志、Traces链路。三者各有分工数据类别回答的问题典型工具Metrics系统整体是否健康比如CPU、请求量、错误率Prometheus、GrafanaLogs某一条请求的具体执行细节比如参数、异常堆栈ELK、LokiTraces一次请求在多服务间的完整调用链路和耗时分布Jaeger、Tempo、Zipkin三者不是替代关系而是配合关系。实际排查中经常是先用指标发现服务异常再用Trace定位是哪条调用链慢最后拉对应日志看具体报错。这也是为什么OpenTelemetry把Traces、Metrics、Logs三条数据线统一采集的原因——它希望成为可观测性数据的“通用语言”让你不用对流式处理的每一类数据都维护一套独立的采集方案。哪怕你当前只用到分布式追踪也应该了解这种设计思路因为它直接影响后续的扩展成本。2. 技术选型为什么我选了OpenTelemetry2.1 主流分布式追踪方案对比在确定使用OpenTelemetry之前我实际调研过Zipkin、Jaeger、SkyWalking这几个主流方案简单说下结论。Zipkin老牌分布式追踪系统Twitter开源数据模型清晰接入简单但功能偏传统没有标准化协议的前瞻性设计更适合中小规模场景。JaegerCNCF毕业项目功能完善支持多种存储后端UI不错但严格来说它更多是后端存储与查询展示平台数据采集端需要配合其他探针或SDK。SkyWalking国内使用率很高功能强大自带UI和告警Java Agent做得很成熟但它的数据协议和存储模型偏向自研体系如果你想同时把数据送到多个后端比如既要Jaeger又要自有监控平台扩展性会打折扣。OpenTelemetryCNCF旗下的事实标准定位不是“一个追踪系统”而是一个可观测性数据采集与标准化框架。它定义了一套厂商中立的API、SDK和导出协议OTLP你只需要按标准埋点和导出数据可以送给Jaeger、Tempo、云厂商、自建平台等多个后端。就我个人的选择逻辑如果团队规模小、只想要一个能看链路调用的工具Zipkin或Jaeger都能满足但如果团队在搞微服务治理规范、想统一埋点标准、未来可能同时接监控、日志多个平台OpenTelemetry的“标准”价值会越来越明显。我现在的原则是能标准化的东西尽量标准化不要给未来留技术债。2.2 OpenTelemetry在三层结构中扮演什么角色OpenTelemetry本身分为三层角色API给业务代码调用、SDK负责采集、处理、导出、Collector独立部署的数据接收/转发/处理服务。而在Spring Boot微服务体系里你和OTel的关系主要是在项目中引入依赖和配置让OTel SDK自动或手动创建Span通过导出器把Span数据发到OTel Collector或后端的HTTP/gRPC端口由Collector统一接收、过滤、批量化再转发给Jaeger、Tempo等存储展示系统。Spring Boot Starter OpenTelemetry这个思路本质上就是帮你把这套复杂流程封装成Spring配置风格。你不用再手动创建繁琐的SDK构建代码只需要引入starter、配置几个关键参数Spring Boot的自动装配就会加载OTel运行时并在应用启动时完成插桩初始化。不过这里要提前说清楚OTel的接入方式有“无侵入Agent”和“手动SDK/starter”两种路线各有适用场景。下一部分我会把两种方式都展开讲最终选择取决于你的团队对侵入性、灵活性和排查体验的取舍。3. Spring Boot项目接入OpenTelemetry从依赖到第一个完整Trace3.1 环境准备JDK版本、Spring Boot版本与后端选型先说环境底线。使用OpenTelemetry Java SDK和Spring Boot Starter通常建议JDK 8以上但实际我用的比较多的是JDK 11和JDK 17两个版本都工作正常如果要用Java Agent自动插桩JDK版本对agent的兼容性影响最大建议直接跟进官方兼容矩阵避免遇到JDK新版本处理器问题。Spring Boot方面我验证过Spring Boot 2.7.x和3.x都能正常接OTel但3.x使用的Spring Framework 6和Jakarta命名空间依赖坐标上会有差异后面给依赖示例的时候我会单独标注。后端展示平台我建议先用Jaeger或Tempo配合OTel Collector理由是这个组合在OTLP协议支持上最顺滑配置代码量最少能最快看到完整链路适合快速验证。有Java经验的同学也可以用Zipkin毕竟Zipkin也支持OTLP导出。3.2 方式一零侵入Java Agent接法推荐快速验证如果你只是想先看效果、验证链路数据最快的方式是用Java Agent不写任何代码、不侵入业务逻辑。步骤很简单下载opentelemetry-javaagent.jar官方jar包启动时加JVM参数-javaagent:opentelemetry-javaagent.jar配置环境变量或系统属性指定服务名、导出地址、采样率等。核心配置项就是一组OTEL_*环境变量比如export OTEL_SERVICE_NAMEorder-service export OTEL_EXPORTER_OTLP_ENDPOINThttp://localhost:4317 export OTEL_TRACES_SAMPLERparentbased_traceidratio export OTEL_TRACES_SAMPLER_ARG0.1OTEL_SERVICE_NAME决定Span上的服务名OTEL_EXPORTER_OTLP_ENDPOINT指向OTel Collector或后端接收端口OTEL_TRACES_SAMPLER和OTEL_TRACES_SAMPLER_ARG控制采样率。启动项目后HTTP调用、数据库访问、Redis操作等常见操作都会被自动插桩自动生成Span。Agent方式的优点是零代码侵入、接入成本极低、覆盖广Java生态里常用的框架基本都有内置插桩支持比如Spring MVC、RestTemplate、Feign、JDBC、Jedis、Kafka等。缺点也客观存在Agent作为一个独立的jar包需要维护版本生产环境里如果JDK版本升级或者框架升级有可能出现Agent与运行环境不兼容的情况另外Agent自动插桩生成的Span比较“机械”比如它只知道这个HTTP调用的路径和耗时并不知道这次请求对应的订单号、用户ID这些业务信息想做业务维度分析还得手动埋点补充。3.3 方式二用Starter方式手动接入SDK可控性更强如果你的团队对链路数据有更高要求比如需要自定义Span属性、做精细化采样策略、追求不依赖Agent的管理方式那Starter方式或手动SDK方式更合适。这里给一套基于Maven的接入依赖示例dependency groupIdio.opentelemetry/groupId artifactIdopentelemetry-api/artifactId version1.40.0/version /dependency dependency groupIdio.opentelemetry/groupId artifactIdopentelemetry-sdk/artifactId version1.40.0/version /dependency dependency groupIdio.opentelemetry/groupId artifactIdopentelemetry-exporter-otlp/artifactId version1.40.0/version /dependency dependency groupIdio.opentelemetry/groupId artifactIdopentelemetry-sdk-extension-autoconfigure/artifactId version1.40.0/version /dependency如果你在Spring Boot 3.x里用Starter风格的方式更推荐的方式是引入官方或社区维护的Spring Boot Starter依赖配合Spring Boot自动装配来简化配置。但我个人还是倾向于保留显式构建SDK的方式因为这样每个组件TracerProvider、Exporter、Sampler的行为完全可控排查问题也更直接。接着在Spring配置类里构建并暴露一个全局的OpenTelemetry实例Configuration public class OpenTelemetryConfig { Bean public OpenTelemetry openTelemetry() { Resource resource Resource.getDefault().merge( Resource.builder() .put(AttributeKey.stringKey(service.name), order-service) .put(AttributeKey.stringKey(deployment.environment), production) .build() ); OtlpGrpcSpanExporter spanExporter OtlpGrpcSpanExporter.builder() .setEndpoint(http://localhost:4317) .build(); SdkTracerProvider tracerProvider SdkTracerProvider.builder() .addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build()) .setResource(resource) .setSampler(SamplingResult.alwaysOn()) .build(); return OpenTelemetrySdk.builder() .setTracerProvider(tracerProvider) .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) .build(); } }那段Resource配置是整个追数据结构里很关键的一环它决定了一个服务的“元数据”比如服务名、环境、部署节点等。Span数据里附带这些资源信息后在Jaeger里就能按服务名筛选、按环境区分否则所有服务发送的数据都混在一起别提做分析了。另一个值得留意的是BatchSpanProcessor。Span数据不是产生一条就立刻发送一条而是先放入内存队列攒够一批或到时间间隔再批量导出。这个设计能显著降低网络和序列化开销但代价是数据有短暂的延迟并且内存队列溢出的极端情况下可能丢Span。3.4 手动创建Span把业务维度写入链路SDK装配好之后就能在业务代码里手动创建Span把业务信息附加进去。获取Tracer的方式很简单Autowired private OpenTelemetry openTelemetry; public void processOrder(Order order) { Tracer tracer openTelemetry.getTracer(order-service, 1.0.0); Span span tracer.spanBuilder(processOrder).startSpan(); try (Scope scope span.makeCurrent()) { span.setAttribute(order.id, order.getId()); span.setAttribute(order.userId, order.getUserId()); span.addEvent(order.received); // 业务逻辑... span.setStatus(StatusCode.OK); } catch (Exception e) { span.recordException(e); span.setStatus(StatusCode.ERROR); throw e; } finally { span.end(); } }这里makeCurrent()特别关键。OpenTelemetry使用Context机制管理Span上下文makeCurrent()会把当前Span绑定到线程的Context里。如果你不在try块里调用它那么这段代码内部生成的子Span就没法和当前链路上下文关联链路会从这里断开。但凡你手动创建Span务必让业务逻辑在Scope的作用域范围内执行并且用finally确保span.end()被调用不然要么链路断裂要么Span泄漏导致内存持续增长。如果不想手写这么多样板代码OTel还提供了WithSpan注解适合快速给某个方法加SpanWithSpan(processOrder) public void processOrder(Order order) { Span span Span.current(); span.setAttribute(order.id, order.getId()); // 业务逻辑 }用这个注解需要额外引入opentelemetry-instrumentation-annotations。它的好处是代码整洁方法的进入和退出自动生成Span和异常状态记录缺点是没法在注释逻辑里精细控制Scope和自定义事件复杂场景我还是建议手动创建。3.5 验证链路是否打通从启动到Jaeger看Trace接入完成后怎么验证我习惯的流程很固定先确保Jaeger和OTel Collector已经启动确认4317端口在监听启动Spring Boot项目观察启动日志里是否有OpenTelemetry相关的初始化输出调用一个简单的REST接口等几秒让BatchSpanProcessor批量导出打开Jaeger UI在Service下拉框里选择刚才配置的service.name比如order-service查看是否出现Trace记录点开Trace查看Span树是否完整有没有报错Span。如果这几步都正常说明最基础的链路已经通了。但别高兴太早真实生产环境的配置远比“能看Trace”复杂下面的参数和传播问题才是真正拉开差距的地方。4. 关键配置与参数采样、导出、标签和传播4.1 采样策略全量采样只会让你破产采样可能是所有可观测性系统里最影响成本、又最容易被忽略的环节。如果每一条请求都生成完整链路数据高并发生产环境的数据量会非常恐怖存储、带宽和查询成本都会失控。所以接入分布式追踪必须优先想清楚采样策略。OpenTelemetry中常见采样方式有几种AlwaysOn全量采样每个Span都保留。适合低流量场景或短期全链路调试高并发环境不建议长期使用。AlwaysOff关闭不采样任何数据。适合临时关闭追踪。TraceIdRatioBased按比例采样按TraceId哈希取模维持一个固定比例比如10%。这是最常用的简单采样式但要注意它是“按Trace比例”而不是“按Span比例”一条Trace要么全保留要么全丢弃这对还原完整链路很重要。ParentBased父级采样根据父Span的采样决策来决定子Span是否采样。通常在入口服务执行采样判断子服务直接继承决策避免层层重复决定导致前面保留、后面丢掉的断链问题。我生产环境里比较推荐的是parentbased_traceidratio入口服务设置一个比例比如0.1下游服务统一继承入囧决策这样数据量可控同时一条Trace内部是完整的。export OTEL_TRACES_SAMPLERparentbased_traceidratio export OTEL_TRACES_SAMPLER_ARG0.1如果你是入门阶段验证功能可以直接用always_on但要记住上线前一定要改成有损采样不然流量一上来存储先撑不住。4.2 导出与Collector应用直连后端为什么不是最佳实践刚接触OTel的人容易把OTEL_EXPORTER_OTLP_ENDPOINT配成Jaeger或Tempo的地址让应用直连后端。这样做小规模验证没问题但是生产环境我强烈建议在中间加一层OTel Collector。原因有三点解耦应用与后端应用只需要知道Collector的地址后端无论怎么换Jaeger换Tempo、加一套监控平台应用配置都不用动。批量与缓冲Collector可以对接收到的Trace数据做批量压缩、内存队列缓冲、重试发送减轻应用端的发送负担。集中处理能力可以在Collector里做数据的过滤、脱敏、采样、路由比如只保留错误Span或者把数据冗余发给两套后端系统。一个最小可用的Collector配置长这样config.yamlreceivers: otlp: protocols: grpc: http: processors: batch: exporters: jaeger: endpoint: jaeger:14250 tls: insecure: true service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [jaeger]这个配置的意思是说Collector的4317端口gRPC和4318端口HTTP接收OTLP数据经过批量处理再转发给Jaeger的14250端口。应用端和Collector都部署在同一个K8s集群或内网时网络延迟和失败率都很低。4.3 资源属性的规范化别给数据“裸奔”我见过不少团队接入OTel后链路上Span是一大堆服务名却叫localhost-service或者忘了设置导致后端界面里所有数据混成一坨。别小看这些细节数据规范化的第一步就是资源属性。资源属性应该覆盖以下常用维度属性键含义示例值service.name服务名order-serviceservice.version服务版本1.2.0deployment.environment环境标识production、staginghost.name主机名k8s-node-01host.ip主机IP10.0.0.8设置方式除了在代码里用Resource.builder()也可以直接环境变量export OTEL_RESOURCE_ATTRIBUTESservice.nameorder-service,deployment.environmentproduction这些资源属性会附加在每条Span上排查时能快速区分环境和机器维度另外很多告警、筛选规则也依赖这些标签所以接入初期就统一规范后面省心非常多。4.4 跨服务传播链路断掉的最常见元凶Trace数据之所以能跨服务串成一条完整链路靠的是跨进程传播Propagation。当一个服务调用另一个服务时需要把当前SpanContext塞进请求的Header中下游服务读取之后以它为父Span创建子Span。标准的传播标准是W3C Trace Context核心Header是traceparent它的格式类似traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01traceparent里包含版本号、TraceId、SpanId、采样标记。OpenTelemetry默认支持W3C这也是我推荐的原因——标准化意味着无论你用的是Java、Go还是Node.js服务只要遵循同一个标准链路就能跨语言串起来。在Spring Boot生态中如果使用内置的RestTemplate、Feign、WebClient并且用的是Agent方式接入那么传播是自动处理的。但如果你用原生HTTP客户端或者用了自己的连接池类库就得手动引入传播逻辑。最简单的做法是在HTTP调用前用Propagators注入HeaderTextMapSetterHttpURLConnection setter (carrier, key, value) - carrier.setRequestProperty(key, value); MapString, String carrier new HashMap(); openTelemetry.getPropagators() .getTextMapPropagator() .inject(Context.current(), carrier, MapTextMapSetter.INSTANCE); // 再把carrier里所有Header设置到HTTP请求上还有一类常见断链场景是消息队列。从服务A发送MQ消息到服务B如果消息发送时没有把SpanContext传播到消息头消费端就无法关联到同一Trace里。Kafka、RocketMQ这类消息中间件的传播需要额外处理但好在OTel在Agent方式下也有相应插桩支持。5. 实际接入中的常见问题与排查实录这部分我整理了自己和身边团队在落地过程中踩过的高频问题按“症状、原因、解法”做成速查表方便你直接对号入座。症状可能原因解决办法Jaeger里看不到任何Trace采样率被设为0、导出器没配置、后端端口不通先设always_on验证再检查OTEL_EXPORTER_OTLP_ENDPOINT和后端日志只有入口服务有Trace下游断链跨服务传播配置缺失或下游也是手动SDK但没统一Propagator确认都用W3C Trace Context检查HTTP Header是否传了traceparent线程池内执行的异步任务没有SpanContext存在ThreadLocal线程切换导致上下文丢失在提交任务前用Context.current().makeCurrent()或用OTel提供的WrappedRunnable服务名显示成未知或不统一service.name资源属性未设置用环境变量OTEL_RESOURCE_ATTRIBUTES或代码里统一设置Agent方式下Redis/MQ没有Span依赖的客户端类库不在Agent插桩支持范围更新agent版本或自行实现对应插桩逻辑/手动埋点Span导出有较大延迟BatchSpanProcessor批量导出需要满足条件才发送调小setScheduleDelayMillis和setMaxExportBatchSize参数高并发下内存攀升Span溢出队列丢弃逻辑导致内存压力关注setMaxQueueSize并配置合理的setMaxExportBatchSize5.1 线程池场景Context上下文丢留给我的教训有一个问题我单独拿出来强调因为它最隐蔽、最容易在生产环境才暴露——跨线程传Context。OpenTelemetry的Context默认绑定在ThreadLocal里动态语言都不用关心这个问题但Java必须处理。一旦代码用了线程池、异步Callable、或者Async注解子线程拿不到父线程的Context那么子线程里创建的Span就成孤儿链路直接断开。我踩过的一个典型场景是用户下单后发送异步通知短信发送逻辑放在线程池里执行。主流程的Trace很正常但短信服务那一小段Trace永远不在订单链路里排查时总感觉数据“缺了一块”。后来定位到是线程切换时Context.current()没有随着任务传递只能手动处理Context parentContext Context.current(); executor.submit(() - { try (Scope scope parentContext.makeCurrent()) { // 异步任务逻辑 } });这个方案能用但代码侵入感强。更优雅的做法是用OTel官方推荐的WrappedRunnable或在提交任务时统一包装维护成本更小。不管用哪种上线前一定要把线程池、MQ消费这类异步场景都纳入验证范围否则链路数据永远是残缺的。5.2 排查链路问题时的思路和实操建议遇到链路数据“不对”的情况我建议按固定顺序排查不要一上来就改代码。第一步先确认Span有没有生成。可以在日志或Debug里检查traceId是否为合法的32位十六进制数如果完全没有大概率是Tracer没正确初始化或采样被关闭。第二步确认Span有没有正确导出。看后端接收端口的日志或者本机用curl往Collector端口发数据测试通不通。第三步确认跨服务传播。在调用方打印HTTP Header看有没有traceparent字段没有就检查是否做了手动inject在接收方打印Header看能否读到该字段读不到就检查是否有extract逻辑。这种“从源头到宿地”逐一确认数据流向的排查法效率远高于瞎猜。定位之后再回头检查采样比例、资源属性这些“软配置”多半问题就水落石出了。6. 从Traces到全链路可观测后续还能怎么扩展6.1 把TraceId带进应用日志只有链路数据排查问题还是不够因为最终一步往往要落到具体日志。把TraceId和SpanId织入日志是最划算的增强手段。做法是在日志框架里配置MDC字段让日志输出自动带上当前Span的TraceId微服务日志聚合后就能按TraceId一次性捞出整条调用链上的所有日志。用Logback举例在pattern里加上%X{trace_id} %X{span_id}即可前提是提前配置了OTel的MDC注入功能Agent方式下会自动把TraceId注入MDC。这样一来从Trace看到某个Span异常直接复制TraceId检索日志原来要连通多套系统的排查工作变成了“搜索同一个ID”效率提升非常明显。6.2 Metrics与Traces联合分析OTel不只会采集Trace也统一支持Metrics数据。把Metrics和Traces打通之后你可以先从指标大盘看到某个服务的P99延迟飙高再下钻到对应时间点的Trace列表找到具体慢在哪条调用链。很多团队的监控和链路两套系统长期割裂问题定位效率大打折扣。OTel的价值正在于此——统一的数据管线下指标异常和链路详情可以无缝衔接。我自己的习惯是上游用Prometheus做告警指标OTel负责TraceMetrics采集数据汇聚到同一个后端。这样既保留了Prometheus生态又避免了多套采集器的运维成本落地起来很顺。6.3 通过Trace数据反哺告警和容量规划等链路数据沉淀一段时间后你会发现它的价值远不止排查问题。按服务维度聚合Span耗时能算出每个服务真实的调用量和依赖耗时占比自动识别出“拖慢全局”的瓶颈服务针对某类Span的错误率设置告警阈值还能在故障扩大前提前收到通知。容量规划也一样链路数据能告诉你哪个服务承担了大部分请求压力扩容时往哪里扩就有了数据支撑。最后说点个人体会OpenTelemetry这套体系真正用顺了会发现分布式追踪的难点其实不在技术接入而在于数据规范和组织协作。技术层面跑通一个Trace很快但要让每个服务都统一服务名、统一资源属性、统一采样策略、统一传播标准才是真正需要花功夫的地方。我建议你在项目第一天上手时就立好这些规矩否则等服务多了再回头统一规范要比现在痛苦得多。另外记得上线前多做几轮异步场景和跨服务调用的链路验证这些地方出问题的概率远高于同步HTTP场景。