服务器大量CLOSE_WAIT,应用代码与内核参数双向排查
服务器大量 CLOSE_WAIT,本质是应用收到了对端的 FIN 后,没有主动调用 close() 关闭自己这一侧的连接。
内核参数只能辅助缩短等待时间,真正要解决的是应用代码里的连接释放逻辑。
下面按“先定位、再调参、后验证”的顺序,讲一套可直接落地的双向排查流程。
先用一条命令确认 CLOSE_WAIT 规模
登录服务器后,别急着改配置,先把连接状态统计出来。
用 ss 命令比 netstat 更快也更直接:
ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn
输出中如果 CLOSE_WAIT 数量持续上升且大量堆积,基本可以判断是服务端应用程序的问题。
接着要看这些连接对应哪个进程:
ss -antp | grep CLOSE_WAIT | head -20
输出里的 users:(("java",pid=12345,fd=88)) 就是进程名、PID 和文件描述符。
记下 PID,再用 lsof -p 12345 | grep TCP 查看具体建立的连接,确认是访问数据库、调用第三方接口还是客户端长连接。
内核参数只能缓解,不能根治
很多人第一反应是调小 tcp_fin_timeout,但它只管 FIN_WAIT_2,对 CLOSE_WAIT 的影响很有限。
真正值得看的是这几个参数:
sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_probes
如果业务允许,可以适当缩短 tcp_keepalive_time,让内核更早探测死连接。
比如把默认 7200 秒改成 1800 秒:
sysctl -w net.ipv4.tcp_keepalive_time=1800
但请先确认应用的连接不会因此被误杀。
如果是短连接业务,改参数收益很小,重点还是回到代码。
重点排查应用代码中的连接释放
确认进程后,去代码里找对应 socket 的创建位置。
常见原因有三类:
- 未关闭输入输出流:只关了
InputStream,OutputStream或底层 socket 没关。 - 异常路径未释放:
try-catch里抛出异常后,close()被跳过。 - 线程池中的连接滞留:任务执行完没有归还或关闭连接,导致 fd 越积越多。
以 Java 为例,正确写法应该放在 finally 里:
Socket socket = null;
try {
socket = new Socket(host, port);
// 业务处理
} finally {
if (socket != null) {
try { socket.close(); } catch (IOException ignored) {}
}
}
Python 可以使用 contextlib.closing 或 with 语句;
Node.js 则要确保 socket.destroy() 被调用。
改完代码后重新发布,观察连接变化。
避坑:别把超时调成“假释放”
这里最容易犯的错是盲目修改内核参数,
比如把 tcp_fin_timeout 调成 30 秒,
表面看 CLOSE_WAIT 少了,
其实是连接被内核提前回收,
可能导致正在传输的数据丢失。
另一个坑是只靠重启应用来恢复,重启后 CLOSE_WAIT 确实清零,但流量上来后会再次堆积。
正确做法是保留现场,用 ss -antp 抓取堆叠连接对应的进程堆栈,再用 jstack 或 strace 确认卡在哪里。
改完后如何验证效果
代码修复和参数调整后,至少观察以下两点:
watch -n 1 'ss -ant | awk "{print $1}" | sort | uniq -c'
一是 CLOSE_WAIT 数量不再持续上升,二是稳定后能回落到正常区间(几百到几千都算正常,关键看走势)。
如果仍然堆积,继续抓 PID 和连接目标地址,排查是否有外部调用没设超时,比如 HTTP 客户端没配 readTimeout。
建议把这套排查步骤写成脚本,定期执行并记录基线。
等下次再出现 CLOSE_WAIT 告警时,直接对比历史数据,能更快定位是代码变更还是依赖服务异常。
如果你正在处理服务器大量 CLOSE_WAIT,建议先按本文完成统计和进程定位,再决定是否需要改代码或调内核参数。
多数情况下,问题出在应用代码,内核参数只是辅助手段,别本末倒置。