6

大量CLOSE_WAIT排查

 2 years ago
source link: https://keys961.github.io/2022/07/17/CLOSE_WAIT%E6%8E%92%E6%9F%A5/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

客户端大量连接打入服务端后,客户端马上关闭,然后再建立连接时,没有收到服务端任何的相应,最后报错超时。

服务端会有一个判断,当连接数量过多时(accept()后的连接数),会主动关闭新accept()的连接。

此时客户端发现连接被主动挂断,会主动退出程序。

2. 观察到的状态

2.1. netstat

首先第一个就是查连接状态,发现了大量的CLOSE_WAIT连接。

# netstat -natp | grep 8080 | grep CLOSE_WAIT 
tcp 46 0 127.0.0.1:8080 127.0.0.1:51004 CLOSE_WAIT - 
tcp 46 0 127.0.0.1:8080 127.0.0.1:51000 CLOSE_WAIT - 
tcp 46 0 127.0.0.1:8080 127.0.0.1:50990 CLOSE_WAIT - 
tcp 46 0 127.0.0.1:8080 127.0.0.1:50998 CLOSE_WAIT - 
tcp 46 0 127.0.0.1:8080 127.0.0.1:50996 CLOSE_WAIT - 
tcp 46 0 127.0.0.1:8080 127.0.0.1:50994 CLOSE_WAIT - 
tcp 46 0 127.0.0.1:8080 127.0.0.1:50992 CLOSE_WAIT - 
tcp 46 0 127.0.0.1:8080 127.0.0.1:51002 CLOSE_WAIT - 

熟悉四次握手的话,可以知道:

  1. 主动端发送一个FIN,从ESTABLISH -> FIN_WAIT_1,被动端收到后从ESTABLISH -> CLOSE_WAIT

  2. 被动端发送一个ACK,主动端收到后从FIN_WAIT_1 -> FIN_WAIT_2

  3. 被动端再发送一个FIN(调用一次close()),从CLOSE_WAIT -> LAST_ACK,主动端从FIN_WAIT_2 -> TIME_WAIT

  4. 主动端发送一个ACK,被动端收到后关闭,主动端等待2倍MSL后关闭

可知,被动端第3步没做,即遗留了连接没有调用close(),那出现在哪一端?

2.2. lsof

通过lsof查看进程打开的文件描述符,发现服务端没有CLOSE_WAIT的连接。

server 88324 root 3u a_inode 0,12 0 12326 [eventpoll] 
server 88324 root 4u a_inode 0,12 0 12326 [eventfd] 
server 88324 root 6u IPv4 14939805 0t0 TCP localhost:8080 (LISTEN) 
... Other ESTABLISHED connections 

难道CLOSE_WAIT连接出现在客户端?但客户端已经挂了,不太可能啊,事实上是这样吗?

2.3. ss

通过ss,查看一下accept队列的情况:

# ss -ntlip '( sport == 8080 )' 

State Recv-Q Send-Q Local Address:Port Peer Address:Port Process 
LISTEN 24 511 127.0.0.1:8080 0.0.0.0:* users:(("server",pid=88324,fd=6)) 
cubic cwnd:10 unacked:8 

发现accept队列里有8个连接没处理,数量和CLOSE_WAIT连接的数量相同,这就很奇怪了。

这里就有推测,已有的连接在accept队列里,但客户端马上退出,而这些连接一直没有被服务端accept()

2.4. tcpdump抓包

这里对CLOSE_WAIT的连接抓包。

# 连接建立 
02:10:37.258319 IP localhost.51046 > localhost.8080: Flags [S], seq 68230391, win 65495, options [mss95,sackOK,TS val 2035158581 ecr 0,nop,wscale 7], length 0 
02:10:37.258330 IP localhost.8080 > localhost.51046: Flags [S.], seq 629648772, ack 68230392, win 65483, options [mss 65495,sackOK,TS val 2035158581 ecr 2035158581,nop,wscale 7], length 0 
02:10:37.258338 IP localhost.51046 > localhost.8080: Flags [.], ack 1, win 512, options [nop,nop,TS val 2035158581 ecr 2035158581], length 0 
# 应该是客户端直接意外退出 
02:10:46.733172 IP localhost.51046 > localhost.8080: Flags [F.], seq 1, ack 1, win 512, options [nop,nop,TS val 2035168055 ecr 2035158581], length 0 
02:10:46.733352 IP localhost.8080 > localhost.51046: Flags [.], ack 2, win 512, options [nop,nop,TS val 2035168056 ecr 2035168055], length 0 

可以发现,CLOSE_WAIT的连接,在客户端退出前,就已经被建立了,并且是由客户端主动发起的。

所以,可以推测,连接的泄露发生在服务端,且泄露的连接保存在了accept队列里,没有被处理。(注意:accept队列保存的是已经建立三次握手的连接)

2.5. gdb查看进程

可以看到,进程一直卡在epoll_wait里没出来,留着accept队列没有处理,符合2.4.的推测。

2.6. 主动再建立连接

这里再主动建立连接,此时有1个已经建立的连接。

这里的配置是,服务端只允许建立1个连接。测试前,已经建立了1个连接。然后客户端测试程序开始大量连接向服务端建立。

通过netstat, ss等工具,可以看到:

  • CLOSE_WAIT的连接变少了1个

  • accept队列的数量没少

并且通过打日志,看到已有的1个最早的CLOSE_WAIT连接被accept(),并且马上被close()

然后退出新建立的连接,可以看到CLOSE_WAIT的连接增加了1个,accept队列数量没变,就是刚刚建立的连接。

至此,原因大致清楚了。

从上面的探测,可以知道:

  • 连接的泄露发生在服务端

  • CLOSE_WAIT连接留在了accept队列里,没被处理

再查看代码,大概是这样的,且listen_fd也通过ET注册在epoll中:

void accept_new_conn() {
  while(true) {
     // ...
     auto fd = accept(...); 
     if(fd != -1) {
        if(conn_count >= limit) {
           close(fd);
           return; // <--- Bug
        }        
        // handle new connection
     } else {
        if(errno == EWOULDAGAIN || errno == EAGAIN) {
            return; // retry
        }
        // handle error...
     }
  }
}

当连接超过数量,主动关闭的时候,就直接返回了,导致遗留已经ESTABLISHED的连接在accept队列没处理。

此时客户端退出,由于服务端使用ET,就不会被唤醒再处理accept队列的连接,而此时里面的连接变成了CLOSE_WAIT,出现了2.1.的现象。

而新连接来的时候,程序被唤醒,旧的CLOSE_WAITaccept()后又被close(),此时conn_count >= limit依旧成立,又直接返回了,遗留的accept队列的连接又没被处理。所以新连接就卡住了。

4. 解决方案

  1. listen_fd注册epoll时,从ET变成LT,从而accept队列非空时,依旧能被唤醒,处理里面的连接。

  2. 将上面的return换成continue,从而能够继续处理accept队列里的连接。

还是得熟悉epoll编程,特别是ET的处理。

此外TCP连接关闭的流程也得回顾回顾。


0 comments

Be the first person to leave a comment!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK