SkyWalking 显示某 Span 的自耗时 90%,如何判定是本地计算慢还是埋点缺失
解读
- 现象:SkyWalking 链路追踪里,该 Span 的“Self”列占比高达 90%,意味着 90% 时间既不在子 Span,也不在远程调用,而是“挂”在当前 Span 上。
- 面试官真正想听的,是你能否用“可观测性三角”——指标、日志、追踪——把“慢”与“缺失”区分开,并给出可落地的排查步骤。
- 国内面试场景里,常会追问:
- 如果线上不能加代码,你还能怎么确认?
- 如果自耗时高的是 MQ 消费或线程池任务,你怎么拆?
因此答案必须兼顾“代码侧”与“运维侧”两套打法。
知识点
- SkyWalking 自耗时计算公式:Self = Span 总耗时 − 所有子 Span 耗时 − 所有 Exit/Entry 网络耗时(含 MQ、DB、RPC)。
- 埋点缺失的典型场景:
- 异步线程、线程池、@Async、CompletableFuture 未跨线程传递 ContextSnapshot;
- 本地方法调用被 AOP 代理绕过,导致未生成子 Span;
- 自定义组件未安装官方插件或自写插件漏埋。
- 本地计算慢的典型场景:
- CPU 密集、Full GC、JIT 未预热、加密/压缩、大 JSON 序列化;
- 锁竞争(synchronized、ReentrantLock、Redisson 锁)、日志同步刷盘。
- 国内常用配套工具:
- Arthas trace/watch 命令,可在生产环境无侵入抓方法耗时;
- Alibaba Dragonwell/OpenJDK 内置 JFR,可录制热点方法、GC、锁;
- 容器场景下,Prometheus + Grafana 看 CPU Throttle、Memory RSS、PSS;
- 若公司用通Tracing,需知 SkyWalking 与 Zipkin、Jaeger 的跨协议差异,防止“双 tracer”导致重复或丢失。
答案
回答采用“四步法”,先快速定界,再深入取证,最后给出修复与回归方案。全程用中文术语,贴合国内习惯。
步骤 1:秒级定界——看指标
- 在同一时间窗口,把 SkyWalking 的 Span 自耗时曲线与 Grafana 的 CPU、Load、GC 耗时、线程阻塞数做叠加。
- 若 CPU/GC/Load 与自耗时同步飙升 → 先怀疑本地计算慢。
- 若指标平稳 → 先怀疑埋点缺失。
- 看链路入口的“慢 SQL”“慢 Dubbo”面板:如果下游调用都 <5 ms,而 Self 却 900 ms,基本可排除下游慢。
步骤 2:代码级取证——Arthas 直捣黄龙
线上允许诊断时,用 Arthas 选择该 Span 对应的入口方法:
trace com.xxx.Service handleOrder -n 5 --skipJDKMethod false
- 若 trace 结果里 80% 耗时落在某一行 Java 代码(如 for-loop、MessageDigest、ObjectMapper),即可确认本地计算慢。
- 若 trace 结果里耗时“平铺”在方法入口到出口之间,没有明显热点行,说明方法内部缺少子 Span,极可能是埋点缺失。
步骤 3:无代码也能看——JFR + 线程栈
若公司禁止 Arthas,可让运维 1 分钟内连目标 Pod:
jcmd <pid> JFR.start name=slow_span settings=profile maxage=60s
- 下载 jfr 文件后,用 JDK Mission Control 打开,看“Hot Methods”“Lock Instances”“Allocation in TLAB”。
- 同时 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、错误率)不劣化。
拓展思考
- 如果自耗时高的 Span 是 MQ 消费端,且消费组采用批量拉取,如何区分“本地计算慢”与“拉取后等待业务线程”?
思路:在 MQ 插件里把“拉取到第一条消息”与“业务线程真正执行”分别埋点,生成两个子 Span;若两者间隔大,说明线程池排队,而不是本地计算慢。 - 国内金融公司常把 SkyWalking 与自研 Metrics 平台双写,如何避免双 tracer 带来的上下文丢失?
思路:使用 OpenTelemetry SDK 的 Baggage 统一传递,把 SW 的 ContextCarrier 写入 Baggage,再由自研 tracer 读取,保证跨线程时只一次 capture。 - 当自耗时高出现在 Go 编写的 Sidecar(如 Istio Envoy)里,Java 后端的 SkyWalking 无法下钻,如何继续?
思路:在 Envoy 开启 %REQ(X-B3-TraceId)% 日志,结合 Loki 检索,看 Envoy 的 duration 与 Java 的 Self 差值;若差值大,说明 Sidecar 里 TLS 解密、WAF 规则耗时,推动运维调优 Envoy filter 顺序或升级 CPU 指令集加速。