🚪 CLOSE_WAIT状态过多:忘记"关门"的代码Bug

57 阅读3分钟

知识点编号:010
难度等级:⭐⭐⭐(必掌握)
面试频率:🔥🔥🔥🔥🔥


🎯 一句话总结

CLOSE_WAIT过多 = 你的代码忘记调用socket.close()了!💀


🤔 什么是CLOSE_WAIT?

四次挥手中的状态:

客户端                  服务器
  | FIN                  |
  |---------------------->|
  |                  CLOSE_WAIT ⚠️
  | ACK                  |
  |<----------------------|
  |                       |
  |(服务器应该close())    |
  |                       |
  | FIN                  |
  |<----------------------|
CLOSED                 LAST_ACK

CLOSE_WAIT:
- 被动关闭方
- 收到FIN,发送ACK后进入
- 等待应用程序调用close()
- 如果不close(),一直停留!

⚠️ CLOSE_WAIT过多的原因

根本原因:代码没close()!

// ❌ 错误代码1:忘记关闭
Socket socket = new Socket("localhost", 8080);
InputStream in = socket.getInputStream();
// ... 使用socket ...
// 忘记socket.close()了!

客户端关闭 → 服务器收到FIN
服务器发送ACK → 进入CLOSE_WAIT
但是代码没有close() → 一直CLOSE_WAIT!

// ❌ 错误代码2:异常时没关闭
Socket socket = new Socket("localhost", 8080);
socket.getInputStream().read(); // 可能抛异常
socket.close(); // 异常时不执行!

// ❌ 错误代码3:HTTP客户端没关闭响应
CloseableHttpResponse response = httpClient.execute(request);
// 使用response...
// 忘记response.close()了!

💻 正确的代码写法

方案1:try-with-resources(最推荐)

// ✅ 自动关闭
try (Socket socket = new Socket("localhost", 8080);
     InputStream in = socket.getInputStream()) {
    
    // 使用socket
    byte[] buffer = new byte[1024];
    int len = in.read(buffer);
    
} // 自动调用close(),即使异常也会关闭

方案2:finally块

// ✅ 手动在finally中关闭
Socket socket = null;
try {
    socket = new Socket("localhost", 8080);
    // 使用socket...
} finally {
    if (socket != null) {
        try {
            socket.close();
        } catch (IOException e) {
            // 记录日志
        }
    }
}

方案3:Apache HttpClient正确用法

// ✅ 正确关闭响应
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
    HttpGet request = new HttpGet("http://example.com");
    
    try (CloseableHttpResponse response = httpClient.execute(request)) {
        HttpEntity entity = response.getEntity();
        String result = EntityUtils.toString(entity);
        // 使用result...
    } // response自动关闭
} // httpClient自动关闭

🔍 排查CLOSE_WAIT

1. 查看CLOSE_WAIT数量

# Linux
netstat -an | grep CLOSE_WAIT | wc -l

# 或
ss -ant | grep CLOSE-WAIT | wc -l

# 查看具体连接
netstat -anp | grep CLOSE_WAIT
# 输出:
# tcp  0  0  192.168.1.1:8080  192.168.1.2:50000  CLOSE_WAIT  12345/java

# 12345是进程PID
# /java是进程名

2. 定位问题代码

# 查看进程打开的文件描述符
lsof -p 12345 | grep TCP

# 使用jstack查看线程堆栈
jstack 12345 > thread.dump

# 分析thread.dump,找到:
# - 哪些线程持有Socket
# - 为什么没有关闭

3. 代码审查

搜索所有new Socket的地方:
grep -r "new Socket" .

检查每个地方是否:
1. 使用了try-with-resources
2. 或在finally中close()
3. 或使用连接池管理

🐛 常见面试题

Q1:CLOSE_WAIT过多是什么原因?怎么解决?

答案:

根本原因:
应用程序没有正确关闭socket

常见场景:
1. 忘记调用socket.close()
2. 异常处理不当,close()没执行
3. HTTP客户端忘记关闭响应
4. 连接池没有归还连接

排查步骤:
1. netstat -an | grep CLOSE_WAIT | wc -l
   查看数量

2. netstat -anp | grep CLOSE_WAIT
   找到进程PID

3. lsof -p PID
   查看打开的文件描述符

4. jstack PID
   查看线程堆栈

5. 代码审查
   检查所有socket相关代码

解决方案:
1. ✅ 使用try-with-resources(Java 7+)
2. ✅ 在finally中关闭
3. ✅ 使用连接池
4. ✅ 定期代码审查

预防措施:
- 代码规范:强制使用try-with-resources
- 静态代码分析:FindBugs、SonarQube
- Code Review:检查资源泄漏

Q2:TIME_WAIT和CLOSE_WAIT的区别?

答案:

TIME_WAIT:
- 主动关闭方
- 四次挥手最后阶段
- 持续2MSL后自动消失
- 正常现象
- 过多 → 架构问题(短连接太多)

CLOSE_WAIT:
- 被动关闭方
- 收到FIN后进入
- 需要应用程序close()
- 不会自动消失
- 过多 → 代码问题(忘记close)

记忆方法:
TIME_WAIT:有时间限制,会自动消失
CLOSE_WAIT:等待关闭,需要代码处理

解决方法:
TIME_WAIT → 用长连接、连接池
CLOSE_WAIT → 修复代码,正确关闭

🎓 总结

CLOSE_WAIT的关键点:

  1. 原因:代码没调用close()
  2. 位置:被动关闭方
  3. 排查:netstat、lsof、jstack
  4. 解决:try-with-resources或finally
  5. 预防:代码规范、静态分析、Code Review

记忆口诀

CLOSE_WAIT不会消 ⚠️
代码忘了调close 🚫
try-with-resources ✅
finally也能行 🛡️
代码规范很重要 📜

一句话记住

CLOSE_WAIT = 你的Bug!
赶紧检查代码,加上close()!

文档创建时间:2025-10-31
作者:AI助手 🤖