项目测试过程中出现大量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
。