这两天用python写了一个TCP的短连接压力测试脚本,模拟100路并发不间断的访问。脚本运行几分钟后开始全面报错,错误代码为10048或者99,使用netstat命令查看网络发现大量的连接处于TIME_WAIT状态。
仔细阅读了《unix网络编程》相关章节,弄清楚了问题发生的原因,并找到了解决办法。
问题原因:
TCP连接的Close默认行为是将socket标记为关闭然后立即返回。此时socket描述符不能继续被进程使用:作为read和write的参数。但是socket依然向对端发生已经缓存在队列中的数据,当所有数据都发生完毕后,socket进入TCP的四次包交换关闭时序。主动调用Close的一端在收到对端发来的FIN之后进入TIME_WAIT状态,并持续2MSL的时间。MSL在不同的TCP的实现中是不同的,一般是30秒到2分钟之间,所以2MSL为1~4分钟。socket再进入closed状态之前端口是一直被占用的,因此在TCP短连接大规模并发时很快便耗尽了可用的端口,因此就出现了10048或者99的错误。
解决办法:
通过SO_LINGER Socket选项改变Close的行为。
首先来了解下SO_LINGER socket选项:
该选项决定socket的close行为,默认情况下,close立即返回,但是如果socket的发送缓冲区中仍然有数据时,系统将继续会发送这些数据到对端。
SO_LINGER socket选项让我们可以改变close的默认行为。该选项要求如下数据结构传递到内核,它的定义在<sys/socket.h>中。
struct linger
{
int l_onoff; /* 0==off, nonzero=on*/
int l_linger; /* linger time, POSIX specifies units as seconds */
};
根据l_onoff和l_linger的不同的取值,有如下三种场景:
1. 如果l_onoff等于0,该选项关闭,l_linger的值忽略,close为默认行为。
2. 如果l_onoff非0时,l_linger也等于0,当调用close时,TCP立即断开连接。详细过程是TCP丢弃所有仍然留在发送缓冲区的数据,向对端发送一个RST, 不进入普通的四次包交换时序。该设置socket避免了CLOSE_WAIT状态。
3. 如果l_onoff非0,l_linger大于0,当socket被close时,系统内核将延迟。即,当socket的发送缓冲区中任然有未发送的数据时,进程进入睡眠直到以下几种情况发生:
i)所有的数据已经发送且收到TCP对端发来的ack, 或者ii) linger超时。
从上述的三种场景来看,很明显2符合避免CLOSE_WAIT状态的要求,是解决该问题的关键。python的实现代码如下:
optval = struct.pack("ii",1,0)
tcp_client.setsockopt(SOL_SOCKET, SO_LINGER, optval )