Spring Boot WebClient批量调用报Too many open files问题求助
问题根源与解决方案
你的核心问题不是单纯的文件描述符不足,而是每次调用getResponseV2方法都重复创建重量级资源,导致资源泄漏和文件描述符耗尽:
- 每次调用都新建
SslContext:这个对象包含SSL信任库等资源,完全不需要重复创建 - 每次调用都新建
TcpClient、HttpClient:Netty的HttpClient自带连接池,每次新建会导致连接无法复用,每个新连接都会占用文件描述符 - 每次调用都新建
WebClient实例:WebClient是线程安全的,应该全局复用,重复创建会带来不必要的资源开销
以下是具体修复步骤:
1. 复用SslContext
把SslContext做成单例,只初始化一次:
private static final SslContext SSL_CONTEXT; static { try { SSL_CONTEXT = SslContextBuilder .forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .build(); } catch (SSLException e) { throw new RuntimeException("Failed to initialize SSL context", e); } }
2. 配置全局可复用的HttpClient
创建一个全局的HttpClient实例,配置连接池参数控制并发连接数:
private static final HttpClient HTTP_CLIENT; static { TcpClient tcpClient = TcpClient.create() .secure(sslCtxSpec -> sslCtxSpec.sslContext(SSL_CONTEXT)) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) .option(ChannelOption.SO_KEEPALIVE, true); HTTP_CLIENT = HttpClient.from(tcpClient) .wiretap("reactor.netty.http.client.HttpClient", LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL) .responseTimeout(Duration.ofSeconds(20)) // 配置连接池参数,根据实际并发调整 .poolResources(PoolResources.fixed("http-client-pool", 200)) // 最大连接数 .keepAlive(Duration.ofMinutes(5)); // 连接空闲超时 }
3. 复用WebClient实例
WebClient是线程安全的,可按baseUrl缓存复用:
// 用ConcurrentHashMap缓存不同baseUrl的WebClient实例 private final Map<String, WebClient> webClientCache = new ConcurrentHashMap<>(); private WebClient getWebClient(String deviceConnection) { return webClientCache.computeIfAbsent(deviceConnection, baseUrl -> WebClient.builder() .baseUrl(baseUrl) .clientConnector(new ReactorClientHttpConnector(HTTP_CLIENT)) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build() ); }
4. 修改getResponseV2方法,复用资源
调整后的方法不再重复创建资源:
private <T> String getResponseV2(String deviceConnection, String uri, T body, Consumer<HttpHeaders> headersConsumer) { try { return getWebClient(deviceConnection) .post() .uri(uri) .headers(headersConsumer) .body(Mono.just(body), body.getClass()) .retrieve() .bodyToMono(String.class) .block(); } catch (Exception e) { log.error("Failed to fetch data from {}: {}", deviceConnection + uri, e.getLocalizedMessage()); log.error("Error details:", e); return null; } }
5. 控制批处理并发数
单次任务发起数千次调用时,要控制并发量避免连接池过载,比如用Reactor的并发控制:
// 示例:用Reactor控制同时处理100个请求 Flux.fromIterable(deviceList) .parallel(100) .runOn(Schedulers.boundedElastic()) .flatMap(device -> // 先获取token,再发起实际请求 getTokenMono(device) .flatMap(token -> doActualPostMono(device, token)) ) .sequential() .blockLast();
关于文件描述符限制
修复资源复用问题后,文件描述符耗尽的情况会大幅缓解。如果仍有需求,可以调整Docker容器的文件描述符限制:
- 启动容器时添加参数:
--ulimit nofile=65535:65535 - 或者在Dockerfile中添加:
RUN ulimit -n 65535
但这只是辅助手段,核心还是修复资源复用问题,否则即使提高限制,随着调用次数增加,依然会耗尽资源。
内容的提问来源于stack exchange,提问作者Vaibhav
相关产品推荐
相关产品推荐

