这几天为开发 DataLoader etl 工具,被 TIME_WAIT 问题和 CLOSE_WAIT 问题给逼疯了。
基本思想是 DataLoader_Server 接收到 etl 请求报文,就 fork 一个子进程进行 etl 处理 。
在实际的测试过程中,发现出现了大量的 TIME_WAIT 和 CLOSE_WAIT ,实在是闹心。
从网络上查了一下,大致上的意思是 tcpip 是一个安全的网络传输协议,为了保证网络传输过程中不会因为一方对网络的关闭导致还在网络链路上的数据出现丢失,因此当发出 close 指令关闭网络套接字后,其网络并不会直接关闭,还需要收到对应放的回执后才会进行关闭。这个中间的等待过程就是 TIME_WAIT 和 CLOSE_WAIT , 但是大量出现长时间的 TIME_WAIT 和 CLOSE_WAIT , 则并不正常。
下面进行原理性的分析:
懒得画了,先盗几张图吧。
这张图完整的诠释了 tcpip 协议中网络传输的握手过程。从这张图上,我们可以清晰的看到,实际上一个完整的 tcpip 协议的数据传输过程,是有四次握手的,具体说明如下:
我们知道,由于 TCP 协议的全双工工作模式,一个 socket 的关闭需要四次挥手来完成:
主动关闭的一方调用 close(),底层协议栈发送 FIN 包,
被动关闭的一方收到 FIN 包后,底层协议栈自动回复 ACK,进入 CLOSE_WAIT 状态,之后回调给业务层,等待其“后续操作”;而主动关闭的一方在收到 ACK 后会进入 FIN_WAIT_2 状态,继续的等待接收对端的 FIN 包;
被动关闭的一方在完成业务层“后续操作”后(所有数据发送完毕),调用 close() 关闭本端 socket;此时,底层协议栈发出 FIN 并等待 ACK,同时进入进入 LAST_ACK 状态;
主动关闭的一方收到 FIN 包,底层协议栈自动回复 ACK;之后,主动关闭连接的一方进入 TIME_WAIT 状态;而被动关闭的一方进入 CLOSED 状态
进入 TIME_WAIT 状态的 socket 需要等待 2MSL 时间后,才会进入 CLOSED 状态
上面描述的是“常规关闭”,还存在一种并不常见的“同时关闭”,会在连接两侧同时出现 TIME_WAIT 。
从上面的分析来看,TIME_WAIT 和 CLOSE_WAIT 是必不可少而且是必须的。但是我们必须看到由于不正确的编码,也可能导致大量的 TIME_WAIT 和 CLOSE_WAIT 事件出现,这就要求我们在开发的时候进行精确控制,避免出现非正常的 TIME_WAIT 和 CLOSE_WAIT 。
从本次 DataLoader 开发的实战来看,主要有两点需要注意:
1、作为一个网络服务器程序,一般而言,都会采用父子进程的方式来进行实现,这就需要我们充分考虑到在父进程和子进程中,完成数据传输过程后,都需要进行网络套接字的关闭。一般而言,我们的程序会按照如下的思路编写:
-
... ...
-
-
socketfd = socket( ... ... );
-
bind(socketfd, ... ... );
-
-
listen(socketfd,...);
-
-
while(1)
-
{
-
connfd = accept(socketfd, ... ... );
-
n = read(connfd, ... ...);
-
if ( n == 0 )
-
{
-
break;
-
}
-
if ( (pid = fork()) > 0 )
-
{ /* 父进程 */
-
... ...
-
} else if ( pid == 0 )
-
{ /* 子进程 */
-
... ...
-
} else /* 创建子进程失败 */
-
break;
-
return SECUSSES ;
-
}
注意:在这段代码中间,我们有两个关键点:
一个是在父进程中,我们需要关闭 connfd , 否则当客户端断开连接后,父进程还是处于连接状态,这会产生大量的 CLOSE_WAIT 。
另一个是在子进程中,当我们进行业务处理时,我们同样需要关闭 connfd ,否则当客户端断开连接的时候,我们的子进程由于完整的复制了父进程的环境,此时在子进程中这个connfd 依然是打开的。这同样会产生大量的 CLOSE_TIME 。
2、作为网络服务器程序而言,我们不应主动寻求关闭网络套接字,而应该是等待客户端关闭网络,这个时候我们需要检查从 read 和 recv 中读取到的字节流,当读取到的字节流为 0 时,可以让进程睡眠一定时间后,再发出 close(connfd) 的指令。
3、可以在服务端使用如下语句来设置网络环境:
-
int opt = 1;
-
setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
这其中:SO_REUSEADDR 参数的含义是告诉网络环境,当发起 close(connfd) 的指令后,立即回收网络端口,从而提高系统的高并发能力。
以上这段代码,需要插入到 socket 函数和 bind 函数之间,这是因为当套接字还没有建立的时候,对于套接字的网络环境设置是没有意义的,而为什么不在 bind 函数之后,则是因为当发起了 bind 指令后,网络环境已经被固定,不可修改了。
4、可以在服务器中修改操作系统的网络环境参数,减少 TIME_WAIT 的等待时间。具体来说,针对 Linux 操作系统而言,如下参数会对 tcpip 的网络协议产生影响。
-
# vi /etc/sysctl.conf
-
=========================================
-
-
net.ipv4.tcp_syncookies = 1
-
-
// 表示开启SYN cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
-
-
net.ipv4.tcp_tw_reuse = 1
-
-
//表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
-
-
net.ipv4.tcp_tw_recycle = 1
-
-
//表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
-
-
net.ipv4.tcp_fin_timeout = 30
-
-
//修改系統默认的 TIMEOUT 时间
以上内容,仅作记录,参考内容如下:
参考内容:
阅读(174) | 评论(0) | 转发(0) |