【架构实战】全链路追踪如何用OpenTelemetry把分布式系统的每一次调用都可视化分布式系统有个经典的难题一个请求进来了调用链路过五关斩六将最后卡住了——到底是哪一环慢了服务A调服务B服务B调服务C和DC又调ED又调F……某一个环节超时但日志分散在5台机器上一个一个去grep这还不是最糟的——最糟的是生产环境偶发你本地死活复现不了日志还没来得及打满就超时结束了查无证据。全链路追踪Distributed Tracing就是来解决这个问题的给每一次请求一个全局唯一的ID贯穿所有服务把调用关系、耗时、状态全部串起来。今天聊清楚原理、工具选型以及怎么用OpenTelemetry落地。一、为什么需要全链路追踪先说痛点。微服务架构里一次用户请求可能涉及10~50个服务调用。传统监控能做到什么Metrics指标CPU多少、内存多少、QPS多少。能看到整体水位但看不到具体哪次请求慢。Logging日志每个服务各自打日志但串不起来。一次请求的日志散落在N个服务的日志文件里要手动关联。APM应用性能监控有链路视图但通常需要每个服务接入Agent侵入性强换语言/框架可能不支持。全链路追踪的核心价值把某次具体请求的调用路径、耗时、异常全部串起来以一个Trace ID为线索一眼看清全局。典型场景线上偶发慢请求排查某用户反馈下单偶尔很慢有Trace ID就能还原那次请求的完整调用链找到最慢的Span。性能瓶颈定位哪个服务的哪个操作最耗时是数据库查询外部API调用还是网络IO依赖关系梳理系统里有多少服务服务之间的调用关系是什么有没有循环依赖Tracer帮你画出来。SLA/SLO分析P99/P999延迟是多少哪个服务拖了后腿二、核心概念三剑客全链路追踪有三个核心概念搞清楚了就理解了所有追踪系统的本质2.1 Trace追踪一次完整的请求链路从入口到出口的所有调用构成一棵调用树这就是一个Trace。Trace: trace-abc123 ├── Span: /order-service/createOrder (耗时: 50ms) │ ├── Span: /inventory-service/deductStock (耗时: 30ms) │ ├── Span: /payment-service/pay (耗时: 15ms) │ │ └── Span: /bank-api/verify (耗时: 10ms) │ └── Span: /notification-service/send (耗时: 5ms) │ └── Span: /sms-api/send (耗时: 4ms) └── Span: /response (耗时: 55ms)2.2 Span跨度Trace里的每一个操作节点叫Span。Span是追踪的最小单位记录了一个操作的开始时间、结束时间、名称、类型、状态以及可选的Attributes键值对和Events时间点事件。Span有父子关系Span A调用Span BB的parentId就是A的spanId。这种父子关系构成调用链树。一个Span包含的关键字段traceId全局唯一的追踪IDspanId当前Span的IDparentId父Span的ID根Span没有parentIdoperationName操作名称如http.get、db.querystartTime/endTime起止时间duration耗时status成功/失败/未知attributes业务属性url、http.status_code、db.system等events时间点事件如异常信息2.3 Context上下文TraceId和SpanId需要在调用链路中传递这就是Context propagation上下文传播。进程内传播请求在一个进程内从A函数传到B函数Context跟着走ThreadLocal/AsyncLocalStorage。进程间传播跨服务调用时Context要跟着HTTP Header或MQ消息头传递。标准格式是W3C Trace Context或B3格式。# W3C Trace Context Header traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 ├── version: 00 ├── trace-id: 0af7651916cd43dd8448eb211c80319c ├── parent-id: b7ad6b7169203331 └── trace-flags: 01 (sampled)每个HTTP请求进来都带这个Header发出去时继续传播给下游。这就是全链路追踪能够串联跨服务请求的原理。三、工具选型从Zipkin到OpenTelemetry3.1 发展历程第一代埋点SDK时代Google Dapper论文2010开启了分布式追踪领域催生了ZipkinTwitter开源、JaegerUber开源等早期产品。那时候每个框架/语言都要自己实现埋点代码侵入性极强。第二代Agent字节码注入时代APM产品Pinpoint、SkyWalking通过Java Agent自动注入字节码无需改业务代码。但换语言就不行且Agent和业务进程耦合升级麻烦。第三代OpenTelemetry标准时代OpenTelemetryOTel2019年CNCF项目统一了Tracing、Metrics、Logging三大信号的采集标准。自动插桩 手动埋点厂商无关Collector可以对接Jaeger/Zipkin/Prometheus/Grafana等后端是目前的事实标准。3.2 OpenTelemetry架构应用代码 │ ├─ 自动插桩Auto Instrumentation无需改代码OTel SDK自动拦截HTTP、数据库、消息队列等调用 │ ↓ ├─ 手动埋点Manual Span在关键业务逻辑里加span.start()/span.end() │ ↓ ├─ SDK层OTel SDKTraces SDK Metrics SDK Logs SDK │ ↓ ├─ OTLP Exporter将数据以OTLP协议发送给Collector │ ↓ ├─ OTel Collector接收 → 处理 → 导出可选中间层可以做过滤、采样、聚合 │ ↓ └─ 后端存储Jaeger / Grafana Tempo / Zipkin / Honeycomb / 商业APM为什么选OpenTelemetry厂商无关今天用Jaeger明天换Grafana Tempo不需要改业务代码。自动插桩强Java/Python/Go/Node.js等主流语言都有成熟的自动插桩库。生态完整Tracing Metrics Logs一体化数据可以关联。社区活跃CNCF毕业项目所有主流APM厂商都在支持。3.3 后端选型后端特点适合场景JaegerCNCF毕业简单易用查询功能完整中小型团队自托管Grafana Tempo与Grafana Metrics/Loki无缝集成全观测已经用Grafana全家桶的团队Zipkin轻量依赖少上手快简单需求不想运维复杂系统商业APM阿里云ARMS、Datadog、New Relic等大型企业免运维省心我的建议小团队用JaegerDocker一键部署中大型团队用Grafana Tempo Loki Prometheus全链路可视化。四、实战Spring Boot OpenTelemetry落地4.1 引入依赖!-- Maven --dependencygroupIdio.opentelemetry/groupIdartifactIdopentelemetry-api/artifactIdversion1.36.0/version/dependencydependencygroupIdio.opentelemetry/groupIdartifactIdopentelemetry-sdk/artifactIdversion1.36.0/version/dependencydependencygroupIdio.opentelemetry.instrumentation/groupIdartifactIdopentelemetry-spring-boot-starter/artifactIdversion2.2.0/version/dependency4.2 配置application.ymlotel:exporter:otlp:endpoint:http://localhost:4317# OTel Collector地址service:name:order-servicetraces:exporter:otlp4.3 手动埋点在关键业务逻辑里加Spanimportio.opentelemetry.api.trace.Tracer;importio.opentelemetry.api.trace.Span;ServicepublicclassOrderService{privatefinalTracertracer;publicOrderService(Tracertracer){this.tracertracer;}publicOrdercreateOrder(OrderRequestrequest){// 开始一个SpanSpanspantracer.spanBuilder(OrderService.createOrder).setAttribute(order.userId,request.getUserId()).setAttribute(order.amount,request.getAmount()).startSpan();try(Tracer.SpanInScopeignoredtracer.withSpan(span)){// 业务逻辑OrderorderorderRepository.save(newOrder());// 扣库存独立的子SpanSpaninventorySpantracer.spanBuilder(InventoryService.deductStock).setAttribute(sku,request.getSku()).setAttribute(quantity,request.getQuantity()).startSpan();try{inventoryService.deductStock(request.getSku(),request.getQuantity());inventorySpan.setStatus(StatusCode.OK);}catch(Exceptione){inventorySpan.setStatus(StatusCode.ERROR,e.getMessage());span.recordException(e);// 把异常记录到父Spanthrowe;}finally{inventorySpan.end();}span.setStatus(StatusCode.OK);returnorder;}catch(Exceptione){span.setStatus(StatusCode.ERROR,e.getMessage());span.recordException(e);throwe;}finally{span.end();}}}Span命名规范用操作.方法名格式如HTTP GET /api/orders、db.query SELECT不要用Span表示整个类用Span表示一个逻辑操作4.4 HTTP传播确保下游服务能收到Context// 服务A调用服务B时自动注入HeaderOpenTelemetry会自动处理// Spring Boot Starter的自动配置已经处理了RestTemplate/WebClient/FekaClient的传播// 如果用OkHttp手动传播SpancurrentSpantracer.currentSpan();RequestrequestnewRequest.Builder().url(targetUrl).header(traceparent,getTraceParentHeader(currentSpan)).build();4.5 配置OTel Collectordocker-compose示例# otel-collector.yamlreceivers:otlp:protocols:grpc:endpoint:0.0.0.0:4317http:endpoint:0.0.0.0:4318processors:batch:timeout:1ssend_batch_size:1024memory_limiter:check_interval:1slimit_mib:1000exporters:jaeger:endpoint:jaeger:14250tls:insecure:trueservice:pipelines:traces:receivers:[otlp]processors:[memory_limiter,batch]exporters:[jaeger]五、采样策略不要让追踪数据打爆你的存储全链路追踪有个容易忽略的问题数据量。高频服务每秒可能产生数十万条Span全量存储代价极大。5.1 采样策略Head-based Sampling在请求入口采样在Span创建之前就决定是否采样。优点决定早可以减少资源消耗。缺点可能漏掉有问题的请求比如前99%采样恰好出问题的1%没采到。// OpenTelemetry SDK配置采样器// AlwaysOnSampler全量采样测试环境// AlwaysOffSampler关闭采样性能敏感// TraceIdRatioBasedSampler按比例采样生产环境SdkTracerProvidertracerProviderSdkTracerProvider.builder().setSampler(TraceIdRatioBasedSampler.create(0.1))// 采样10%.build();Tail-based Sampling在Span结束后采样所有Span先缓存Span结束后根据条件耗时超阈值、包含异常决定是否保存。保证有问题的请求一定被采到但需要额外存储如Redis缓存Span数据。推荐方案Head-based Tail-based结合。Head-based全局采样1%过滤掉99%的正常请求Tail-based对采样到的慢请求或异常请求补充采集完整链路采样率提高到100%5.2 采样配置实战# OpenTelemetry Collector配置Tail-based Samplingtraces:exporters:jaeger:endpoint:jaeger:14250processors:tail_sampling:decision_wait:10s# 等10秒收集Span再决定采样num_traces:50000# 最多缓存5万条Tracepolicies:-name:errors-policytype:status_codestatus_code:{status_codes:[ERROR]}-name:slow-traces-policytype:latencylatency:{threshold_ms:1000}# 超过1秒的请求必采-name:probabilistic-policytype:probabilisticprobabilistic:{sampling_percentage:10}六、Span属性设计让排查更高效Span的attributes属性设计很关键。好的属性设计能让你在UI里一键过滤、搜索快速定位问题。6.1 语义约定Semantic ConventionsOpenTelemetry定义了标准化的Span属性命名这些叫Semantic Conventions遵循它们能让不同服务的Span有统一的查询语言。常用标准属性http.methodHTTP方法GET/POST/PUThttp.url请求URLhttp.status_codeHTTP状态码http.response_content_length响应体大小db.system数据库类型postgresql/mysql/mongodbdb.statementSQL语句注意脱敏不要记录密码db.operation操作类型SELECT/INSERT/UPDATEmessaging.system消息系统类型kafka/rabbitmqmessaging.destinationTopic/Queue名6.2 自定义业务属性// 加上业务上下文排查时一目了然span.setAttribute(order.id,order.getId());span.setAttribute(order.type,order.getType());span.setAttribute(user.tier,user.getTier());// 用户等级影响业务逻辑span.setAttribute(feature.enabled,featureFlag.isEnabled(new_payment));七、Grafana可视化从Trace到Dashboard7.1 Trace关联Metrics把Trace数据和Metrics数据关联起来是全链路追踪的高阶玩法。# 找出P99延迟最高的接口 histogram_quantile(0.99, sum(rate(http_server_duration_seconds_bucket{operation/api/orders}[5m])) by (le) ) # 找出错误率最高的服务 sum(rate(http_server_duration_seconds_count{status_code~5..}[5m])) by (service_name)在Grafana里可以把Jaeger/Tempo的数据和Prometheus Metrics面板放在一起左边是接口延迟的折线图右边是具体某次慢请求的Trace火焰图一眼定位是数据库慢还是外部API慢。7.2 依赖拓扑图Service GraphJaeger和Grafana Tempo都支持基于Trace数据自动生成服务依赖拓扑图哪个服务调用了哪个调用量多少延迟多少一目了然。┌─────────────┐ │ order-svc │ └──────┬──────┘ │调用量: 5000/min ↓ ┌────┴────┐ │ │ ┌─▼──┐ ┌─▼──┐ │inv. │ │pay │ │-svc │ │svc │ └─────┘ └────┘这个拓扑图能帮你发现是否有循环依赖A→B→C→A是否有单点瓶颈某服务被大量下游依赖但没做熔断是否有不必要的跨机房调用延迟会显著增加八、避坑清单坑1Context传播断链常见场景HTTP传播了但异步消息Kafka/RabbitMQ没传。下游消费消息时TraceID丢了链路就断了。解决消费端手动注入Context发送端手动提取并放入消息Header。坑2Span命名不一致不同开发者在不同地方给同一个操作命名不统一导致UI里同一个接口有多种名字无法聚合。解决制定Span命名规范服务名.操作类型.资源Code Review时检查。坑3敏感数据入Span有些团队把用户ID、订单金额、甚至密码打到Span属性里然后Span数据存在Jaeger/Tempo敏感数据泄露。解决Span属性只记录脱敏后的业务标识ID、数量的量级不要记录具体内容。坑4高基数属性把用户ID、订单ID这种唯一值打到Span属性里做过滤Jaeger/Tempo会崩溃高基数导致索引爆炸。解决属性值必须是低基数的有限枚举唯一标识用Span ID/Trace ID来关联不要打到attributes里。坑5过度采样导致问题被淹没生产环境99%的请求都正常但你采样1%恰好有问题的请求漏采了。解决Tail-based Sampling保证慢请求/异常请求必采或者采样率提到5%。九、总结原理Trace 一次请求的完整调用链Span 链路中的每个操作节点Context 跨进程传递的traceId/spanId。标准OpenTelemetry是当前事实标准厂商无关生态完整强烈推荐用。采集自动插桩覆盖80%的场景关键业务逻辑手动埋点。采样Head-based Tail-based结合保证异常请求必采。属性遵循语义约定自定义属性用低基数字段敏感数据脱敏。可视化Jaeger/Tempo Grafana全链路可观测Trace Metrics Logs三合一。全链路追踪是分布式系统可观测性的三大支柱之一Metrics/Logs/Traces。装上OTel之后你会第一次真正看见系统的全貌——以前靠猜、靠日志、靠经验判断的问题现在一目了然。这套能力值得每个微服务团队认真落地。我是做架构的关注我一起搞定分布式系统里的那些坑。