临时端口被webservice耗尽
一、问题现象
最近项目中遇到一个诡异的问题:应用明明已经成功启动了,但控制台却打印了端口被占用的错误日志;接口有时能正常调用,有时却失败;对方Nginx说根本没收到请求,但我的代码明明已经执行了。
更让人困惑的是,netstat 显示端口被占用,但 taskkill 杀掉进程后问题依旧。
这到底是怎么回事?
二、排查过程
第一阶段:怀疑代码问题
起初,我以为是 HttpURLConnection 使用不当导致请求没发出去。检查了代码,确认调用了 getInputStream(),请求理论上应该已经发出。
java
BufferedReader in = new BufferedReader(
new InputStreamReader(con.getInputStream()), SIZE);
第二阶段:怀疑网络问题
对方反馈“请求没到我们的Nginx”,我开始排查网络链路:
-
检查防火墙规则 ✓
-
检查代理设置 ✓
-
检查hosts文件 ✓
-
用curl测试连通性 ✓
一切都正常,但问题依然存在。
第三阶段:发现TIME_WAIT异常
执行 netstat -an | findstr TIME_WAIT 发现了异常:
text
TCP 172.16.10.75:9438 172.16.10.201:57152 TIME_WAIT 0 TCP 172.16.10.75:9438 172.16.10.201:57368 TIME_WAIT 0 ...(共2.5万条)
25,594 个 TIME_WAIT 连接! 这远超 Windows 默认的临时端口范围(16,384个)。
第四阶段:定位TIME_WAIT来源
通过分析连接方向,发现这些 TIME_WAIT 来自另一台服务器(172.20.1.75)频繁调用我的 WebService(172.20.1.201:9438)。
问题根因:客户端使用短连接调用,每次请求后主动关闭连接,导致 TIME_WAIT 堆积在服务端。
第五阶段:启用端口复用
为了解决端口快速重启问题,添加了 Tomcat 配置:
java
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
return factory -> factory.addConnectorCustomizers(connector -> {
connector.setProperty("socket.soReuseAddress", "true");
connector.setProperty("reuseAddress", "true");
});
}
配置后,应用可以快速重启了,但控制台依然报端口占用错误。
第六阶段:发现IPv6连接堆积
进一步排查发现,有大量 IPv6 本地回环连接处于 TIME_WAIT:
text
TCP [::1]:51490 [::1]:9438 TIME_WAIT 0 TCP [::1]:51559 [::1]:9438 TIME_WAIT 0
这说明有客户端通过 IPv6 的 localhost 在疯狂调用服务。
第七阶段:最终真相
在反复检查代码后,终于发现了问题所在:
java
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
// ... 中间有其他代码 ...
SpringApplication.run(Application.class, args); // 第二遍!!!
}
SpringApplication.run 写了两遍!
-
第一遍:Tomcat 启动,绑定端口成功
-
第二遍:尝试再次启动,发现端口已被占用,报错
-
第一遍的实例实际上正常运行了,所以应用能访问
三、问题总结
根本原因
-
直接原因:
SpringApplication.run被调用了两次 -
加剧因素:客户端频繁使用短连接,导致大量 TIME_WAIT 堆积
-
干扰因素:IPv6 localhost 连接、端口复用配置导致的误导性日志
为什么这么难排查?
| 现象 | 误导方向 | 真相 |
|---|---|---|
| 端口占用错误 | 以为端口真被占用 | 第二个实例启动失败 |
| 应用能正常访问 | 以为没问题 | 第一个实例正常运行 |
| 大量 TIME_WAIT | 以为网络问题 | 客户端短连接行为 |
| 加了端口复用还报错 | 以为配置无效 | 重复启动才是主因 |
四、解决方案
1. 修复代码
java
public static void main(String[] args) {
SpringApplication.run(Application.class, args); // 只留一行
}
2. 让客户端主动关闭连接(可选)
java
@Component
public class ConnectionCloseFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
((HttpServletResponse) response).setHeader("Connection", "close");
chain.doFilter(request, response);
}
}
3. 优化 Windows 端口配置
cmd
# 扩大临时端口范围 netsh int ipv4 set dynamicport tcp start=10000 num=55000
五、经验教训
-
遇到“不可能”的问题时,检查最简单的代码 - 重复的
run()就是最典型的例子 -
工具输出要全面分析 -
netstat不仅要看 LISTENING,还要看 TIME_WAIT -
区分 IPv4 和 IPv6 - 很多端口问题与 IPv6 有关
-
日志错误不一定代表功能失败 - 有时候错误日志是误导性的
-
保持怀疑精神 - “应用能访问”不等于“启动过程没问题”
六、排查工具清单
| 工具 | 用途 |
|---|---|
netstat -ano |
查看端口状态和连接 |
tasklist / taskkill |
管理进程 |
wmic process |
查看进程详细信息 |
netsh int ipv4 show dynamicport tcp |
查看临时端口范围 |
netsh interface ipv4 show excludedportrange |
查看被系统保留的端口 |
-Djava.net.preferIPv4Stack=true |
强制使用 IPv4 |
七、写在最后
这个问题困扰了我好几天,最终发现是两行代码的重复调用。有时候,技术问题最难的往往不是技术本身,而是我们总是倾向于往复杂的方向思考。
越是奇怪的问题,越要检查最基础的地方。
因为系统既是服务端又是客户端,作为服务端,客户端连上的webservice接口没有自动断开,一直TIME_WAIT;作为客户端,还要频繁请求外部接口,而且还没有连接池;这一去一来,并发量大的时候,临时端口耗尽……导致外部接口根本没有请求出去!
后面改了代码,下次不要再出线上bug啊!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)