一、问题现象

最近项目中遇到一个诡异的问题:应用明明已经成功启动了,但控制台却打印了端口被占用的错误日志;接口有时能正常调用,有时却失败;对方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 启动,绑定端口成功

  • 第二遍:尝试再次启动,发现端口已被占用,报错

  • 第一遍的实例实际上正常运行了,所以应用能访问

三、问题总结

根本原因

  1. 直接原因SpringApplication.run 被调用了两次

  2. 加剧因素:客户端频繁使用短连接,导致大量 TIME_WAIT 堆积

  3. 干扰因素: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

五、经验教训

  1. 遇到“不可能”的问题时,检查最简单的代码 - 重复的 run() 就是最典型的例子

  2. 工具输出要全面分析 - netstat 不仅要看 LISTENING,还要看 TIME_WAIT

  3. 区分 IPv4 和 IPv6 - 很多端口问题与 IPv6 有关

  4. 日志错误不一定代表功能失败 - 有时候错误日志是误导性的

  5. 保持怀疑精神 - “应用能访问”不等于“启动过程没问题”

六、排查工具清单

工具 用途
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啊!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐