bigywater.tech
微服务 · 63

span 父子关系错乱的常见原因,以及时钟偏移的影响

trace 的瀑布图为什么会出现子 span 比父 span 先开始?为什么一个请求的耗时在不同地方不一样?为什么多个请求被并进同一个 trace?丢一点 span 会让多少 trace 残缺?这篇用 OpenTelemetry Python SDK 逐项复现。

99 / 100
下游时钟比调用方慢 20 ms 时,子 span 比父 span 先开始的请求数(下游快 20 ms 时是 100 / 100 个比父 span 晚结束)
1 个 trace
线程池里忘记 detach 时,6 个互不相干的请求一共出现的 trace 数(正确 detach 是 6 个)
1529 / 2000
每个 span 被丢弃概率为 5% 时,5 个 span 的 trace 里完整的个数

时钟偏移

实测 · 本机执行输出
  下游时钟相对调用方 | 请求数 | 下游 span 比父 span 先开始 | 下游 span 比父 span 后结束 | 表观网络耗时中位数(ms,下游开始 − 调用方开始)
  ----------+-----+---------------------+---------------------+---------------------------
  0(同步)     | 100 | 0                   | 0                   | 0.93                      
  +5 ms     | 100 | 0                   | 100                 | 5.97                      
  +20 ms    | 100 | 0                   | 100                 | 20.36                     
  -5 ms     | 100 | 99                  | 0                   | -4.66                     
  -20 ms    | 100 | 99                  | 0                   | -18.96                    
  +100 ms   | 100 | 0                   | 100                 | 100.45                    
  下游时钟偏快时,子 span 看起来在父 span 开始很久之后才开始、甚至在父 span 结束之后才结束;偏慢时子 span 看起来比父 span 先开始。追踪后端画出来的瀑布图、算出来的网络耗时都会受影响;父子关系本身(parent_id)不受时钟影响

父子关系由 parent_id 决定,不受时钟影响,但时间轴上的位置、表观网络耗时都会错位。

忘记 detach

实测 · 本机执行输出
  做法           | 本该是根 span、却有父 span 的请求数 | 6 个请求一共出现的 trace 数
  -------------+-------------------------+-------------------
  (a)正确 detach | 0 / 6                   | 6                 
  (b)忘记 detach | 5 / 6                   | 1                 
  线程池里的线程会被复用,没有 detach 的上下文留在线程里,后面的请求创建 span 时以它为父:每个请求的根 span 被挂在了上一个请求的 span 下面,6 个互不相干的请求并成了一个 trace

线程池线程复用,没 detach 的上下文留在线程里,后面的请求都挂到它下面。

异步任务

实测 · 本机执行输出
  request span:15.2 ms;background span(是 request 的子 span:True):207.4 ms;background 比 request 晚结束 192.4 ms。按 trace 里“根 span 的耗时”统计这次请求耗时是 15.2 ms,按“trace 里最晚的 span 结束时间 − 最早的开始时间”统计是 207.6 ms,两者之比 13.6 倍

后台任务比父 span 活得久,根 span 的耗时和整个 trace 的跨度差很多。

丢 span

实测 · 本机执行输出
  每个 span 被丢弃的概率 | 完整的 trace | 有孤儿 span 的 trace(父 span 丢了) | 不完整但没有孤儿(只丢了叶子) | 一个 span 都没收到
  ---------------+-----------+-----------------------------+-----------------+-------------
  0%             | 2000      | 0                           | 0               | 0           
  1%             | 1893      | 66                          | 41              | 0           
  5%             | 1529      | 289                         | 182             | 0           
  20%            | 634       | 886                         | 479             | 1           
  丢掉任何一个非叶子 span,它的子 span 就成了孤儿,追踪后端要么把它们当作新的根,要么显示“缺失的父 span”;丢的概率是按 span 算的,一个 trace 有 5 个 span,完整的 trace 比例比 1 减去丢弃概率低得多
ERRORS {}

丢掉非叶子 span 会产生孤儿 span;完整的 trace 比例按 span 数的幂次下降。

分步演示

span 树与时钟
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。OpenTelemetry Python SDK 1.45.1;时钟偏移是把下游进程里 opentelemetry.sdk.trace.time_ns 加上固定偏移得到的(不是真的两台机器,也没有用 NTP 或抖动);随机丢弃用固定种子的伪随机数,每个概率只跑了一次;线程池实验只有 1 个线程
    • 没有验证:追踪后端(Jaeger、Tempo 等)对时钟偏移的校正功能,以及它们怎么显示孤儿 span;真实机器之间时钟偏差的典型大小和 NTP 的精度;异步任务里 span 结束时机的正确写法(例如 link 代替 parent);Java / Go SDK 里同类的串线问题
    京ICP备2026067502号-1京公网安备11010502063487号