一个出现异常CLOSE_WAIT连接的问题

项目测试过程中出现大量CLOSE_WAIT状态的连接,后来发现是给进程设置的open files数目太少导致。下面还原这个问题。

复现

从网上找了个使用了epoll的 web server,拉取下来后,执行以下命令进行编译:

cd MiniHttpd
cmake .
make

项目用到了zlib和libconfig,如果编译报错,Ubuntu下可以尝试执行以下命令安装:

sudo apt-get install zlib1g-dev
sudo apt-get install libconfig libconfig++-dev libconfig-dev

修改HttpServer.cpp文件:

--- a/src/HttpServer.cpp
+++ b/src/HttpServer.cpp
@@ -77,7 +77,7 @@ void HttpServer::load_config(string path){
     }
     catch(FileIOException io_exception){
         cout<<"Config not found, use default settings"<<endl;
-        port = 0;
+        port = 9999;
         baseURL = "/home/wuyuixn/Webroot";
         request_queue_length = 5;
     }
@@ -138,7 +138,9 @@ void HttpServer::start_listen(){
    while(1){
        int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
        for (int i = 0; i < number; i++){
            int sockfd = events[i].data.fd;

            //处理新到的客户连接
            if (sockfd == server_sock){
                int client_sock = -1;
                struct sockaddr_in client_name;
                socklen_t  client_name_len = sizeof(client_name);
                 client_sock = accept(server_sock,
                         (struct sockaddr *)&client_name,
                         &client_name_len);
+                printf("accept %d\n", client_sock);
                 if (client_sock == -1){
+                    fprintf(stderr, "errno is %d (%s)\n", errno, strerror(errno));
                     Log::log("accept failed",ERROR);
                     continue;
                 }

重新make,执行./Minihttpd,用ls -p pid看一下一共打开了6个文件描述符:

Minihttpd 242544 vbox    0u      CHR  136,4      0t0       7 /dev/pts/4
Minihttpd 242544 vbox    1u      CHR  136,4      0t0       7 /dev/pts/4
Minihttpd 242544 vbox    2u      CHR  136,4      0t0       7 /dev/pts/4
Minihttpd 242544 vbox    3u     IPv4 656642      0t0     TCP *:9999 (LISTEN)
Minihttpd 242544 vbox    4u  a_inode   0,14        0   10277 [eventpoll]
Minihttpd 242544 vbox    5w      REG  253,0      464 1048929 2023-12-03.log

新建一个脚本,内容是ulimit -n 6;./Minihttpd(如果直接在shell里执行,会把当前shell的最大open files设为6),执行该脚本,另起一个终端执行nc 127.0.0.1 9999,就会发现脚本输出:

accept -1
errno is 24 (Too many open files)

但用ss -tp | grep 9999发现已经建立了连接:

ESTAB   0     0    127.0.0.1:9999     127.0.0.1:39320
ESTAB   0     0    127.0.0.1:39320    127.0.0.1:9999     users:(("nc",pid=249546,fd=3))

调用accept时,协议栈已经完成了握手建立了连接,accept只是创建一个文件描述符并和这个连接关联起来。
关掉nc再看:

CLOSE-WAIT   1        0      127.0.0.1:9999           127.0.0.1:39320
FIN-WAIT-2   0        0      127.0.0.1:39320          127.0.0.1:9999

nc进程退出时关闭了socket,发送了FIN并收到了ACK,等待对端发送FIN,所以处于FIN-WAIT-2状态。
Minihttpd进程没有文件描述符关联这个连接,所以没办法调用close,就不会发送FIN,所以会一直处于CLOSE-WAIT状态,直到进程退出。

过了一会,FIN-WAIT-2超时后就只剩下了CLOSE-WAIT了:

CLOSE-WAIT   1        0      127.0.0.1:9999           127.0.0.1:39320

实际上拿来测试的这个Minihttpd本身就有bug,对端主动关闭时,它不会调用close,所以慢慢会出现很多CLOSE-WAIT的连接。

延伸阅读:【tcp】关于 TCP FIN_WAIT2状态的一个细节问题

更进一步

accept因文件描述符不足失败(以下简称accept失败)后,连接难道就“资源泄露”了吗?其实不是,此时连接并没有从completely established sockets waiting to be accepted队列(以下简称为队列)中拿掉。如果accept失败后,释放了一些文件描述符,再次调用accept时就能取出之前取失败的连接了。

epoll分别在水平触发和边沿触发时情况有点不同:
水平触发时,accept失败后,如果不做额外处理,下次epoll_wait会立即触发读事件,accept继续失败……,然后会陷入死循环。这个比较难处理。
边沿触发时,accept失败后,又有新连接建立时,才会触发读事件。如果此时文件描述符足够,accept成功后只处理了1个连接,队列里还有别的连接没处理呢,所以应该循环调用accept,直到返回EAGAIN或EWOULDLOCK错误;如果此时文件描述符仍不足,队列中的连接会越来越多,队列满了之后就会拒绝新连接的建立,再之后就没机会触发读事件了(测试在后面),相当于死锁了。

测试:边沿触发,将listen的第2个参数设为0(测试发现队列大小实际是backlog+1),客户端发起两个连接,第2次的客户端请求,epoll_wait并不会返回,观察发现:

ESTAB      0    0    127.0.0.1:9999   127.0.0.1:36294
SYN-SENT   0    1    127.0.0.1:33070  127.0.0.1:9999     users:(("telnet",pid=6361,fd=3))
ESTAB      0    0    127.0.0.1:36294  127.0.0.1:9999     users:(("nc",pid=6150,fd=3))

第2个连接的握手都无法完成了,一定时间后telnet提示telnet: Unable to connect to remote host: Connection timed out

你可能感兴趣的:(基础,tcp)