关键词: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-webflux | 2.7.9 |
| spring-webflux | 5.3.25 |
| reactor-netty-http | 1.0.28 |
| reactor-core | 3.4.27 |
webflux 版本唯一、干净,就是 Spring 5。进一步全量扫描org.springframework:*,classpath 上没有任何 Spring 6 的 jar。
结论:问题不在项目,而在探针加载了错误版本的插件。
三、为什么"探针的错"会让三方调用超时
这一步是最容易想不通的:探针报个错而已,为什么业务调用会超时?答案在 Reactor 的异常处理机制。
NoSuchMethodError属于Error(LinkageError的子类),不是普通Exception。- 日志首行
throwIfFatal detected a jvm fatal exception说明:Reactor 在onNext阶段检测到这是JVM 致命错误,会把它当作 fatal直接向上抛,而不会转成正常的onError信号。 - 正常的响应式流应该是:
onNext(响应)→onComplete(),下游.block()/await收到完成信号才返回。 - 现在拦截器在
onNext阶段就 fatal 崩了,响应式流的信号链被中断,onComplete永远发不出来。 - 下游调用方还在死等那个永远不会到达的完成信号 → 触发超时 / 一直阻塞。
关键认知: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 不同。
| 环境 | 是否挂 Agent | Agent 情况 | 结果 |
|---|---|---|---|
| 本地 | 否(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注意事项:
- 别误删 5.x 插件——那是 Spring 5 真正要用的,只移 6.x。
- 别移到
optional-plugins/或bootstrap-plugins/——那两个目录 Agent 同样会加载(bootstrap 还是强制加载),等于没移。就用自建的disabled-plugins/。 - 改完必须重启应用进程——Agent 只在启动时加载插件,不重启不生效。
重启后验证:NoSuchMethodError ... WebFluxWebClientInterceptor不再出现,三方调用不再超时,即修复成功。
6.3 应急止血(动不了 Agent 时)
如果暂时无法操作 Agent,可临时把关键的三方调用从响应式栈(WebClient)换成非响应式栈(RestTemplate/OkHttp),绕开被污染的 webflux 拦截器。但这只是应急,正解仍是处理 Agent。
七、附带收获
7.1 精简插件目录还能解决"启动慢"
加了 SkyWalking 后启动变慢是普遍现象,主因是:
- 字节码增强:应用启动加载成千上万个类,每加载一个类,agent 都要拿它和所有插件定义做一次匹配,命中的还要改写字节码。插件越多,逐类匹配成本越高。
- 全量插件扫描:启动时加载
plugins/下全部jar、跑每个插件的 witness 检查。默认带一两百个插件,用不到的也全扫。 - 连接 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.*的类,否则会编译失败。
八、经验总结
NoSuchMethodError里出现org.apache.skywalking...包名,第一反应就该是"探针插件与应用框架版本错配",而不是怀疑业务代码或依赖冲突。- 探针(javaagent)是跑在你自己 JVM 里的第三方代码,它出 bug 就等于你的进程出 bug;"服务端的问题"这个直觉是错的。
- 响应式栈里,
Error级致命异常会中断信号链,导致onComplete发不出,表现为"超时"而非"报错"——这类"请求明明成功却超时"的现象要往信号链断裂方向想。 - witness 是猜测,不是精确匹配,会误判;处理探针插件错配,"移除错误插件"比"换 agent 版本"更确定、更可控。
- 多环境表现不一致时,优先对比环境差异(是否挂 agent、agent 版本、插件集、网络),而不是先怀疑代码——本例中日志配置(logback vs logback-spring)就是一条典型的误导线索。