SkyWalking 显示某 Span 的自耗时 90%,如何判定是本地计算慢还是埋点缺失

解读

  1. 现象:SkyWalking 链路追踪里,该 Span 的“Self”列占比高达 90%,意味着 90% 时间既不在子 Span,也不在远程调用,而是“挂”在当前 Span 上。
  2. 面试官真正想听的,是你能否用“可观测性三角”——指标、日志、追踪——把“慢”与“缺失”区分开,并给出可落地的排查步骤。
  3. 国内面试场景里,常会追问:
    • 如果线上不能加代码,你还能怎么确认?
    • 如果自耗时高的是 MQ 消费或线程池任务,你怎么拆?
      因此答案必须兼顾“代码侧”与“运维侧”两套打法。

知识点

  1. SkyWalking 自耗时计算公式:Self = Span 总耗时 − 所有子 Span 耗时 − 所有 Exit/Entry 网络耗时(含 MQ、DB、RPC)。
  2. 埋点缺失的典型场景:
    • 异步线程、线程池、@Async、CompletableFuture 未跨线程传递 ContextSnapshot;
    • 本地方法调用被 AOP 代理绕过,导致未生成子 Span;
    • 自定义组件未安装官方插件或自写插件漏埋。
  3. 本地计算慢的典型场景:
    • CPU 密集、Full GC、JIT 未预热、加密/压缩、大 JSON 序列化;
    • 锁竞争(synchronized、ReentrantLock、Redisson 锁)、日志同步刷盘。
  4. 国内常用配套工具:
    • Arthas trace/watch 命令,可在生产环境无侵入抓方法耗时;
    • Alibaba Dragonwell/OpenJDK 内置 JFR,可录制热点方法、GC、锁;
    • 容器场景下,Prometheus + Grafana 看 CPU Throttle、Memory RSS、PSS;
    • 若公司用通Tracing,需知 SkyWalking 与 Zipkin、Jaeger 的跨协议差异,防止“双 tracer”导致重复或丢失。

答案

回答采用“四步法”,先快速定界,再深入取证,最后给出修复与回归方案。全程用中文术语,贴合国内习惯。

步骤 1:秒级定界——看指标

  1. 在同一时间窗口,把 SkyWalking 的 Span 自耗时曲线与 Grafana 的 CPU、Load、GC 耗时、线程阻塞数做叠加。
    • 若 CPU/GC/Load 与自耗时同步飙升 → 先怀疑本地计算慢。
    • 若指标平稳 → 先怀疑埋点缺失。
  2. 看链路入口的“慢 SQL”“慢 Dubbo”面板:如果下游调用都 <5 ms,而 Self 却 900 ms,基本可排除下游慢。

步骤 2:代码级取证——Arthas 直捣黄龙
线上允许诊断时,用 Arthas 选择该 Span 对应的入口方法:
trace com.xxx.Service handleOrder -n 5 --skipJDKMethod false

  1. 若 trace 结果里 80% 耗时落在某一行 Java 代码(如 for-loop、MessageDigest、ObjectMapper),即可确认本地计算慢。
  2. 若 trace 结果里耗时“平铺”在方法入口到出口之间,没有明显热点行,说明方法内部缺少子 Span,极可能是埋点缺失。

步骤 3:无代码也能看——JFR + 线程栈
若公司禁止 Arthas,可让运维 1 分钟内连目标 Pod:
jcmd <pid> JFR.start name=slow_span settings=profile maxage=60s

  1. 下载 jfr 文件后,用 JDK Mission Control 打开,看“Hot Methods”“Lock Instances”“Allocation in TLAB”。
  2. 同时 jstack -l <pid> 3 次,间隔 5 s;若 90% 线程栈都卡在同一个 monitor 或 CPU 计算,可确认本地慢;若栈顶都在 Runnable 但 SkyWalking 无子 Span,则埋点缺失。

步骤 4:修复与回归
A. 本地计算慢:

  • 把热点方法拆子 Span,加 @Trace 或 manualSpan.tag("step", "crypto").start(),上线后观察 Self 占比是否降到 30% 以下;
  • 若算法不可拆,就推动算法优化或异步化,再用性能基线回归,要求 P99 下降 50% 以上。
    B. 埋点缺失:
  • 异步线程:在提交任务前 ContextManager.capture(),任务内 ContextManager.continued(snapshot);
  • 线程池:使用 SkyWalking 提供的apm-custom-enhance-plugin,在 config/span-enhance.xml 里配置要增强的类与方法;
  • 自定义组件:按插件开发规范,实现 InstanceMethodsAroundInterceptor,打包后放入 optional-plugins,重启即生效。
    回归测试时,用同一压测脚本(TPS、并发、数据量相同)跑 30 min,对比:
  • Self 占比 <20% 为合格;
  • 子 Span 完整率(有埋点的关键方法/总关键方法)>95%;
  • 业务黄金指标(RT、TPS、错误率)不劣化。

拓展思考

  1. 如果自耗时高的 Span 是 MQ 消费端,且消费组采用批量拉取,如何区分“本地计算慢”与“拉取后等待业务线程”?
    思路:在 MQ 插件里把“拉取到第一条消息”与“业务线程真正执行”分别埋点,生成两个子 Span;若两者间隔大,说明线程池排队,而不是本地计算慢。
  2. 国内金融公司常把 SkyWalking 与自研 Metrics 平台双写,如何避免双 tracer 带来的上下文丢失?
    思路:使用 OpenTelemetry SDK 的 Baggage 统一传递,把 SW 的 ContextCarrier 写入 Baggage,再由自研 tracer 读取,保证跨线程时只一次 capture。
  3. 当自耗时高出现在 Go 编写的 Sidecar(如 Istio Envoy)里,Java 后端的 SkyWalking 无法下钻,如何继续?
    思路:在 Envoy 开启 %REQ(X-B3-TraceId)% 日志,结合 Loki 检索,看 Envoy 的 duration 与 Java 的 Self 差值;若差值大,说明 Sidecar 里 TLS 解密、WAF 规则耗时,推动运维调优 Envoy filter 顺序或升级 CPU 指令集加速。