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

Spring WebClient多并发请求失败(Too many open files)求助

问题场景

使用Spring WebClient执行460个并发HTTPS请求进行模拟测试,要求所有请求同时启动,通过JUnit实现。当并发数低于440-450时正常,设置为460时部分线程抛出java.io.IOException: Too many open files错误。已将macOS Sequoia 15.1.1的系统文件打开限制从256修改为120000,但问题依旧。

测试代码如下:

@Test
void testConcurrentSimulations() throws InterruptedException {
    var simulationCount = 460;
    var latch = new CountDownLatch(simulationCount);
    var webClient = getWebClient(simulationCount);
    var params = getParams();
    var apiCallSupplier = getApiCallSupplier(webClient, params);

    var simulationThreads = IntStream.range(0, simulationCount)
            .mapToObj(i -> new Simulation(apiCallSupplier, latch))
            .toList();

    //when
    simulationThreads.forEach(Simulation::start);

    //then
    latch.await();
}

private WebClient getWebClient(int maxConnections) {
    var httpClient = HttpClient.create(
            ConnectionProvider.builder("myTestConnectionProvider")
                    .maxConnections(maxConnections)
                    .pendingAcquireTimeout(Duration.of(-1, ChronoUnit.SECONDS))
                    .build()
    );
    return WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();
}

private Supplier<Mono<SomeResponse>> getApiCallSupplier(WebClient webClient,
                                                        LinkedMultiValueMap<String, String> authParams) {
    return () -> webClient
            .post()
            .uri("https://someapi.com/endpoint")
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .body(BodyInserters.fromFormData(authParams))
            .retrieve()
            .bodyToMono(SomeResponse.class);
}

Simulation类代码:

public class Simulation extends Thread {

private final Supplier<Mono<SomeResponse>> apiCallSupplier;
private final CountDownLatch latch;

public Simulation(Supplier<Mono<SomeResponse>> apiCallSupplier,
                  CountDownLatch latch) {
    this.apiCallSupplier = apiCallSupplier;
    this.latch = latch;
}

@Override
public void run() {
    try {
        apiCallSupplier.get().subscribe(System.out::println);
    } finally {
        latch.countDown();
    }
}}

错误原因

  1. 线程模型不匹配导致资源浪费:WebClient基于Reactor异步非阻塞模型设计,但当前实现用了Thread+CountDownLatch的同步线程方式,每个线程触发一个Mono订阅,会创建460个线程,每个HTTPS请求需要占用至少一个文件描述符(Socket连接),加上SSL握手的额外资源,瞬间占用的文件描述符数量超过了JVM进程的限制。
  2. JVM进程文件描述符限制未更新:虽然修改了系统级的文件打开限制,但JVM进程自身的文件描述符上限可能仍为默认值(比如1024),未同步调整。
  3. CountDownLatch时机错误:当前代码中latch.countDown()在subscribe()后立即执行,而subscribe()是异步操作,主线程会提前完成等待,但请求可能还在处理中,资源无法及时回收,加剧了文件描述符的占用。
  4. 连接池配置不合理:pendingAcquireTimeout设为-1会让请求无限等待连接,导致请求堆积,进一步占用更多资源;同时未针对单域名设置maxConnectionsPerRoute,可能导致同一域名下的并发连接超过合理上限。

解决方法

  1. 改用Reactor原生并发方式:抛弃手动创建线程的方式,利用Reactor的Flux实现并发请求,让框架自动管理线程和资源,避免创建大量线程。修改测试代码如下:
@Test
void testConcurrentSimulations() {
    var simulationCount = 460;
    var webClient = getWebClient(simulationCount);
    var params = getParams();
    var apiCallSupplier = getApiCallSupplier(webClient, params);

    // 用Flux实现并发,指定并发数为simulationCount
    Flux.range(0, simulationCount)
            .flatMap(i -> apiCallSupplier.get(), simulationCount)
            .doOnNext(System.out::println)
            .blockLast(); // 等待所有请求完成
}

同时删除Simulation类,无需手动管理线程。

  1. 调整JVM进程文件描述符限制:

    • 临时生效:在运行测试前,在终端执行ulimit -n 120000,确保当前shell的文件描述符上限已修改。
    • 永久生效:在JVM启动参数中添加-XX:MaxFileDescriptorCount=120000,强制JVM使用更高的文件描述符上限。
  2. 修复CountDownLatch时机(如果保留线程方式):如果必须保留手动线程的方式,需要在请求完成后再执行countDown(),修改Simulation类的run()方法:

@Override
public void run() {
    apiCallSupplier.get()
            .subscribe(
                    System.out::println,
                    Throwable::printStackTrace,
                    latch::countDown // 请求完成后再递减latch
            );
}

注意:这种方式仍不如Reactor原生并发高效,不推荐。

  1. 优化连接池配置:
    • 设置maxConnectionsPerRoute,针对同一域名限制并发连接数,避免单域名连接过多:
    var httpClient = HttpClient.create(
            ConnectionProvider.builder("myTestConnectionProvider")
                    .maxConnections(simulationCount)
                    .maxConnectionsPerRoute(simulationCount) // 针对单域名设置最大连接数
                    .pendingAcquireTimeout(Duration.of(10, ChronoUnit.SECONDS)) // 设置合理的等待超时,避免无限堆积
                    .build()
    );
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 23:38:15