You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring WebClient 5高负载触发executor not accepting a task异常排查

Spring WebFlux高负载下WebClient抛出executor not accepting a task异常根因分析

问题现象

  • 生产环境部署Spring WebFlux应用,业务逻辑通过WebClient 5发起HTTP调用下游服务
  • 异常仅在生产环境7K TPS高负载水位下触发,本地低负载环境无法复现
  • 异常栈信息为:java.lang.IllegalStateException: executor not accepting a task

相关代码片段

HTTP调用实现逻辑:

public Mono<Context> doHttp(WebClient webClient, WBConfig webClientConfiguration, String data, Context ctx) {
     return getWebclient(webClient, webClientConfiguration, data, ctx)
        .headers(httpHeaders -> logHeadersAndPayload(ctx, data, httpHeaders))
        .retrieve()
        .bodyToMono(Object.class)
        .publishOn(TPHelper.getSharedPool())
        .onErrorResume(getErrorHandleFunction(webClientConfiguration, ctx, taskName))
        .retryWhen(buildRetrySpec(webClientConfiguration))
        .flatMap(getResponseToContextMonoFunction(ctx, taskName, startTime));
}

自定义共享调度器逻辑(生产环境核心逻辑示意):

public class TPHelper {
// 初始化后创建共享线程池
private static Scheduler tp;

@PostConstruct
void init() {
  this.tp = Schedulers.newBoundedElastic(100, Integer.MAX_VALUE, new ThreadFactoryBuilder().setNameFormat("http-threads").build(), 60);
}

public static Scheduler getSharedPool() {
  return this.tp;
}
}

根因分析

这个异常就是自定义BoundedElastic线程池任务过载触发拒绝导致的,和初步怀疑方向一致,核心触发逻辑如下:

  • 链路中.publishOn(TPHelper.getSharedPool())会将该算子之后的所有逻辑(错误处理、重试触发、响应转换业务逻辑)全调度到自定义的100线程BoundedElastic池执行,不再占用Netty EventLoop线程
  • ReactorBoundedElastic调度器的拒绝逻辑为:当工作线程数达到配置上限(当前配置为100),新提交任务进入全局排队队列,当排队任务数达到配置的队列容量上限时,直接抛出executor not accepting a task拒绝任务
  • 7K TPS水位下,100个工作线程只要单任务平均耗时超过14ms就会达到处理瓶颈(100线程 * 1000ms/14ms ≈ 7142 TPS)。如果后续逻辑存在阻塞、重试风暴等问题,实际处理能力会远低于理论值,任务快速积压就会打满队列触发异常
  • 贴出的TPHelper为逻辑示意,生产环境大概率没有把队列容量(newBoundedElastic第二个参数queuedTaskCap)设为Integer.MAX_VALUE——如果真用无界队列不会抛这个拒绝异常,只会触发OOM。默认BoundedElastic队列容量仅为100000,高负载下很快就会打满
  • retryWhen放在publishOn之后,所有重试任务也会提交到这个共享线程池,一旦出现下游超时、响应慢的情况,重试流量会进一步加剧队列积压,形成雪崩。

排查方向

  • 先核对生产环境实际运行的线程池初始化参数,不要参考示意代码:直接反编译生产部署包的TPHelper类,或通过监控系统查看线程池配置,重点确认newBoundedElastic传入的线程上限、队列容量两个参数,90%的场景是队列容量实际配置过小,被突发流量打满
  • 给自定义http-threads线程池埋点监控核心指标:工作线程活跃数、队列等待任务长度、任务平均执行耗时、任务拒绝次数,异常抛出的时间点必然和队列长度达到上限的时间点完全重合
  • 全链路排查publishOn之后的所有执行逻辑:包括错误处理函数、重试判断逻辑、响应转换逻辑,绝对不能存在任何阻塞操作:比如同步数据库/缓存调用、文件IO、Thread.sleep()、重量级锁等待、同步HTTP调用。哪怕在BoundedElastic线程池上,阻塞操作会直接占死工作线程,让线程池吞吐量骤降,是这类问题最常见的诱因
  • 检查算子顺序合理性:如果加publishOn只是为了把较重的业务响应处理逻辑从EventLoop剥离开,应该把publishOn放到retryWhen之后,避免重试调度、重试判断这类轻量逻辑占用业务线程池资源;如果没有强制的线程隔离需求,完全没必要手动加这层publishOn,非阻塞的响应逻辑直接跑在Netty EventLoop上性能更高
  • 排查线程池生命周期逻辑:确认TPHelper是单例Bean,初始化方法init()仅在应用启动时执行一次,没有任何业务代码路径会主动调用这个Scheduler的dispose()方法,避免线程池被意外关闭后提交任务抛错。

临时优化方案

  • 临时扩容线程池:根据实际任务平均耗时调整线程数上限,比如单任务平均耗时30ms,7K TPS下理论需要210个工作线程,预留30%冗余可以设为280,不要盲目设太大避免上下文切换开销
  • 调整队列容量:不要设为Integer.MAX_VALUE避免无界积压导致OOM,配置为可支撑10秒左右的流量积压即可(比如7K TPS设为70000),同时配置队列长度告警
  • 优化重试逻辑:给重试加指数退避,严格限制最大重试次数,避免下游故障时重试风暴打满线程池。

内容的提问来源于stack exchange,提问作者Pavan Kumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 16:15:32