☰
记一次 SkyWalking 探针导致 WebFlux 三方调用超时的排查
2026/10/9 1:15:53 网站建设 项目流程

关键词:SkyWalking Agent、Spring WebFlux、NoSuchMethodError、witness 机制、插件版本错配

环境:Spring Boot 2.7.9(Spring Framework 5.3.25)+ SkyWalking Java Agent 9.7.0

一、现象

test 环境的应用日志里频繁出现下面这条 WARN,同时业务侧反馈:调用第三方 HTTP 接口大面积超时。

2026-09-29 [reactor-http-epoll-2] WARN reactor.core.Exceptions - throwIfFatal detected a jvm fatal exception, which is thrown and logged below: java.lang.NoSuchMethodError: 'org.springframework.http.HttpStatusCode org.springframework.web.reactive.function.client.ClientResponse.statusCode()' at org.apache.skywalking.apm.plugin.spring.webflux.v6.webclient .WebFluxWebClientInterceptor.lambda$afterMethod$1(WebFluxWebClientInterceptor.java:92) at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(...) ...

诡异的是:

  • 本地开发:完全正常,没有任何报错;
  • 生产环境:同样挂了 SkyWalking,也正常;
  • 只有 test 环境:报错 + 三方调用超时。

同一份代码、同一个 Spring 版本,为什么表现天差地别?下面是完整的排查过程。

二、先读懂这条异常

2.1 报错的不是业务代码,是探针

堆栈第一行的调用方是:

org.apache.skywalking.apm.plugin.spring.webflux.v6.webclient.WebFluxWebClientInterceptor

这是SkyWalking Agent 的插件代码,不是我们自己的业务代码。也就是说,崩溃发生在探针拦截 WebClient 响应的环节。

2.2 NoSuchMethodError 意味着"签名对不上"

报错的方法签名是:

HttpStatusCode ClientResponse.statusCode()
  • HttpStatusCode是Spring 6 才引入的类型;
  • 在Spring 5里,ClientResponse.statusCode()的返回值是HttpStatus,根本没有HttpStatusCode这个类。

所以:一个为 Spring 6 编写的插件(包名里的webflux.v6),跑在了Spring 5的应用上,按 Spring 6 的签名去调方法,运行时找不到该方法 →NoSuchMethodError。

2.3 先确认项目自身的 webflux 版本是干净的

用 Maven 依赖树核实,排除"项目自己依赖冲突"的可能:

mvn dependency:tree"-Dincludes=org.springframework:spring-webflux,org.springframework.boot:spring-boot-starter-webflux,io.projectreactor*"

结果:

依赖版本
spring-boot-starter-webflux2.7.9
spring-webflux5.3.25
reactor-netty-http1.0.28
reactor-core3.4.27

webflux 版本唯一、干净,就是 Spring 5。进一步全量扫描org.springframework:*,classpath 上没有任何 Spring 6 的 jar。

结论:问题不在项目,而在探针加载了错误版本的插件。

三、为什么"探针的错"会让三方调用超时

这一步是最容易想不通的:探针报个错而已,为什么业务调用会超时?答案在 Reactor 的异常处理机制。

  1. NoSuchMethodError属于Error(LinkageError的子类),不是普通Exception。
  2. 日志首行throwIfFatal detected a jvm fatal exception说明:Reactor 在onNext阶段检测到这是JVM 致命错误,会把它当作 fatal直接向上抛,而不会转成正常的onError信号。
  3. 正常的响应式流应该是:onNext(响应)→onComplete(),下游.block()/await收到完成信号才返回。
  4. 现在拦截器在onNext阶段就 fatal 崩了,响应式流的信号链被中断,onComplete永远发不出来。
  5. 下游调用方还在死等那个永远不会到达的完成信号 → 触发超时 / 一直阻塞。

关键认知:HTTP 请求其实已经成功发出、也拿到响应了,但探针在处理响应时崩溃,把 Reactor 的信号链搞断了,业务侧拿不到结果,只能等到超时。

而 Agent 的字节码增强是全局的、按类型匹配的:项目里每一个 WebClient 请求的响应都会经过同一个被织入的坏拦截器。所以不是某一条业务受影响,而是所有走 WebClient 的三方调用集体超时。

因果链总结:

SkyWalking 6.x 插件(为 Spring 6 编写) ↓ 被错误加载进 Spring 5.3.25 应用 织入 WebClient 响应拦截器 ↓ 调用 Spring 6 才有的 statusCode() 签名 NoSuchMethodError(Error,非 Exception) ↓ Reactor throwIfFatal 判定为 JVM 致命错误,直接上抛 响应式流信号链断裂,onComplete 发不出 ↓ 所有走 WebClient 的三方调用卡在等待 下游超时

四、三个环境为什么表现不同

差异既不在代码,也不在日志配置,而在各环境挂载的 SkyWalking Agent 不同。

环境是否挂 AgentAgent 情况结果
本地否(IDE 直接 Run,无-javaagent)探针根本没注入不报错
test是9.7.0,6.x 插件被错误激活报错 + 超时
生产是版本/插件集不同,未触发不报错
  • 本地不报错:因为本地 IDE 启动没有-javaagent,探针没被注入,WebClient是原生干净的,走 Spring 5 自己的statusCode(),一切正常。
  • test 报错:部署时通过JAVA_OPTS/ Dockerfile / K8s initContainer 挂载了 agent,探针被织入,坏掉的 6.x 插件被激活。
  • 生产不报错:生产挂的 agent 与 test不是同一套(版本不同,或plugins/内容被清理过),6.x 插件没被激活。

顺带排除一个误导性线索:logback vs logback-spring

一开始怀疑是不是日志配置(生产用logback.xml、test 用logback-spring.xml)导致的。对比两份文件后确认:这是条误导线索。

  • 两份配置在本问题上完全相同,都用了同一个 SkyWalking 日志布局TraceIdPatternLogbackLayout;
  • 区别只是logback-spring.xml多了 DEBUG/WARN/ALL 几个 appender 和 MyBatis 日志级别,只影响日志写到哪、写多少级别;
  • logback.xml与logback-spring.xml的真正区别只是加载时机和是否支持<springProfile>标签,跟 WebClient 探针崩溃毫无关系。

五、根因:witness 机制误判

SkyWalking 确实有"自动匹配插件版本"的机制,叫witness(见证)机制——但它不是读你的 Spring Boot 版本号,而是靠探测 JVM 里某些特定类/方法是否存在来判断框架版本。

  • webflux-5.x 插件:见证条件命中 Spring 5 特征时激活;
  • webflux-6.x 插件:本应只在探测到 Spring 6 特征时才激活。

问题在于:witness 是"尽力而为(best-effort)"的猜测,会误判。查看 Agent 启动日志skywalking-agent/logs/skywalking-api.log后确认——在我们的 Spring 5 环境里,6.x 插件确实被激活了。

而 classpath 上根本没有 Spring 6 的类,说明 6.x 插件的 witness 条件写得过宽/有缺陷,把 Spring 5 也存在的某个特征误认成了 Spring 6,于是错误激活,运行时才暴露签名不匹配。

一句话:witness 靠"特征类探测"猜框架版本,不是精确读版本号。猜错了就会激活不匹配的插件,然后在运行时崩。

六、解决方案

6.1 为什么"换版本"不是好办法

  • 升级到更新版:不保证。9.7.0 已足够新,且没有公开 changelog/issue 明确说明某版本修复了该 witness 误判,升上去可能照样误判。
  • 降级到老版:有一定道理(早于 webflux 插件拆分 5.x/6.x 的老 agent 根本没有 6.x 插件),但会丢掉这几年的其它修复与兼容性改进,得不偿失。

结论:换版本是碰运气,不是稳妥方案。

6.2 推荐方案:移除错误激活的 6.x 插件(与版本无关、100% 确定)

插件文件都不在了,witness 再怎么误判也无从加载,问题必然消失;且 5.x 插件继续正常追踪 WebClient,Spring 5 功能零损失。

关键原则:必须移出plugins/目录。只要 jar 还在plugins/里,Agent 就会加载它,移到子目录也没用。

cd/usr/local/soft/zhiyuxue/skywalking-agent-9.7.0/skywalking-agent/# 1. 先看清楚确切文件名lsplugins/|grep-Ei"webflux|webmvc"# 2. 建停用目录(和 plugins 同级,切勿放 plugins 里面)mkdir-pdisabled-plugins# 3. 用 mv 而非 rm,方便随时回滚mvplugins/apm-spring-webflux-6.x-plugin-*.jar disabled-plugins/mvplugins/apm-spring-webmvc-6.x-plugin-*.jar disabled-plugins/# 4. 确认 plugins 里只剩 5.xlsplugins/|grep-Ei"webflux|webmvc"

第 4 步应只剩:

apm-spring-webflux-5.x-plugin-*.jar apm-spring-webmvc-5.x-plugin-*.jar

注意事项:

  1. 别误删 5.x 插件——那是 Spring 5 真正要用的,只移 6.x。
  2. 别移到optional-plugins/或bootstrap-plugins/——那两个目录 Agent 同样会加载(bootstrap 还是强制加载),等于没移。就用自建的disabled-plugins/。
  3. 改完必须重启应用进程——Agent 只在启动时加载插件,不重启不生效。

重启后验证:NoSuchMethodError ... WebFluxWebClientInterceptor不再出现,三方调用不再超时,即修复成功。

6.3 应急止血(动不了 Agent 时)

如果暂时无法操作 Agent,可临时把关键的三方调用从响应式栈(WebClient)换成非响应式栈(RestTemplate/OkHttp),绕开被污染的 webflux 拦截器。但这只是应急,正解仍是处理 Agent。

七、附带收获

7.1 精简插件目录还能解决"启动慢"

加了 SkyWalking 后启动变慢是普遍现象,主因是:

  1. 字节码增强:应用启动加载成千上万个类,每加载一个类,agent 都要拿它和所有插件定义做一次匹配,命中的还要改写字节码。插件越多,逐类匹配成本越高。
  2. 全量插件扫描:启动时加载plugins/下全部jar、跑每个插件的 witness 检查。默认带一两百个插件,用不到的也全扫。
  3. 连接 OAP Collector:初始化 gRPC 通道连collector.backend_service,若地址不通/网络慢/DNS 慢,会卡在启动阶段。

提速办法:

  • 精简plugins/目录(性价比最高):把用不到的插件移到disabled-plugins/。正好和移除 6.x 插件是同一件事,顺手把不相关框架的插件一起清掉,既修 bug 又提启动速度。
  • 确认 OAP 连接顺畅:检查agent.config的collector.backend_service在 test 环境是否可达。
  • 看日志定位瓶颈:skywalking-api.log能看到各阶段耗时、加载插件数、连接报错,先分清是慢在"字节码增强"还是"连 OAP"。
  • 注意:采样率(agent.sample_n_per_3_secs等)是运行时开销,与启动速度无关。

7.2 一个顺手发现的依赖隐患

排查依赖版本时发现service模块手动写死了spring-test的版本和作用域:

<!-- service/pom.xml --><spring-test.version>5.3.8</spring-test.version>...<dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><version>${spring-test.version}</version><!-- 绕过 BOM,锁死 5.3.8 --><scope>compile</scope><!-- 正常应为 test --></dependency>

导致项目里spring-*出现5.3.25 与 5.3.8 两个版本并存。

  • 这不是本次 webflux 报错的原因(两者都是 Spring 5);
  • 但版本不一致本身是隐患。建议去掉<version>让 Spring Boot BOM 托管(自动对齐 5.3.25),并把 scope 改为test;
  • 改 scope 前要确认src/main里没有引用org.springframework.test.*的类,否则会编译失败。

八、经验总结

  1. NoSuchMethodError里出现org.apache.skywalking...包名,第一反应就该是"探针插件与应用框架版本错配",而不是怀疑业务代码或依赖冲突。
  2. 探针(javaagent)是跑在你自己 JVM 里的第三方代码,它出 bug 就等于你的进程出 bug;"服务端的问题"这个直觉是错的。
  3. 响应式栈里,Error级致命异常会中断信号链,导致onComplete发不出,表现为"超时"而非"报错"——这类"请求明明成功却超时"的现象要往信号链断裂方向想。
  4. witness 是猜测,不是精确匹配,会误判;处理探针插件错配,"移除错误插件"比"换 agent 版本"更确定、更可控。
  5. 多环境表现不一致时,优先对比环境差异(是否挂 agent、agent 版本、插件集、网络),而不是先怀疑代码——本例中日志配置(logback vs logback-spring)就是一条典型的误导线索。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询