
问题:服务端出现大量 CLOSE_WAIT 状态,什么原因?有什么危害?应该怎么办?
CLOSE_WAIT 是什么时候出现的
回答这个问题,首先需要知道 CLOSE_WAIT 状态是什么时候出现的?还是回到经典的四次挥手过程,这个过程得不断看,直到印在脑海里(图解可以看《三次握手与四次挥手》)。
可以看到,CLOSE_WAIT 状态是在主动方(这道题里面就是客户端)发起了第一次挥手,发送 FIN 报文到被动方(这里指服务端)。在被动方收到 FIN 报文并且回复 ACK 后,被动方状态变为了 CLOSE_WAIT(这里埋个问题:是收到 FIN 之后就变为 CLOSE_WAIT,还是回复 ACK 之后才变为?从 Linux 源码看,内核在收包流程 tcp_data_queue() 里处理按序到达的 FIN 时,就会调用 tcp_fin() 把状态直接置为 TCP_CLOSE_WAIT,而回复 ACK 只是同一个收包流程里紧随其后的动作。也就是说,状态切换发生在处理 FIN 报文的那一刻,并不是等 ACK 发出去之后才切,这两件事是在同一个报文处理流程里先后完成的)。
在被动方发送自己的 FIN 后,CLOSE_WAIT 状态才变为了 LAST_ACK。顺带一提,处于 CLOSE_WAIT 的被动方并非「半死不活」:它依然能正常收报文、回 ACK,细节可以看《CLOSE_WAIT 状态还能收到报文吗》。
现在服务端出现了大量 CLOSE_WAIT 状态的 TCP 连接,那么从 TCP 层面看就是:没有发送 FIN 报文,也就是没有调用 close() 函数。
所以如果服务端出现大量的 CLOSE_WAIT 状态,问题就出在服务端,要排查服务端的代码。为什么没有调用 close() 函数,就和每个程序的业务相关了,可能导致出现这个状况的原因有:
- 服务端线程在某个地方阻塞了,没法调用
close()。 - 调用
close()前有大量的耗时操作,导致短时间内处于 CLOSE_WAIT 状态的连接数增加。 - 没有正确地监听并处理连接关闭的事件,忽略了应答客户端的关闭请求。
用命令确认
在 Linux 系统中,可以使用 netstat 或 ss 命令来查询处于 CLOSE_WAIT 状态的 TCP 连接数量。
netstat -n | awk '/^tcp/ && /CLOSE_WAIT/ {print $5}' | cut -d: -f1 | sort | uniq -c
这条命令会做以下几件事:
- 使用
netstat -n列出所有连接,-n表示不进行域名解析,以便快速处理。 awk先筛出 tcp 开头的行,再筛选出状态为 CLOSE_WAIT 的行。cut用来从第 5 列(对端的「IP:端口」)中提取 IP 部分。sort和uniq -c用来计数每个 IP 有多少个 CLOSE_WAIT 状态的连接。
ss -t state close-wait | wc -l
这将会输出处于 CLOSE_WAIT 状态的连接总数。注意两个细节:ss 的状态名是 close-wait(带连字符);输出第一行是表头,所以连接总数是 wc -l 的结果减 1,也可以改用 ss -tH state close-wait | wc -l 直接去掉表头。
大量 CLOSE_WAIT 的危害
- 资源泄漏:每个 CLOSE_WAIT 状态的连接都会占用一个文件描述符(file descriptor, fd)。系统资源有限,大量 CLOSE_WAIT 连接会迅速消耗可用的文件描述符,可能导致新的连接无法建立,出现服务拒绝的问题。
- 性能下降:随着 CLOSE_WAIT 状态的连接积累,系统需要维护这些连接的状态,消耗内存和 CPU 资源,从而影响整体服务性能。
- 稳定性风险:在极端情况下,资源耗尽可能导致服务崩溃或重启,影响服务的稳定性和可靠性。
应该怎么办
- 检查被动方的代码,找到没有调用
close()函数的原因。 - 在业务层增加自己的心跳检查机制,如果在连续几个心跳收不到后,对这条连接进行
close()处理。 - 记录这条连接上次收到消息的时间,如果大于一定时间没有收到下一条则进行
close()处理。 - TCP 自带的 Keepalive,可以修改时间缩短检测时间(关于 Keepalive 的细节可以看《TCPKeepalive》)。