【架构实战】全链路追踪:如何用OpenTelemetry把分布式系统的每一次调用都可视化
分布式系统有个经典的难题:一个请求进来了,调用链路过五关斩六将,最后卡住了——到底是哪一环慢了?
服务A调服务B,服务B调服务C和D,C又调E,D又调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 B,B的parentId就是A的spanId。这种父子关系构成调用链树。
一个Span包含的关键字段:
traceId:全局唯一的追踪IDspanId:当前Span的IDparentId:父Span的ID(根Span没有parentId)operationName:操作名称(如http.get、db.query)startTime/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到OpenTelemetry
3.1 发展历程
第一代:埋点SDK时代
Google Dapper论文(2010)开启了分布式追踪领域,催生了Zipkin(Twitter开源)、Jaeger(Uber开源)等早期产品。那时候每个框架/语言都要自己实现埋点代码,侵入性极强。
第二代:Agent字节码注入时代
APM产品(Pinpoint、SkyWalking)通过Java Agent自动注入字节码,无需改业务代码。但换语言就不行,且Agent和业务进程耦合,升级麻烦。
第三代:OpenTelemetry标准时代
OpenTelemetry(OTel,2019年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 SDK(Traces 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 后端选型
| 后端 | 特点 | 适合场景 |
|---|---|---|
| Jaeger | CNCF毕业,简单易用,查询功能完整 | 中小型团队,自托管 |
| Grafana Tempo | 与Grafana Metrics/Loki无缝集成,全观测 | 已经用Grafana全家桶的团队 |
| Zipkin | 轻量,依赖少,上手快 | 简单需求,不想运维复杂系统 |
| 商业APM | 阿里云ARMS、Datadog、New Relic等 | 大型企业,免运维,省心 |
我的建议:小团队用Jaeger(Docker一键部署),中大型团队用Grafana Tempo + Loki + Prometheus全链路可视化。
四、实战:Spring Boot + OpenTelemetry落地
4.1 引入依赖
<!-- Maven --><dependency><groupId>io.opentelemetry</groupId><artifactId>opentelemetry-api</artifactId><version>1.36.0</version></dependency><dependency><groupId>io.opentelemetry</groupId><artifactId>opentelemetry-sdk</artifactId><version>1.36.0</version></dependency><dependency><groupId>io.opentelemetry.instrumentation</groupId><artifactId>opentelemetry-spring-boot-starter</artifactId><version>2.2.0</version></dependency>4.2 配置application.yml
otel:exporter:otlp:endpoint:http://localhost:4317# OTel Collector地址service:name:order-servicetraces:exporter:otlp4.3 手动埋点:在关键业务逻辑里加Span
importio.opentelemetry.api.trace.Tracer;importio.opentelemetry.api.trace.Span;@ServicepublicclassOrderService{privatefinalTracertracer;publicOrderService(Tracertracer){this.tracer=tracer;}publicOrdercreateOrder(OrderRequestrequest){// 开始一个SpanSpanspan=tracer.spanBuilder("OrderService.createOrder").setAttribute("order.userId",request.getUserId()).setAttribute("order.amount",request.getAmount()).startSpan();try(Tracer.SpanInScopeignored=tracer.withSpan(span)){// 业务逻辑Orderorder=orderRepository.save(newOrder());// 扣库存(独立的子Span)SpaninventorySpan=tracer.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时,自动注入Header(OpenTelemetry会自动处理)// Spring Boot Starter的自动配置已经处理了RestTemplate/WebClient/FekaClient的传播// 如果用OkHttp,手动传播:SpancurrentSpan=tracer.currentSpan();Requestrequest=newRequest.Builder().url(targetUrl).header("traceparent",getTraceParentHeader(currentSpan)).build();4.5 配置OTel Collector(docker-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:按比例采样(生产环境)SdkTracerProvidertracerProvider=SdkTracerProvider.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 Conventions)
OpenTelemetry定义了标准化的Span属性命名,这些叫Semantic Conventions,遵循它们能让不同服务的Span有统一的查询语言。
常用标准属性:
http.method:HTTP方法(GET/POST/PUT)http.url:请求URLhttp.status_code:HTTP状态码http.response_content_length:响应体大小db.system:数据库类型(postgresql/mysql/mongodb)db.statement:SQL语句(注意脱敏,不要记录密码)db.operation:操作类型(SELECT/INSERT/UPDATE)messaging.system:消息系统类型(kafka/rabbitmq)messaging.destination:Topic/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到Dashboard
7.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 Graph)
Jaeger和Grafana Tempo都支持基于Trace数据自动生成服务依赖拓扑图:哪个服务调用了哪个,调用量多少,延迟多少,一目了然。
┌─────────────┐ │ order-svc │ └──────┬──────┘ │调用量: 5000/min ↓ ┌────┴────┐ │ │ ┌─▼──┐ ┌─▼──┐ │inv. │ │pay │ │-svc │ │svc │ └─────┘ └────┘这个拓扑图能帮你发现:
- 是否有循环依赖(A→B→C→A)
- 是否有单点瓶颈(某服务被大量下游依赖但没做熔断)
- 是否有不必要的跨机房调用(延迟会显著增加)
八、避坑清单
坑1:Context传播断链
常见场景:HTTP传播了,但异步消息(Kafka/RabbitMQ)没传。下游消费消息时TraceID丢了,链路就断了。
解决:消费端手动注入Context,发送端手动提取并放入消息Header。
坑2:Span命名不一致
不同开发者在不同地方给同一个操作命名不统一,导致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之后,你会第一次真正"看见"系统的全貌——以前靠猜、靠日志、靠经验判断的问题,现在一目了然。这套能力,值得每个微服务团队认真落地。
我是做架构的,关注我,一起搞定分布式系统里的那些坑。