一、问题现象
某基于 Java 应用在 Windows Server 上运行一段时间后,突然抛出异常:
java.net.SocketException: No buffer space available (maximum connections reached?): connect 或 java.net.BindException: Address already in use: getsockopt
应用无法处理新请求,重启后短暂恢复,但几小时后再次出现相同错误。初步判断为系统资源耗尽,但具体原因需要深入排查。
二、环境信息
- 操作系统:Windows Server 2019
- JDK 版本:Java 17.0.14
- 应用框架:Spring Boot 2.x
- 进程 PID:12345(示例)
三、排查过程
1. 检查 TCP 连接总数
以管理员身份打开命令提示符,执行:
netstat -an | find /c "TCP"
输出结果:
63113
系统当前存在63,113个 TCP 连接,远超正常水平。
2. 检查动态端口范围
netsh int ipv4 show dynamicport tcp
输出:
协议 tcp 动态端口范围
启动端口 : 9000
端口数 : 56000
动态端口范围已设置为 9000~64999(共 56000 个),但连接数已超过此范围,端口耗尽。
3. 分析连接状态分布
统计各种状态的连接数量:
netstat -an | find /c "TIME_WAIT"
netstat -an | find /c "ESTABLISHED"
netstat -an | find /c "CLOSE_WAIT"
输出示例:
TIME_WAIT: 281
ESTABLISHED: 7120
CLOSE_WAIT: 55690
关键发现:55,690个连接处于 CLOSE_WAIT 状态,占总连接数约 88%,这是典型的连接泄漏。
4. 查看 CLOSE_WAIT 连接的详细信息
netstat -ano | findstr CLOSE_WAIT
输出片段:
TCP 192.168.1.100:54995 203.0.113.5:8080 CLOSE_WAIT 12345
TCP 192.168.1.100:54996 203.0.113.5:8080 CLOSE_WAIT 12345
TCP 192.168.1.100:54997 203.0.113.5:8080 CLOSE_WAIT 12345
...
- 本地地址:
192.168.1.100(应用服务器)
- 远程地址:
203.0.113.5:8080(固定的目标服务)
- 状态:
CLOSE_WAIT
- 进程 PID:
12345(与 Java 应用 PID 一致)
结论:应用程序作为 TCP 客户端,向 203.0.113.5:8080 发起大量连接,但未正确关闭,导致连接堆积在 CLOSE_WAIT 状态,最终耗尽临时端口。
四、根本原因分析
当 TCP 连接被对端主动关闭后,本地会进入 CLOSE_WAIT 状态,此时应用程序必须调用 close() 释放 socket 资源。如果应用程序未关闭 socket,连接将永远停留在 CLOSE_WAIT,占用端口和句柄。
根据 netstat 信息,本应用频繁连接 203.0.113.5:8080,但代码中未正确释放连接资源。经代码审查,发现应用使用 Apache HttpClient 发送 HTTP 请求,但未关闭 CloseableHttpResponse 对象,导致底层 TCP 连接无法释放。
五、解决方案
1. 修复代码:确保资源正确关闭
错误示例(导致泄漏)
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet request = new HttpGet("http://203.0.113.5:8080/api");
CloseableHttpResponse response = httpClient.execute(request);
String result = EntityUtils.toString(response.getEntity());
正确示例(使用 try-with-resources)
private static final CloseableHttpClient httpClient = HttpClients.custom()
.setMaxConnTotal(200)
.setMaxConnPerRoute(50)
.build();
public String callRemoteApi() {
HttpGet request = new HttpGet("http://203.0.113.5:8080/api");
try (CloseableHttpResponse response = httpClient.execute(request)) {
return EntityUtils.toString(response.getEntity());
} catch (IOException e) {
}
}
public void destroy() {
try { httpClient.close(); } catch (IOException e) { }
}
关键点:
- 每次
execute() 返回的 CloseableHttpResponse 必须关闭。
- 使用连接池(复用
CloseableHttpClient)减少连接创建开销。
- 确保消费响应体(如
EntityUtils.toString()),否则连接可能无法释放。
2. 调整 Windows 动态端口范围(临时缓解)
在代码修复上线前,可扩大端口范围以延缓耗尽:
netsh int ipv4 set dynamicport tcp start=1024 num=64512
此命令将动态端口范围设为 1024~65535(共 64512 个),修改后立即生效,无需重启。
3. 缩短 TIME_WAIT 延迟(非必需,但可优化)
如果存在大量短连接,可缩短 TIME_WAIT 时长(默认 240 秒),加速端口回收。通过注册表调整:
- 打开注册表编辑器:
regedit
- 定位到:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
- 新建 DWORD(32 位)值,名称为
TcpTimedWaitDelay,数值数据输入 30(十进制),表示 30 秒。
- 重启生效。
六、验证修复效果
- 重启应用。
- 监控 CLOSE_WAIT 数量:
netstat -an | find /c "CLOSE_WAIT"
应保持稳定且较低(例如几十到几百,取决于并发请求数)。
3. 监控总连接数,不应持续增长。
七、预防措施
- 代码规范:所有网络资源(Socket、HttpResponse、数据库连接等)必须在 finally 块或 try-with-resources 中关闭。
- 连接池化:使用连接池管理 HTTP 连接、数据库连接,避免为每个请求创建新连接。
- 监控告警:定期检查系统 CLOSE_WAIT 数量,设置阈值告警(例如超过 1000 触发报警)。
- 压力测试:上线前进行压力测试,观察连接数是否稳定。
- 日志记录:在关键网络操作处添加日志,便于追踪未关闭的连接。
八、总结
本次故障源于应用程序作为 HTTP 客户端未正确关闭连接,导致海量 CLOSE_WAIT 连接堆积,最终耗尽系统临时端口。通过代码修复(确保资源释放)和连接池优化,彻底解决了问题。Windows 环境下 netstat 是排查网络问题的利器,结合进程 PID 可快速定位泄漏源。希望本文能帮助读者避免类似问题,提升应用稳定性。
附:一键复制命令
以下命令可直接复制使用:
:: 查看 TCP 连接总数
netstat -an | find /c "TCP"
:: 查看 CLOSE_WAIT 数量
netstat -an | find /c "CLOSE_WAIT"
:: 查看 CLOSE_WAIT 详细信息
netstat -ano | findstr CLOSE_WAIT
:: 扩大动态端口范围
netsh int ipv4 set dynamicport tcp start=1024 num=64512
:: 查看动态端口范围
netsh int ipv4 show dynamicport tcp