我的Linux服务器应用程序侦听端口8000
,并使用close()
正确关闭其所有文件描述符(FD)。
尽管如此,我有时会观察到多达3000个CLOSE_WAIT
TCP连接:
# netstat -antp | grep CLOSE_WAIT
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 149 0 127.0.0.1:8000 127.0.0.1:49630 CLOSE_WAIT -
tcp 236 0 127.0.0.1:8000 127.0.0.1:48440 CLOSE_WAIT -
tcp 251 0 127.0.0.1:8000 127.0.0.1:41748 CLOSE_WAIT -
tcp 149 0 127.0.0.1:8000 127.0.0.1:46064 CLOSE_WAIT -
tcp 251 0 127.0.0.1:8000 127.0.0.1:56654 CLOSE_WAIT -
tcp 251 0 127.0.0.1:8000 127.0.0.1:37502 CLOSE_WAIT -
tcp 251 0 127.0.0.1:8000 127.0.0.1:56976 CLOSE_WAIT -
tcp 251 0 127.0.0.1:8000 127.0.0.1:36416 CLOSE_WAIT -
... ~3000 more of these ...
字符串
(netstat
作为root
运行,因此没有丢失数据。
我知道CLOSE_WAIT
会发生在服务器应用程序没有close()
连接到套接字的FD时。这在RFC793的TCP状态图中有解释(更好的渲染,例如here),也在例如https://blog.cloudflare.com/this-is-strictly-a-violation-of-the-tcp-specification/中有解释。
但是我知道我的服务器正确地执行了close()
,因为服务器进程上的ls -1 "/proc/$(pidof myserver)/fd" | wc -l
只显示了90个打开的FD,而不是3000个。
正确关闭的进一步证据是上面显示的netstat -p
没有列出与端口相关的程序(参见CLOSE_WAIT -
)。
一些其他未解决的案例的集合,其中CLOSE_WAIT -
显示没有相关的过程:
- 如何删除CLOSE_WAIT套接字连接
- https://forum.huawei.com/enterprise/en/nfs-lock-services-are-affected-due-to-firewall-shielding-of-some-ports/thread/667243322445545472-667213878863474688的
- https://serverfault.com/questions/311009/netstat-shows-a-listening-port-with-no-pid-but-lsof-does-not/847910#847910
- https://unix.stackexchange.com/questions/10106/orphaned-connections-in-close-wait-state的
- 在这里,
CLOSE_WAIT
s和-
作为进程看起来像是来自客户端。
所以问题是:
CLOSE_WAIT
状态怎么会比开放套接字FD多这么多?
为什么Linux在x1m15 n1x和x1m16 n1x的输出上自相矛盾,如果x1m18 n1x必须有一个未关闭的套接字(FD)与之关联,那么x1m17 n1x怎么可能发生呢?
2条答案
按热度按时间2eafrhcq1#
在许多情况下都可以观察到这一点,但如果没有服务器实现的细节,就不可能有效地回答这个问题。
首先,让我们澄清一下,您不能期望在/proc/$PID/fd中看到与您的进程没有关联的连接的文件句柄(在输出的最后一列中没有您的进程ID表示这一点),因此根本不存在不一致性,并且您可能会对这些套接字在某个时候根据所使用的端口号与应用程序相关联这一事实感到困惑,但显然不是当前运行的进程,或者至少不是
accept()
,如下面案例1的示例所示:字符串
其次,如果您的应用程序执行
close()
,则在CLOSE_WAIT中不应看到套接字,但您的应用程序可能没有这样做,这是正确的,因为:1.这些连接在您的应用程序能够处理它们之前就被卡住了,因此在您的应用程序有机会这样做之前,套接字就被客户端关闭了。
1.由于涉及到多个线程、进程甚至可能是信号处理程序,fd可能已经被改变,因此除非检查来自close()的返回状态,否则它可能根本就不有效,如果您的应用程序依赖于操作系统关闭
exit()
处的所有文件句柄,甚至可能不为SIGCHLD提供默认信号处理程序,这可能导致进程被保留为丧尸第二种情况的一个有趣的例子(原始的),但是它们是相关的,因为共享的套接字如下所示:
型
在这里,客户端得到了一个没有响应的服务器,并被挂起,直到很久以后,当没有响应的服务器最终被收割,让它连接到一个套接字没有pid,直到最终移动它的连接到另一个服务器进程“正式”。
最后,工作fd和CLOSE_WAIT中fd的数量之间的极端不平衡意味着应用程序中存在严重问题,接收队列中的高数值可能表明服务器没有响应,因此这可能是客户端集体关闭连接的副作用,并通过重试使问题变得更糟。
确保进程之间没有共享套接字,并且没有任何东西会阻塞侦听器套接字(例如:一些垃圾收集),这可能会有所帮助。如果这只是一个容量问题,那么缩短侦听队列并水平扩展可能是最好的选择。
e0bqpujr2#
我想明白了:
当在Linux内核的
listen()
backlog队列中等待的客户端在用户空间应用程序accept()
打开它之前断开连接时,会出现无关联进程的CLOSE_WAIT
状态。这很容易用
netcat
重现,见下文。关于这一问题的现有评论摘要:
CLOSE_WAIT
是由服务器忘记调用close()
而创建的。/proc/<pid>/fd
显示不知何故窃听。使用
nc
复制简短的复制摘要(阅读下面的解释):
字符串
终端1(“服务器”):
型
这将创建一个套接字(并在其上调用
listen(..., 1)
,队列长度为backlog
,并调用accept()
以等待连接。(Can使用
strace
进行验证。)旁白:
accept()
ed* 的连接,但我在这里将其称为listen()
队列,因为它的大小由listen()
决定,它的生存时间由listen()
返回的套接字决定。6.1.51
)实际上将真实的队列大小设置为backlog + 1
,所以队列实际上有2个插槽。我还没有研究为什么会这样,但它在这里被提到,我已经通过实验验证了它:对于listen(, ...)
,3个客户端可以连接到上面的netcat服务器(1个accept()
艾德,2个在队列中),只有第4个客户端在没有连接的情况下会挂起。backlog
大小可以在下面的ss -tlpn 'sport = :1234'
输出中观察到Send-Q
字段。终端2(“客户端A”):
型
此连接使
accept()
返回服务器。终端3(“客户端B”):
型
此连接填充了服务器
listen()
队列中的一个空槽。它不是accept()
ed。现在,按
Ctrl+C
取消此nc
。这将从问题创建CLOSE_WAIT
状态。观察
ss
输出如果我们在另一个终端上观看
sudo watch -n1 --exec ss -tapn 'sport = :1234'
的同时运行上面的repro,我们可以观察上面每个步骤之后的状态:型
在最后一步中,我们已经可以观察到
Process
连接为空,这是因为连接确实建立了--但只与服务器内核建立了连接,而不是服务器进程,因为进程还没有accept()
连接。内核为我们执行TCP SYN-ACK-ACK握手,因此在
accept()
发生之前,在内核中建立了TCP连接。现在,在客户端B断开连接后,我们看到没有进程的
CLOSE-WAIT
:型
而且
Recv-Q
仍然是1
,所以断开的连接仍然在内核队列中!观察
netstat
输出netstat
:sudo watch -n1 'netstat -antpe | grep 1234'
我们看到了更多的行,因为
netstat
不能做ss
提供的方便的'sport = :1234'
过滤,所以我们也看到了客户端套接字:型
同样,这里我们将第一个查找的
-
作为ESTABLISHED
连接的PID/Program name
。这也回答了这个问题:
并且在客户端B断开之后:
型
这就是我们的
CLOSE_WAIT
和-
工艺。使用Python实现更小的repro
由于
nc
可能会随着时间的推移而改变它的具体功能(例如,它进行的系统调用),这里有一个类似的Python TCP服务器,可以让你更清楚地了解服务器上发生了什么:型
无进程
CLOSE_WAIT
为什么存在,如何清除内核会将断开的
CLOSE_WAIT
连接保留在listen()
定义的队列中,而不使用关联的进程,直到出现以下情况之一:close()
s由listen()
返回的套接字,或者accept()
是已经断开的连接。接受已经断开的连接将成功:
accept()
将向服务器发送FD。这将无进程的CLOSE_WAIT
转换为CLOSE_WAIT
with 进程。服务器现在可以在FD上调用close()
以关闭连接并解析CLOSE_WAIT
状态。在
listen()
返回的套接字上调用close()
会拆除整个内核队列,因此CLOSE_WAIT
会立即消失。摘要
CLOSE_WAIT
s是进入listen()
队列的连接,在我们的进程accept()
s之前,它们被另一端发送TCPFIN
* 终止,然后 * 被从listen()
队列中取出。accept()
尚未发生。CLOSE_WAIT
可以比文件描述符多。问题中有3000个这样的无进程
CLOSE_WAIT
的问题表明服务器没有accept()
连接。这可能是由于错误,或者因为服务器进程正在忙碌其他事情(例如垃圾收集或运行其他一些函数)。因此队列填满。服务器必须调用listen(, 3000)
或更高版本。实际上,我可以看到ss -tlpn 'sport = :8000'
中的Send-Q
是4096
。当排队的客户端由于超时而最终给予时,排队的ESTABLISHED
连接变成排队的CLOSE_WAIT
连接。因此,解决多达3000个
CLOSE_WAIT
连接的问题的下一步应该是找出服务器停止调用accept()
的原因。