在很多中大型团队里,微服务已经不再是一个技术名词,而是日常开发的默认形态。但微服务拆得越细,技术栈越容易变成多语言混部:Java 承担核心交易,Go 负责高并发网关和支付,Python 跑算法与数据分析,Node.js 做 BFF 聚合层。单看每一个服务,日志和监控都正常,可当一次请求横跨四五个服务时,想快速回答“这条请求到底经历了什么”就变得很困难。这个问题的核心,就是链路追踪没有在跨语言边界上统一起来。
本文围绕“千万 QPS 架构”背景下的跨语言追踪展开,从核心概念、标准规范、多语言接入实战,到高 QPS 场景下的采样与性能治理,再到常见问题排查和工程规范,整理一套可以照着落地的完整方案。适合后端开发、微服务架构师和负责可观测性建设的技术人员阅读。
1. 从“单一”到“统一”:跨语言追踪的价值与挑战
1.1 单语言时代的链路追踪
在服务化早期,很多团队的链路追踪是在单一技术栈内完成的。比如 Java 生态里常见的 Spring Cloud Sleuth + Zipkin 组合,或者自研一套基于拦截器的 Trace 工具。这类方案有一个共同前提:所有服务都用同一套语言和框架,上下文传递可以依赖框架内部的过滤器、拦截器机制,Trace ID 的生成和透传规则也由团队内部统一约定。
这种模式在服务数量不多、技术栈单一的时候是够用的。一次请求进来,网关生成一个 Trace ID,通过自定义 Header 传给下游,下游再透传给更下游的服务。所有服务把同一个 Trace ID 打到日志和调用链数据里,排障时按 ID 一搜,链路基本就能还原出来。
问题在于,这套体系从设计之初就是“单语言私有协议”。它没有考虑过 Java 服务调用 Go 服务时,Go 服务是否认这个 Header;也没有考虑 Python 的日志框架要如何把 Trace ID 注入进去。当业务体量增长到千万 QPS 级别时,服务拆分和语言选型会越来越自由,单语言时代建立的那套隐式约定,很快就会被多语言边界冲垮。
1.2 多语言架构下的追踪痛点
多语言混部带来的追踪问题,通常不是某一个环节出错,而是从 Trace ID 生成、上下文传递、采样决策到数据上报的整条链路都没有统一。常见痛点可以归纳为以下几类。
Header 协议混乱。有的服务用X-Request-Id,有的用X-B3-TraceId,有的直接叫traceId。网关生成一个 ID 后,下游服务不知道该取哪个字段,只好每个都读一遍,取不到就自己新生成一个。结果一条请求在 A 服务叫a1b2c3,到 B 服务变成了d4e5f6,从数据上看就是两条完全无关的 Trace。
数据模型不统一。Java 侧上报的是 Zipkin 格式,Go 侧上报的是 Jaeger 格式,Python 侧可能用自研 SDK。后端存储和查询系统被迫同时兼容多种协议,字段含义、时间单位、状态码定义都对不上,查询页面很难把各语言产生的 Span 拼成一条完整链路。
采样策略各自为政。单个服务为了控制成本,各自配置了采样率。A 服务采样了这条请求,B 服务却因为本地采样率没有采样,导致这条 Trace 在 A 之后直接断掉。从用户视角看,就是一个完整的调用链被拦腰砍断,排查问题反而更难。
日志无法关联。服务没有统一的 Trace ID 注入机制,日志里看不到 Trace ID,到了排障时只能靠时间戳和关键词硬猜。千万 QPS 级别下,同一秒内相同关键词的日志可能成千上万条,没有 Trace ID 关联几乎等于大海捞针。
1.3 跨语言追踪要达成的目标
跨语言追踪要解决的不是“把某个语言的追踪做好”,而是建立一套与语言无关的统一标准,让所有服务无论用什么技术栈,都能生成同一套语义的 Trace 数据,并上报到同一个后端。
具体来说,至少要达成四个目标:
- 统一上下文标准:所有服务使用同一套 Trace ID 生成和传递规则,任何语言都能理解并传播这份上下文。
- 统一数据模型:Trace、Span 的属性定义一致,不同语言产生的 Span 能被同一条 Trace 串起来。
- 统一采集与存储:所有服务的 Span 通过统一协议上报,后端只需要对接一种采集入口。
- 统一查询与排障:在同一个页面里完整看到跨 Java、Go、Python 的调用链,并能通过 Trace ID 关联日志和监控指标。
这四个目标,正好对应了本文后续要讲的标准选型、SDK 接入和平台建设。
2. 跨语言追踪的核心概念与标准模型
2.1 Trace、Span 与 SpanContext
要理解跨语言追踪,首先要把三个基础概念弄清楚:Trace、Span 和 SpanContext。
Trace表示一次请求从入口到出口的完整调用路径。一次用户下单请求,可能经过网关、订单服务、支付服务、库存服务,所有这些调用合在一起就是一条 Trace。在数据层面,一条 Trace 由一个全局唯一的trace-id标识。
Span是 Trace 的最小工作单元,表示一次具体的操作,比如一次 HTTP 调用、一次数据库查询、一段业务逻辑。每个 Span 有独立的span-id,同时记录自己的父 Span ID,从而形成一棵调用树。根 Span 没有父 ID,它就是整条 Trace 的入口。
SpanContext则是跨进程传递的核心载体。它包含trace-id、span-id、采样标记以及可选的厂商透传数据。SpanContext 会被序列化到 HTTP Header、RPC Metadata 或消息队列属性中,从上游传递给下游。
用一个简单的 ASCII 图来表示三服务调用关系:
Service A (Java) Service B (Go) Service C (Python) Span A ───────────────> Span B ──────────────> Span C trace-id: 0af7651916cd43dd8448eb211c80319c(相同) parent: 无 parent: Span A parent: Span B关键点在于:三个服务可以各自生成 Span,但它们必须共享同一个trace-id,并且每个 Span 都要正确记录父 Span ID。只有这样,后端才能把分散在不同服务的 Span 拼接成一条完整的 Trace。
2.2 W3C Trace Context 传递标准
跨语言追踪必须先解决“上下文如何传递”的问题。早期各家有各家的方案,B3、Jaeger、SkyWalking 都有自己的 Header 格式,互不兼容。后来 W3C 组织制定了Trace Context标准,定义了traceparent和tracestate两个 HTTP Header,成为目前跨语言、跨厂商追踪的事实协议。
traceparent的格式如下:
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01字段从左到右依次是:
| 字段 | 长度 | 含义 |
|---|---|---|
| version | 2 位十六进制 | 版本号,当前固定为 00 |
| trace-id | 32 位十六进制 | 全局唯一 Trace ID,即trace-id |
| parent-id | 16 位十六进制 | 当前调用方的 Span ID,即父 Span ID |
| flags | 2 位十六进制 | 追踪标记,最低位表示是否采样 |
其中flags的01表示这条 Trace 需要被采样记录,00表示不记录。下游服务收到traceparent后,通过 SDK 解析出trace-id和父 Span ID,再用当前节点生成的 Span ID 作为新的parent-id继续往下传。
tracestate则用于携带厂商自定义的数据,比如某个平台要透传的优先级、租户信息等。绝大多数场景下,我们只需要关注traceparent即可。
W3C Trace Context 的意义在于,它把上下文传递从“语言私有约定”变成了“公开标准协议”。任何一个语言只要实现了该标准,就能和其他语言无缝串联。
2.3 OpenTelemetry:从百家争鸣到事实标准
有了统一的标准协议,还需要一套统一的 API、SDK 和数据上报协议。这里就不得不提 OpenTelemetry 的发展背景。
早期业界有两套主流方案:CNCF 旗下的 OpenTracing 和 Google 主导的 OpenCensus。OpenTracing 侧重定义 API 规范,OpenCensus 则连数据采集、上报、后端统计一起做了。两套方案各有优势,但也让使用者陷入选择困难。2019 年,两个项目合并为OpenTelemetry,统一了 API、SDK、数据模型和传输协议 OTLP,并成为 CNCF 孵化项目。
OpenTelemetry 对跨语言追踪最重要的贡献,是提供了几乎所有主流语言的 SDK 和自动埋点能力,包括 Java、Go、Python、Node.js、C++、Ruby、PHP 等。同时它原生支持 W3C Trace Context,内部数据模型也能无损导出到 Jaeger、Zipkin、SkyWalking 等后端。
需要说明的是,SkyWalking 本身也是一套成熟的 APM 方案,它有自己的探针协议和存储模型。如果你的团队已经在 SkyWalking 上投入很深,也可以继续使用;但如果你需要的是“多语言统一、协议开放、不被特定平台绑定”的追踪体系,OpenTelemetry 是目前兼容性最好、社区最活跃的选择。
2.4 上下文传播通道:HTTP、RPC 与消息队列
跨语言追踪的上下文传播,不只有 HTTP 一条通道。在真实的微服务架构里,调用可能通过 gRPC、Dubbo、Kafka、RabbitMQ 等多种方式发生,每一种通道都需要有对应的上下文传播机制。
- HTTP/HTTPS:通过
traceparentHeader 传递,现代的 HTTP 客户端和框架大多已支持自动注入与解析。 - gRPC:通过 Metadata 传递,OpenTelemetry 的 gRPC 拦截器会自动处理。
- 消息队列:通过消息头或消息属性传递,比如 Kafka 的 Record Header、RabbitMQ 的 Message Properties。这也往往是手工埋点最多的地方,因为很多 MQ 客户端不会自动传播追踪上下文。
- 异步线程:业务代码中通过线程池执行异步任务时,需要显式把父级 Context 传入子线程,否则 Span 会丢失父级关系。
在接入追踪系统时,建议先把 HTTP 和 gRPC 通道打通,再逐步覆盖消息队列和异步线程,避免一开始就推进过深导致排查困难。
3. 技术选型与整体架构
3.1 组件选型建议
跨语言追踪的落地,可以拆成“客户端埋点”和“服务端平台”两部分。
客户端埋点直接选择 OpenTelemetry SDK 或自动埋点工具,各语言接入方式不同,但数据模型和上报协议一致。服务端平台可以选择 Jaeger、SkyWalking、Elastic APM,也可以基于 OpenTelemetry Collector 加 ClickHouse/Elasticsearch 自建。
如果团队规模不大、链路追踪链路条数可控,推荐先用 Jaeger 作为后端,部署成本低,社区资料多,能快速看到效果。如果已经有一定规模的监控体系,或者对存储有更高要求,可以选择 OTLP Collector 统一接收数据,再转发到自己的存储和分析系统。在千万 QPS 架构下,建议采用 Collector 独立部署的方式,避免业务进程直连后端存储带来的性能压力和耦合。
3.2 环境与版本说明
本文示例中的代码以常见稳定版本为参考,重点演示接入思路。实际项目请根据当前官方发布的最新稳定版本调整依赖,不要盲目照搬版本号。
- 操作系统:Linux,本文示例不依赖特定发行版
- Java:JDK 8 及以上,示例以 Spring Boot 2.x 风格编写
- Go:1.18 及以上
- Python:3.8 及以上
- Node.js:14 及以上
- 后端存储:Jaeger 或 Elasticsearch,示例仅演示链路打通
3.3 整体链路架构
一个典型的跨语言追踪架构可以简化为下图:
+----------------+ OTLP +---------------------+ | Java 订单服务 | ---------------> | | +----------------+ | OpenTelemetry | +----------------+ OTLP | Collector | | Go 支付服务 | ---------------> | (统一接收/预处理) | +----------------+ | | +----------------+ OTLP +----------+----------+ | Python 分析服务 | ---------------> | +----------------+ v +---------------------+ | Jaeger/ES/ClickHouse| +---------------------+各语言服务通过 OTLP 协议将 Span 上报到 Collector,Collector 承担数据接收、批处理、过滤、采样和转发的工作,最终数据落库并在查询端统一展示。这套架构的好处是业务侧只需要关心埋点,不需要关心数据怎么存储、怎么建索引。
4. 多语言接入实战:Java、Go、Python、Node.js
下面我们用一个模拟的下单场景来演示跨语言接入:Java 订单服务收到请求后,调用 Go 支付服务,Go 支付服务再调用 Python 分析服务,最后整条链路在 Jaeger 中合并为一条 Trace。
4.1 Java 服务接入 OpenTelemetry
Java 接入 OpenTelemetry 有两种常见方式:无侵入的 Agent 自动埋点,以及手动埋点。
方式一:Agent 自动埋点
在启动命令中加入-javaagent参数,即可自动对 Spring MVC、RestTemplate、JDBC、Kafka 等主流框架埋点,无需修改业务代码。
java -javaagent:/opt/otel/opentelemetry-javaagent.jar \ -Dotel.service.name=order-service \ -Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \ -jar order-service.jar这是接入成本最低的方案,适合在存量项目中快速落地。自动埋点默认对 HTTP 客户端、服务端、数据库访问等操作生成 Span,基本满足多数服务对“快速接入”的诉求。
方式二:手动埋点
如果需要在关键业务中增加自定义属性,或者对 Span 生命周期做更精细的控制,可以在代码中手动创建 Span。使用 Agent 时,业务代码可以直接从GlobalOpenTelemetry获取 Tracer。
// 文件路径:src/main/java/com/example/order/OrderController.java import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Scope; @RestController @RequestMapping("/order") public class OrderController { private static final Tracer TRACER = GlobalOpenTelemetry.getTracer("order-service", "1.0.0"); @GetMapping("/{orderId}") public String handleOrder(@PathVariable String orderId) { Span span = TRACER.spanBuilder("handleOrder") .setAttribute("order.id", orderId) .setAttribute("order.userLevel", "vip") .startSpan(); try (Scope scope = span.makeCurrent()) { // 调用下游 Go 支付服务,自动携带 traceparent return restTemplate.getForObject( "http://payment-service/pay?orderId=" + orderId, String.class); } catch (Exception e) { span.recordException(e); span.setStatus(StatusCode.ERROR); throw e; } finally { span.end(); } } }手动埋点的核心原则是:startSpan之后必须在finally中end,并尽量使用 try-with-resources 或Scope保证当前 Context 在线程内可见。这样下游调用才能正确取到父 Span 并建立父子关系。
4.2 Go 服务接入 OpenTelemetry
Go 语言没有类似 Java Agent 的通用自动埋点机制,通常是通过 SDK 在入口和出口显式埋点。Go 的 OpenTelemetry 提供了otelhttp等集成包,可以简化对标准库 HTTP 的埋点。
先引入基础依赖:
go get go.opentelemetry.io/otel go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp核心代码示例:
// 文件路径:main.go package main import ( "context" "net/http" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" ) var tracer = otel.Tracer("payment-service") func handlePayment(w http.ResponseWriter, r *http.Request) { ctx, span := tracer.Start(r.Context(), "handlePayment") defer span.End() span.SetAttributes( attribute.String("payment.channel", "wechat"), attribute.String("order.id", r.URL.Query().Get("orderId")), ) // 调用下游 Python 服务时,必须把 ctx 继续传入 if err := callAnalysisService(ctx, r.URL.Query().Get("orderId")); err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write([]byte("payment ok")) } func main() { http.HandleFunc("/pay", handlePayment) http.ListenAndServe(":8081", nil) }这里需要特别强调:在 Go 中,Context 的传递是显式的。如果业务代码在调用下游时只传了context.Background(),那么traceparent就不会被注入到下游请求中,链路自然断裂。这是 Go 服务接入时最高频的坑。
另外可以把 HTTP Handler 包一层otelhttp,让框架自动为每个 HTTP 请求生成服务端 Span:
http.Handle("/pay", otelhttp.NewHandler(http.HandlerFunc(handlePayment), "http.server"))4.3 Python 服务接入 OpenTelemetry
Python 接入 OpenTelemetry 可以使用自动埋点命令行工具,也可以手动初始化 SDK 后通过 Tracer 创建 Span。
先安装依赖:
pip install opentelemetry-api pip install opentelemetry-sdk pip install opentelemetry-exporter-otlp-proto-grpc pip install opentelemetry-instrumentation-flask手动初始化 SDK 的示例:
# 文件路径:app.py from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter resource = Resource.create(attributes={"service.name": "analysis-service"}) provider = TracerProvider(resource=resource) provider.add_span_processor( BatchSpanProcessor( OTLPSpanExporter(endpoint="http://otel-collector:4317", insecure=True) ) ) trace.set_tracer_provider(provider) tracer = trace.get_tracer("analysis-service") def analyze_image(image_id: str): with tracer.start_as_current_span("analyzeImage") as span: span.set_attribute("image.id", image_id) # 业务处理逻辑如果使用 Flask 框架,也可以直接用 OpenTelemetry 提供的命令行工具自动埋点,不需要在业务代码里写 Span:
opentelemetry-instrument --service_name analysis-service flask run --port=8082自动埋点会把 Flask 请求、requests 库的 HTTP 调用都生成 Span,并把traceparent自动注入到下游请求中,适合快速验证链路。
4.4 Node.js 服务接入 OpenTelemetry
Node.js 接入相对特殊,SDK 需要在应用入口最早的位置初始化,越早越好,否则后续加载的模块可能无法被自动埋点。
// 文件路径:tracing.js const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base'); const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc'); const { Resource } = require('@opentelemetry/resources'); const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions'); const provider = new NodeTracerProvider({ resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'bff-node-service', }), }); provider.addSpanProcessor( new BatchSpanProcessor( new OTLPTraceExporter({ url: 'http://otel-collector:4317' }) ) ); provider.register();然后在应用入口文件的第一行引入:
require('./tracing'); const express = require('express');4.5 跨语言链路串联与验证
四个语言的接入代码写完,先别急着看平台数据。拉通一条完整链路的验证方式如下:启动三个服务后,从 Java 订单服务发起一次请求:
curl -v "http://localhost:8080/order/order-12345"请求到达 Java 服务时,Java 服务会生成traceparent,并通过 RestTemplate 传递给 Go 支付服务;Go 服务解析后,把它作为当前 Span 的父级,继续通过 http.Client 传递给 Python 分析服务。整个过程可以用-v看请求头中的 traceparent 变化:
< traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01如果三个服务都上报成功,在 Jaeger 查询这个 trace-id,就能看到一条包含三个服务、三个 Span 的完整调用链。日志侧也可以同步验证:Java 服务日志里能搜到 trace-id,Python 服务日志里同样能搜到同一个 trace-id。
5. 高 QPS 场景下的追踪性能治理
千万 QPS 架构下,“能不能查到链路”不是唯一标准,“引入追踪后会不会拖垮业务”才是更关键的问题。全量采集一条 Trace 的成本是真实存在的,Span 的创建、序列化、传输、存储都会占用 CPU、内存、网络和磁盘。因此高 QPS 场景下的治理重点,通常集中在采样、异步导出和存储三方面。
5.1 采样策略:从全量到精准
如果每天真实请求量是千万级 QPS,单条请求平均产生 5 个 Span,那么一秒就会产生数千万甚至上亿条 Span。这个量级直接全量采集,对后端存储和查询系统都是巨大压力。所以首先要做的是采样。
OpenTelemetry 的采样器配置可以通过环境变量或 SDK 配置完成。比较推荐的是parentbased_traceidratio,也就是按 Trace ID 比例采样,同时保证子 Span 跟随父 Span 的采样决策:
OTEL_TRACES_SAMPLER=parentbased_traceidratio OTEL_TRACES_SAMPLER_ARG=0.01这里的0.01表示采样 1% 的 Trace。为什么强调 parentbased?因为如果每个服务独立按比例采样,上游采样了而下游没采样,就会出现断链。parentbased 能保证一条 Trace 要么全链路都被记录,要么整条不记录,不会出现半截链路。
还有一种更精细的方案是尾采样(Tail Sampling)。由 Collector 收集完整 Trace 后再决定是否落库,适合在“保留错误全量、普通请求低比例采样”这类场景下使用。但尾采样需要 Collector 缓存一定时间内的 Span,内存开销更大,在千万 QPS 级别下需要结合成本仔细权衡。
5.2 异步导出与背压处理
业务进程中的 Span 导出必须是异步的,绝不能阻塞业务请求。OpenTelemetry 默认使用 BatchSpanProcessor,也就是先放入内存队列,再由后台线程批量导出。
在高 QPS 下,需要关注三个参数:
- 队列大小:队列越大,能承受的瞬时突发越多,但内存占用越高。
- 批量大小:每次导出的 Span 条数,适当调大能提高吞吐。
- 导出间隔:控制导出的实时性,间隔越短实时性越好,但请求数越多。
如果队列满了怎么办?正确的做法是丢弃新增 Span,或丢弃最老的 Span,而不是阻塞业务线程。这也是为什么要在 Collector 层做缓冲和批处理,而不是让业务进程直接连接存储。
Collector 侧可以配置内存限制和批处理,核心配置片段如下:
# collector.yaml 核心片段 processors: memory_limiter: check_interval: 1s limit_mib: 512 batch: send_batch_size: 8192 timeout: 5s在 Collector 不可用时,业务侧必须快速降级,重试不能无限阻塞。很多团队会把“追踪数据丢失”和“业务故障”当成两件事来设计:追踪数据丢了可以事后补,业务超时就是真事故了。
5.3 存储与查询优化
跨语言追踪平台的数据量增长非常快,存储方案需要提前规划。Jaeger 默认支持 Elasticsearch 作为生产存储,适合中小规模;当数据量达到千万 QPS 级别时,很多团队会把 Span 明细存储迁移到 ClickHouse 或自建明细存储,以 Trace ID 作为分片键,保证同一条 Trace 的 Span 落在同一分片,查询时按 Trace ID 直接命中。
存储优化上还可以考虑:
- 保留策略分层:错误 Span 和核心业务 Span 保留更长时间,普通成功 Span 缩短保留周期。
- 字段裁剪:只保留查询和排障必要的 Attribute,高基数、低价值的属性不上报。
- 索引规划:按 Trace ID、服务名、时间范围建索引,避免对业务属性盲目建索引导致写入性能下降。
6. 常见问题与排查清单
6.1 高频问题速查表
接入跨语言追踪后,最常遇到的问题集中在“链路断了”和“性能变差”两类。下面整理一份高频问题速查表。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 跨服务调用后 Trace 断裂 | 下游服务未接入 SDK,或未识别 traceparent | 确认 traceparent 是否到达下游,补齐 SDK 接入 |
| 一条 Trace 只有根 Span | 下游采样策略与上游不一致 | 统一使用 parentbased 采样器 |
| Span 时间顺序异常或为负 | 服务节点时钟不同步 | 统一 NTP/chrony 时间同步 |
| 高 QPS 下接口 RT 上升 | 同步导出阻塞业务线程 | 改为 BatchSpanProcessor,异步导出 |
| Span 丢失严重 | 导出队列满被丢弃,或 Collector 过载 | 调大队列,增加 Collector 副本,降低采样率 |
| 日志里没有 trace_id | 日志注入未开启,或日志库版本不兼容 | 开启 Agent 的 MDC 注入,或手动添加 trace_id |
| 同一 Trace 的 Span 分属不同页面记录 | 某服务重新生成了 trace-id | 检查该服务是否错误覆盖了入站 traceparent |
6.2 一条 Trace 断链的排查流程
跨语言链路断链是最常见的排查场景,按下面的步骤来通常能快速定位。
- 从网关入口日志或前端请求中拿到 Trace ID。
- 到 Jaeger 或查询平台搜索该 Trace ID,确认断点发生在哪个服务。
- 进入断点服务,查看该服务接收到的请求头中是否包含 traceparent。
- 如果请求头没有 traceparent,问题出在上游服务的注入逻辑;如果有但本地没生成子 Span,问题出在该服务 SDK 或采样配置。
- 查看该服务进程的 OpenTelemetry 导出日志和队列指标,确认 Span 是否成功发出。
- 检查 Collector 的接收指标,关注 accepted、refused、dropped 三类计数。
- 复