简单分析一起主机Socket连接数过高的问题
2022-10-14 13:53:43 阿炯

host131.17上有半关闭连接数过高的报警,经查有大量的CLOSE_WAIT状态连接;如何在不重启进程的情况来释放这些过多的问题连接?

登录主机先查看一下:

[root@host131.17 tmp]# ss -s
Total: 75432
TCP:   75261 (estab 19, closed 50051, orphaned 0, timewait 7)
Transport Total     IP        IPv6
RAW       0         0         0       
UDP       8         7         1       
TCP       25210     14        25196   
INET      25218     21        25197   
FRAG      0         0         0

[root@host131.17 tmp]# netstat -nat |grep CLOSE_WAIT > t10
[root@host131.17 tmp]# perl -anlE 'say $F[4]' /tmp/t10 > /tmp/t11
[root@host131.17 tmp]# perl -F':' -alne '$s{$F[0]}++;' -e 'END { foreach (keys %s){chomp($_);print "$_:$s{$_}"} }' /tmp/t11

使用Perl统计IP产生的CLOSE_WAIT状态的数量情况。

关于CLOSE_WAIT

CLOSE_WAIT表示服务器已收到来自客户端的第一个FIN信号,并且连接正在关闭过程中。连接服务器端的CLOSE_WAIT意味着服务器已从客户端收到一个FIN,将向客户端确认这一点,然后通知应用程序它可以关闭连接;然后当应用程序确信所有数据都已从连接中读取后,就可以放弃连接。一旦放弃连接,服务器将向客户端发送最终的FIN,连接将完全关闭。

CLOSE_WAIT的意思与它的字面意思差不多——内核正在等待本地进程关闭其文件描述符,然后再删除该条目。TCP连接已被完全断开,客户端可能会觉得连接是已经完成了的,但服务端仍在坚持。唯一需要注意的是,很多CLOSE_WAIT条目都会消耗内核内存和文件描述符表条目,尤其是有大量这些条目。如果正在查看的条目是暂时的,那么可能只是正在通过大量TCP连接进行循环,并且在连接关闭和进程开始关闭文件描述符之间的一小段时间内看到了其中的一小部分。另一方面,如果它们是永久性的(端口和IP地址不会随时间变化),那么说明(服务)应用中正在泄漏描述符,需要进行修复,以便在结束时总是关闭这些文件句柄。

CLOSE_WAIT状态表示另一端发送FIN段以关闭连接。可以认为是半双工的模式,允许这一端刷新任何缓冲区,向这一端发送最后一位数据,请求在关闭这一端的连接之前关闭连接。如果有大量连接停留在CLOSE_WAIT中,这意味着负责的进程在套接字进入CLOSE_WAIT后不会关闭套接字。可以使用tcpdump或其他网络流量捕获工具来查看数据包。


如果处理操作受限,还可以从其它方面进行调整:
1.通过ulimits指令或/proc中的相关选项进行文件描述符的最大数量限制(系统范围)
2.缩短TCP等待时间(net.ipv4.tcp_keepalive_),net.ipv4.tcp_keepalive_time

这可能是没有关闭服务器上运行的应用程序中的某个资源(文件句柄、网络连接)所导致。CLOSE_WAIT表示相关的程序仍在运行,尚未关闭套接字(内核正在等待它这样做)。CLOSE_WAIT表示连接的本地端已从另一端收到FIN,但操作系统正在等待本地端的程序实际关闭其连接。问题是在本地计算机上运行的程序没有关闭套接字,这不是TCP优化问题。当程序保持连接打开时,连接可以(并且非常正确)永远保持在CLOSE_WAIT中。一旦本地程序关闭套接字,操作系统就可以将FIN发送到远程端,在等待FIN确认的同时,远程端会将其状态转换到LAST_ACK。一旦收到,连接就完成了,并从连接表中删除(如果您端处于CLOSE_WAIT状态,则不会以TIME_WAIT状态结束)。

在linux内核>=4.9上,可以使用iproute2中的ss命令,其支持带'-K'的选项
ss -K dst client1.freeoa.net dport = 4987

内核必须在启用CONFIG_INET_DIAG_DESTROY选项的情况下编译。

ss --tcp state CLOSE-WAIT '(dport = 22 or dst 1.1.1.1)' --kill
ss --tcp state CLOSE-WAIT '(dst srcip)' --kill
ss --tcp state CLOSE-WAIT dst srcip --kill
ss -K dst srcip sport = 5050

可惜CentOS7上自带的ss版本不支持这个kill操作。。。


看看通过连接的socket的fd,从而在/proc/PID/fd/目录下
[root@host131.17 tmp]# ls -l /proc/29498/fd/|head -19
total 0
lr-x------. 1 freeoa freeoa 64 Sep 30 22:26 0 -> /dev/null
l-wx------. 1 freeoa freeoa 64 Sep 30 22:26 1 -> /apps/micro-service/tmp/kkFileView.log
lr-x------. 1 freeoa freeoa 64 Sep 30 22:26 10 -> /dev/random
lr-x------. 1 freeoa freeoa 64 Sep 30 22:26 100 -> /apps/micro-service/tmp/kkFile/rBKMhWLqJq2ETfliAAAAAI1hUFk078.pdf (deleted)
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 1000 -> socket:[239535956]
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 10000 -> socket:[245115534]
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 10001 -> socket:[245116375]
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 10002 -> socket:[245115448]
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 10003 -> socket:[245115562]
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 10004 -> socket:[245115591]
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 10005 -> socket:[245116327]
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 10006 -> socket:[245115608]
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 10007 -> socket:[245120096]
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 10008 -> socket:[245115613]
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 10009 -> socket:[245115846]
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 1001 -> socket:[239535979]
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 10010 -> socket:[245116397]
lrwx------. 1 freeoa freeoa 64 Sep 30 22:26 10011 -> socket:[245120226]

删除对应的链接文件看是否能处理掉这些CLOSE_WAIT状态的连接。结果是不能删除,即使是root用户。还是先尝试升级一下iproute2,以使其中的ss支持'kill'选项。

[root@host131.17 iproute2-5.12.0]# yum install bison bison-devel flex flex-devel

还是报错了
[root@host131.17 iproute2-5.12.0]# ss -n --tcp state CLOSE-WAIT dst srcip sport = :5050 -K
SOCK_DESTROY answers: Invalid argument

看来还的另想办法。

另外一台host131.73主机也报了连接数过高的异常:各种状态连接计数:半关闭连接(closed)数为:48835。

host131.73的情况与host131.17上还不一样。后者上是由外部的请求访问导致的,而前者最大的可能是其上的应用创建了Socket,并没有对这些Socket进行使用(且没有能主动回收累积造成)。从ss 指令和(/proc/net/sockstat)文件中均能看到此情况。即存在socket fd泄漏,可以用lsof命令检查系统sock的文件句柄(lsof | grep sock)。

# ss -s
Total: 50837 (kernel 51423)
TCP:   50661 (estab 152, closed 48897, orphaned 0, synrecv 0, timewait 1/0), ports 0
Transport Total     IP        IPv6
*         51423     -         -       
RAW       0         0         0       
UDP       9         7         2       
TCP       1764      3         1761    
INET      1773      10        1763    
FRAG      0         0         0

[root@host131.73 fd]# ls -l|tail -19
lrwx------. 1 freeoa freeoa 64 Oct  8 10:16 9980 -> socket:[1448519476]
lrwx------. 1 freeoa freeoa 64 Oct  8 10:16 9981 -> socket:[1433999943]
lrwx------. 1 freeoa freeoa 64 Oct  8 10:16 9982 -> socket:[1434280254]
lrwx------. 1 freeoa freeoa 64 Oct  8 10:16 9983 -> socket:[1485221306]
lrwx------. 1 freeoa freeoa 64 Oct  8 10:16 9984 -> socket:[1434493384]
lrwx------. 1 freeoa freeoa 64 Oct  8 10:16 9985 -> socket:[1485367700]
lrwx------. 1 freeoa freeoa 64 Oct  8 10:16 9986 -> socket:[1485363394]
lrwx------. 1 freeoa freeoa 64 Oct  8 10:16 9987 -> socket:[1434256594]

# file 9987
9987: broken symbolic link to `socket:[1434256594]'

# ls -l 9987
lrwx------. 1 freeoa freeoa 64 Oct  8 10:16 9987 -> socket:[1434256594]

链接文件在终端中呈现出红色的跳动背景,文件显然已经损坏不存在了,,,

网上有推荐了如下的解决办法(原文如下):
1.Attach with gdb and call close() on the fd. You can map from addr/port to inode number via /proc/net/tcp and from inode number to FD inside the process with ls -la /proc/$pid/fd.
2.Spoof a RST packet. You'll need to generate it locally and guess the SEQ number somehow.
3.Maybe setup an iptables rule to generate a RST on the next packet.
4.Write a kernel module.

目前似乎没有一种得到很好支持的方法来做到这一点;如果进程的FD意外关闭,进程很可能会崩溃。查找到的资料多数建议提高最大允许打开的文件数,但调升此值很可能无法真正解决这种问题,还可能让该问题恶化。


Remove a CLOSE_WAIT socket connection
Manually closing a port from commandline
关闭在运行中的进程的socket比较难但并不是不可能。

方法1:
定位进程号:
netstat -np

将得到一个源/目标ip:port portstate-pid/processname映射

在进程中找到套接字的文件描述符(file descriptor):
lsof -np $pid

会得到一个列表:进程名称、pid、用户、fileDescriptor……一个连接字符串。

找到连接的匹配fileDescriptor编号,它将类似于"97u",意思是"97"。

现在连接至进程(通过进程号):
gdb -p $pid

现关闭socket连接:
call close($fileDescriptor) //does not need ; at end.

比如:
call close(97)

退出gdb:
quit

Socket则已关闭。

方法2:
使用ss关闭侦听的套接字:
ss --kill state listening src :Port_Numer

ss是iproute2软件包的一部分,因此在现代Linux上已经是必备工具了。

方法3:
tcpkill - kill TCP connections on a LAN(dsniff toolkits)

tcpkill kills specified in-progress TCP connections (useful for libnids-based applications which require a full TCP 3-whs for TCB creation).


Socket与其Inode

netstat -alep

可以看到相应socket的i-node编号,再用lsof来对其进行查找
lsof -i | grep "Inode-NO"

在TCP连接表里进行查找
grep -i "Inode-NO" /proc/net/tcp

正常情况下能找到对应的行,如果不能找到,就意味着连接出来了问题,对端放弃了而我端还维持着,,同样也就会发生下面的情况。


关于/proc目录下的sockets文件描述符报断开的符号链接问题
/proc/<pid>/fd sockets with broken symlinks
/proc/PDI/fd sockets with broken symlinks
HowTo handle broken link after file deletion in /proc/<pid>/fd directory

如果进程打开了一个文件,并且该文件被删除,则该文件在/proc/PID/fd中显示为受损断开的链接。如果一个程序打开了很多文件,但没有对它们做任何操作(关闭),这可能表明程序中存在错误。必须联系程序的作者来修复它(如果有源代码也可以自行修复)。无法通过/proc对进程进行操作,可以将调试器(gdb)挂接(attach)到进程并使其关闭文件,但不能保证正常的结果。

chmod(1)手册中说:chmod从不更改符号链接的权限;chmod系统调用也无法更改其权限。这不是问题,因为符号链接的权限从未使用过;但对于命令行中列出的每个符号链接,chmod会更改指向文件的权限。

Linux的/proc文件系统为虚拟的文件层级:它使用一个已知的API将对象表示为文件,proc(5)中的/proc/[pid]/fd/条目记录了许多详细信息,例如:
/proc/[pid]/fd/
[...]
For file descriptors for pipes and sockets, the entries will be symbolic links whose content is the file type with the inode. A readlink(2) call on this file returns a string in the format:
对于管道和套接字的文件描述符,条目将是符号链接,其内容是具有inode的文件类型。对此文件的readlink(2)调用返回以下格式的字符串:

type:[inode]
[...]
Permission to dereference or read (readlink(2)) the symbolic links in this directory is governed by a ptrace access mode PTRACE_MODE_READ_FSCREDS check; see ptrace(2).
取消引用或读取(readlink(2))此目录中符号链接的权限由ptrace访问模式ptrace_mode_read_FSCREDS检查控制;请参阅ptrace(2)。

因此只能检查属于同一用户的进程(如果进程设置为非转储/非ptracable和其他特殊警告,则甚至不能检查)。通常/proc/[pid]/fd/符号链接上显示的访问权限反映了文件描述符的打开方式。
lr-x------
l-wx------
lrwx------

同样,使用pipe(2)创建的(未命名)管道将有一个文件句柄处于读取模式,另一个处于写入模式。套接字是双向的:没有以只读或只写方式“打开”套接字的概念,实际上从来没有对它们进行开放的系统调用。它们将被视为'lrwx-------'以反映它们可以被读或写。


套接字(socket)[xxxxx]符号链接总是断开,无法提供打开具有给定inode号的套接字的路径。符号链接目标的文本不是指文件,而是指/proc/net/tcp表中的一个条目,该条目使用编码过的文本字段描述每个套接字。例如,在某系统上可以看到:
$ ls -l /proc/2672/fd/7
lrwx------ 1 freeoa freeoa 64 Feb 13 15:08 /proc/2672/fd/7 -> socket:[19164451]

对应于tcp表中的这一行:
$ grep 19164451 /proc/net/tcp
 433: 0100007F:C8AA 0100007F:0C8A 01 00000000:00000000 02:00000286 00000000  1000        0 19164451 2 0000000000000000 20 4 1 10 27

可以使用-p选项告诉netstat读取所有/proc/PID/fd目录下链接以了解哪些套接字属于哪些进程,netstat就会这样做:
netstat -naptu
netstat -naptue

proc(5)手册页对主题的描述非常简洁:
/proc/net/tcp

Holds a dump of the TCP socket table. Much of the information is not of use apart from debugging. The "sl" value is the kernel hash slot for the socket, the "local address" is the local address and port number pair. The "remote address" is the remote address and port number pair (if connected). "St" is the internal status of the socket. The "tx_queue" and "rx_queue" are the outgoing and incoming data queue in terms of kernel memory usage. The "tr", "tm->when", and "rexmits" fields hold internal information of the kernel socket state and are only useful  for  debugging. The "uid" field holds the effective UID of the creator of the socket.

保存TCP套接字表的转储。除了调试之外,大部分信息都没有用。"sl"值是套接字的内核哈希槽,"local address"是本地地址和端口号对。"远程地址"是远程地址和端口号对(如果已连接),"St"是套接字的内部状态。就内核内存使用量而言,"tx_queue"和"rx_queuse"是传出和传入数据队列。"tr"、"tm->when"和"rexmits"字段保存内核套接字状态的内部信息,仅对调试有用。"uid"字段保存套接字创建者的有效uid。

tcp-socket出现的"connection reset by peer"和"broken pipe":
1)往一个对端已经close的通道写数据的时候,对方的tcp会收到这个报文,并且反馈一个reset报文。当收到reset报文的时候,继续做读数据的时候就会抛出Connect reset by peer的异常。

2)当第一次往一个对端已经close的通道写数据的时候会和上面的情况一样,会收到reset报文。当再次往这个socket写数据的时候,就会抛出Broken pipe了。根据tcp的约定,当收到reset包的时候,上层必须要做出处理,调用将socket文件描述符进行关闭,其实也意味着pipe会关闭,因此会抛出这个顾名思义的异常。


/proc/[pid]/fd 中socket描述符后面的数字是什么
inode

在命令行用指令'ls -l /proc/[pid]/fd'查看进程文件描述符,会发现socket描述符后面都带着一个数字。
lrwx------. 1 freeoa freeoa 64 Oct  8 10:16 9983 -> socket:[1485221306]

[1485221306]后面数字代表inode。

inode Table专门用于存放文件的描述信息(区别于文件数据),如文件类型(常规、目录、fifo文件、符号链接等)、访问权限、文件大小、创建、最后一次修改、访问时间(可用ls -l命令查询)。每个文件都有一个inode,一个块组(Block Group)中所有inode组成inode表。inode表总共占多少个块,在格式化的时候就要决定并写入块组描述符,由GDT/配置信息管理,mke2fs(格式化工具)的默认策略是一个块组有多少个8KB,就分配多少个inode。

inode与socket关系

上面的数字1485221306代表inode的一个编号,称为inode编号。可以用来唯一标识改该文件,也可以定位inode在磁盘中的位置。在socketfs虚拟文件系统中,socket对应的inode并不像普通的文件系统一样位于磁盘上,而是位于内核的socket结构中(内存)。类似地,socket的inode也可以唯一标识当前socket通信连接。同样遵守操作系统中的文件管理模式。

文件描述符总是从0开始,内核总是为新创建的文件描述符分配尽可能小的非负数。然而进程总是可以自由关闭它拥有的任何文件描述符,因此fd/目录下描述符编号完全可能是断档的、非连续的。

综上,基本就4步找出连接与进程关系

1).通过在netstat中添加-p选项来获取程序的PID(比如$PID)。

2).通过查看local_address和/或rem_address字段来确定/proc/net/tcp文件中的正确行(注意它们是十六进制格式,特别是IP地址是以小端字节顺序表示的),还要确保st是01(对于ESTABLISHED);

3).注意分配的inode字段(比如$inode);

4).在/proc/$pid/fd中的文件描述符中搜索该inode,还可以查询符号链接的文件访问时间:
find /proc/$pid/fd -lname "socket:\[$inode\]" -printf %t


关于TCP的Socket

TCP的Socket就是一个文件流,是非常准确的。因为不仅Socket在Linux中就是以文件的形式存在的。除此之外还存在文件描述符。写入和读出也是通过文件描述符。在内核中的Socket是一个文件,那对应就有文件描述符。

每一个进程都有一个数据结构task struct,里面指向一个文件描述符数组,来列出这个进程打开的所有文件的文件描述符。文件描述符是一个整数,是这个数组的下标。这个数组中的内容是一个指针,指向内核中所有打开的文件的列表。既然是一个文件,就会有一个inode,只不过Socket对应的inode不像真正的文件系统一样,保存在硬盘上的,而是在内存中的。在这个inode中指向了其在内核中的Socket结构。在此结构里面,主要的是两个队列,一个是发送队列,一个是接收队列。在这两个队列里面保存的是一个缓存 sk_buff。这个缓存里面能够看到完整的包的结构和TCP收发包可以对应起来。

数据结构图:




参考文档

Linux-Kernel-Networking-Iproute2
More about socket files in /proc/PID/fd
从Linux内核出发弄懂socket底层的来龙去脉
Linux套接字与虚拟文件系统(1)-初始化和创建
Linux套接字与虚拟文件系统(2)-操作和销毁