在分布式追踪里,跨度(Span)可以理解成一次请求中某个具体环节的记录,父子关系则用来表示这些环节之间的调用顺序与归属关系。
打个比方:你在外卖App下单,这个动作背后牵扯到订单服务、支付服务、骑手派单服务,整个流程记成一本流水账,咱们把这个账本叫Trace,账本里每一行记录就是一个Span,某个Span调用了另一个服务,那它就是父Span,被调用的那个就是子Span。
实际业务里怎么识别Span和父子关系
日常排障时最直观的感受
以前排查慢请求,只能翻各个服务的日志,时间对不上就抓瞎,有了Span以后,每个服务做了什么事、耗时多少、传了什么参数,都在一条追踪链上。
- 进程内动作:一个服务里查数据库、调缓存、做计算,每一步都可以是一个Span。
- 跨进程调用:订单服务调用支付服务,这个HTTP请求本身就是一个新的Span,而且这个Span的父Span就是订单服务里发起调用的那段代码。
- 辅助操作:消息队列发送、异步任务的执行,同样能注册成Span。
一张表看懂字段含义
每个Span自带基础信息,我之前接触过几个开源自建方案,字段大同小异。
| 字段名 | 含义 | 典型值 |
|---|---|---|
| traceId | 整个链路的全局唯一编号 | 40位十六进制字符串 |
| spanId | 当前Span的编号 | 16位十六进制字符串 |
| parentSpanId | 父Span的编号,表示谁调用了你 | 16位十六进制字符串 |
| operationName | 当前操作名称 | POST /api/pay |
| startTime / endTime | 开始、结束时刻 | 纳秒或毫秒时间戳 |
| tags | 键值对,描述属性 | http.status_code=200 |
| logs | 记录事件或错误 | error.stacktrace |
traceId是所有Span共享的,spanId是自己独有的,parentSpanId把上下两代串起来,拿订单服务调支付服务举例:父Span的spanId是abc123,那么支付服务那个子Span的parentSpanId写的就是abc123。
分布式追踪父子关系怎么理解才能到位
本质是一棵调用树
整个请求链

路画出来是一棵从上到下的树,根节点是入口请求,往下延伸出各种分支,这棵树就是Trace,树上的每个节点就是一个Span,为什么强调树形结构?因为咱们监控整个链路的时候,最关心的就是层级关系:哪个服务是入口、哪个服务是中间环节、哪个服务响应太慢拖了后腿。
一句话理解父子关系:谁调的,谁是爹
父子关系的核心在于归属与因果,你打开App的首页,首页接口调用了商品推荐和广告系统,那么首页接口的Span就是父Span,商品推荐和广告系统各自的Span是子Span,子Span还能继续往下调别的服务,比如广告系统去查用户画像,那就是更深一层的孙Span。
异步调用时父子关系仍然存在
同步调用好理解,一手交钱一手交货,返回了才结束,异步调用比较绕,比如订单服务把消息发到MQ就返回了,而消费端的服务过了几秒才处理,业界标准采用插拔式的方式,通过traceId和parentSpanId透传,让消费端生成的子Span依然挂在订单服务那个父Span下面,只是时间上不重叠。
错误传播也沿树形结构扩散
父Span的状态码会被子Span的异常影响,比如支付服务返回了500,那支付这个子Span会标记为错误,订单服务这个父Span也会连带标记为有错误发生,但根因追踪时可以定位到最底层的那个具体出错Span。
分布式追踪选型对比:从父子关系梳理难易度看
这几年接触的进销存、电商、物流类项目里,大家选追踪系统时参考的核心其实是一样的拿到Trace以后,还原调用树时省不省心。
自研 vs 开源框架
- 自研方案:很多公司早期是在日志里加traceId,然后靠日志平台搜索,这种方式在做跨线程、跨MQ场景时,需要手写透传逻辑,而且一个请求会产生大量零散日志,按traceId聚合出来的是平面列表,不是树状结构,排障效率打折扣。
- 开源方案:以Jaeger和Zipkin为代表,它们原生支持Span的父子关系展示,界面直接呈现树形瀑布图,行业共识认为,这两款是入门级绝佳搭配,区别在于Jaeger存储依赖Cassandra或Elasticsearch,Zipkin则更轻量。
技术栈兼容性对比
如果你的业务大量使用Spring Cloud微服务架构,那用Spring Cloud Sleuth搭配Zipkin能省不少事,因为Sleuth能自动对Feign调用、RestTemplate调用生成父子Span,不用改业务代码,如果用的是Go或Python写的高性能服务,那OpenTelemetry规范下的Jaeger支持更顺手。

| 框架 | 自动织入能力 | 依赖组件 | 上手难度 |
|---|---|---|---|
| Spring Cloud Sleuth | 强,看到Controller方法就埋点 | Zipkin | 低 |
| Jaeger | 需要配合OpenTelemetry SDK | Cassandra/ES | 中 |
| SkyWalking | 强,支持Java Agent | ES | 中 |
不管怎么选,先把标准统一
这里强烈建议直接跟随W3C Trace Context标准,也就是用traceparent头传递traceId、spanId和采样标记,这就好比咱们统一了口径,前端和后端、两个微服务之间传参数时,不用特地去记对方系统里叫法,都用同一个HTTP头就对了。
实操:手动创建Span并设置父子关系
虽然大部分框架能自动埋点,但有些场景必须手动干预,比如从MQ消费消息的时候,或者写了个自定义的异步线程池,下面这套操作路径适用于OpenTelemetry和Jaeger,比较通用。
第一步:获取当前上下文
Span parentSpan = Span.current();
这里拿到的就是当前线程上下文里的Span,可能是框架自动创建的,如果这里返回的是个空Span,那说明链路已经断了,得检查前面透传有没有做好。
第二步:创建子Span
Tracer tracer = GlobalOpenTelemetry.getTracer("my-instrumentation");
Span childSpan = tracer.spanBuilder("handle-mq-message")
.setParent(Context.current().with(parentSpan))
.startSpan();
重点在.setParent(Context.current().with(parentSpan)),如果没有这一步,你的新Span会变成一个孤儿,跟父Span没有任何关系,很多人在排查时发现好几个同名Span各自为政,就是这个原因。
第三步:结束并标注状态
try {
// 业务逻辑处理
childSpan.setStatus(StatusCode.OK);
} catch (Exception e) {
childSpan.setStatus(StatusCode.ERROR);
childSpan.recordException(e);
} finally {
childSpan.end();
}
注意,任何Span都要有end操作,多线程场景尤其考验这个,忘记关了会直接推高链路延迟采样的平均值。
第四步:手动传递HTTP头

String traceparent = String.format("00-%s-%s-01",
parentSpan.getSpanContext().getTraceId(),
parentSpan.getSpanContext().getSpanId());
headers.add("traceparent", traceparent);
接收到这个请求的下游服务,再从traceparent头里解析出父SpanId,挂到自己的父节点下,只要保证这套透传逻辑,跨Kafka、跨Redis缓存穿透都能成串。
排查链路慢的定位路径
- 在界面里输入traceId,直接搜出整条调用树。
- 看瀑布图里横向长条,哪块最长就是瓶颈所在。
- 点开对应Span看tags和logs,判断是网络等待、锁竞争还是SQL慢查询导致。
跨进程调用场景里最容易踩的坑
数据库连接池复用导致串了请求
连接池里的连接被不同请求复用,如果在代码里手动创建了Span但忘了把它放入当前Context,那么查询数据库的时候新Span的父就是最近一次使用的那个Span,就会出现孤儿或错挂,解决办法是使用Context.makeCurrent()把上下文绑定到当前线程,用完再关闭。
日志采集与追踪割裂
有些团队只落Span数据,但日志里没把traceId打印出来,结果排查时还得两头跑,既看日志又对链路,建议直接把traceId注入到MDC中,这样日志里每行都带traceId,排障效率翻倍,这是成本最低的收效手段。
分布式追踪跨度与父子关系常见问题解答
一条链路最多能有多少个Span?
没有硬性数量上限,但业界常规玩法是设置单链路采样比例,当节点非常多,比如超过1000个Span,后端存储和UI渲染都会面临压力,像Jaeger就支持采样策略,默认是头采样或尾采样,生产环境一般配置10%或更低的采样率来降低存储开销。
子Span比父Span晚很久才结束,算不算异常?
不算,异步场景经常出现,比如发MQ后父Span立即结束,消费端几秒后才执行完写子Span,只要parentSpanId正确,时间线不重叠也合法。
用Jaeger还是SkyWalking?
SkyWalking容易上手,Agent方式无侵入,更适合Java微服务与Kubernetes环境;Jaeger胜在标准支持度,对多语言和OpenTelemetry兼容性好,项目里如果纯粹想快速排查调用链慢的问题,优先选SkyWalking,如果公司内部有规范需求选Jaeger更灵活,二者都遵循宽泛的标准规范体系。