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

Spring Webflux WebClient启用Keep-Alive时SSLSocket关闭警告排查咨询

Spring Webflux WebClient 长连接SSL关闭警告问题排查

问题背景

我的应用基于Spring Webflux WebClient,需要向一个无法控制的启用SSL的第三方服务器发送数据。使用的客户端初始化代码如下:

WebClient client = WebClient.create().mutate()
    .clientConnector(new ReactorClientHttpConnector(
        HttpClient.create().keepAlive(true)
            .secure(sslContextSpec -> sslContextSpec.sslContext(getMySSLContext()))
    )).build();

final var response = client.post().uri("https://...")

问题现象

在高负载场景下(向服务器发送大量请求),频繁看到如下警告堆栈:

javax.net.ssl|WARNING|79|Keep-Alive-Timer|2023-05-15 11:05:39.124 CST|SSLSocketImpl.java:595|SSLSocket duplex close failed. Debug info only. Exception details: (
"throwable" : {
  java.net.SocketException: Socket is closed
    at java.base/java.net.Socket.shutdownInput(Socket.java:1601)
    at java.base/sun.security.ssl.BaseSSLSocketImpl.shutdownInput(BaseSSLSocketImpl.java:217)
    at java.base/sun.security.ssl.SSLSocketImpl.shutdownInput(SSLSocketImpl.java:848)
    at java.base/sun.security.ssl.SSLSocketImpl.bruteForceCloseInput(SSLSocketImpl.java:798)
    at java.base/sun.security.ssl.SSLSocketImpl.duplexCloseOutput(SSLSocketImpl.java:660)
    at java.base/sun.security.ssl.SSLSocketImpl.close(SSLSocketImpl.java:584)
    at java.base/sun.net.www.http.HttpClient.closeServer(HttpClient.java:1137)
    at java.base/sun.net.www.http.KeepAliveCache.run(KeepAliveCache.java:260)
    at java.base/java.lang.Thread.run(Thread.java:833)
    at java.base/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:162)}

)
javax.net.ssl|WARNING|C6|Keep-Alive-Timer|2023-05-15 10:08:48.053 UTC|null:-1|SSLSocket duplex close failed. Debug info only. Exception details: (
"throwable" : {
  java.net.SocketException: Socket is closed
        at java.base/java.net.Socket.shutdownInput(Unknown Source)
        at java.base/sun.security.ssl.BaseSSLSocketImpl.shutdownInput(Unknown Source)
        at java.base/sun.security.ssl.SSLSocketImpl.shutdownInput(Unknown Source)
        at java.base/sun.security.ssl.SSLSocketImpl.bruteForceCloseInput(Unknown Source)
        at java.base/sun.security.ssl.SSLSocketImpl.duplexCloseOutput(Unknown Source)
        at java.base/sun.security.ssl.SSLSocketImpl.close(Unknown Source)
        at java.base/sun.net.www.http.HttpClient.closeServer(Unknown Source)
        at java.base/sun.net.www.protocol.https.HttpsClient.closeServer(Unknown Source)
        at java.base/sun.net.www.http.KeepAliveCache.run(Unknown Source)
        at java.base/java.lang.Thread.run(Unknown Source)
        at java.base/jdk.internal.misc.InnocuousThread.run(Unknown Source)}

)

咨询问题

  1. 该警告信息具体含义是什么?
  2. 是第三方服务器关闭了Socket,还是客户端自身关闭的?
  3. 如果是客户端关闭的,为何已设置keepAlive(true)仍会关闭Socket?如何避免客户端关闭Socket?
  4. 如何解决该问题?还有哪些进一步调试的方法?

问题解答

1. 警告信息的具体含义

这个警告是JDK SSL底层在尝试关闭一个已经处于关闭状态的SSL Socket时触发的。从堆栈可以看到,是客户端的Keep-Alive-Timer线程在清理长连接缓存里的Socket时,发现Socket已经被关闭,执行关闭操作时抛出了这个异常。它属于调试级别的警告,不会直接导致请求失败,但说明长连接的管理出现了不一致的情况。

2. 谁关闭了Socket?

从调用链分析,有两种可能:

  • 更常见的情况是第三方服务器主动关闭:服务器通常会有连接超时、最大连接数限制等策略,当连接空闲过久或者达到连接上限时,会主动断开Socket。客户端的长连接缓存还没及时感知到这个状态,后续清理线程执行关闭操作时就会触发警告。
  • 客户端自身重复关闭:客户端的连接池清理机制可能先关闭了Socket,但后续的清理逻辑重复执行了关闭操作,不过这种情况相对少见。

3. 设置keepAlive(true)为何还会关闭Socket?

keepAlive(true)只是开启了TCP长连接的支持,不代表连接会永久保持:

  • 客户端的Reactor Netty连接池本身有闲置超时、最大存活时间的默认配置,闲置过久的连接会被客户端主动清理。
  • 第三方服务器也会有自己的连接超时策略,不管客户端是否开启长连接,服务器都可能主动断开空闲连接。
  • 网络中间设备(比如负载均衡、防火墙)也可能主动断开长时间空闲的连接。

如果要避免客户端主动关闭Socket,可以调整Reactor Netty连接池的参数:

  • 延长idleTimeout(闲置超时)的时间,尽量和服务器的超时策略匹配
  • 调整maxLifeTime(连接最大存活时长),避免客户端过早回收连接

4. 解决方法与进一步调试手段

解决方法

  • 调整客户端连接池参数:显式配置连接池的闲置超时和最大存活时间,示例代码如下:
HttpClient httpClient = HttpClient.create()
    .keepAlive(true)
    // 配置连接池参数
    .pool(pool -> pool
        .idleTimeout(Duration.ofMinutes(5)) // 闲置超时,根据服务器实际超时调整
        .maxLifeTime(Duration.ofHours(1))   // 连接最大存活时间
    )
    .secure(sslContextSpec -> sslContextSpec.sslContext(getMySSLContext()));

WebClient client = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .build();
  • 忽略警告(业务无异常时):这个警告只是调试信息,不会影响请求执行。如果业务没有异常,可以通过日志配置关闭javax.net.ssl的WARNING级别日志,比如在logback.xml中添加:
<logger name="javax.net.ssl" level="ERROR"/>
  • 配置TCP层面的keep-alive探测:让客户端主动探测连接是否存活,提前感知服务器的断开,避免无效的连接清理:
HttpClient httpClient = HttpClient.create()
    .keepAlive(true)
    .tcpConfiguration(tcp -> tcp
        .option(ChannelOption.SO_KEEPALIVE, true)
        .option(EpollChannelOption.TCP_KEEPIDLE, 300) // 空闲300秒后开始发探测包
        .option(EpollChannelOption.TCP_KEEPINTVL, 60) // 每60秒发一次探测包
        .option(EpollChannelOption.TCP_KEEPCNT, 5)    // 探测失败5次后断开连接
    )
    .secure(sslContextSpec -> sslContextSpec.sslContext(getMySSLContext()));

进一步调试手段

  • 开启Reactor Netty DEBUG日志:配置reactor.netty.http.client的日志级别为DEBUG,查看连接的创建、复用、关闭全过程,排查连接池的管理逻辑。
  • 抓包分析:用Wireshark抓取SSL连接的数据包,确认是服务器发送了FIN/RST包主动断开,还是客户端主动关闭连接。
  • 对齐服务器配置:如果能联系第三方服务器运维,了解他们的连接超时、最大连接数等配置,方便客户端参数调整。
  • 测试不同配置:尝试调整闲置超时、最大存活时间等参数,观察警告出现的频率变化,找到最优配置。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 12:53:08