TIME_WAIT

TIME_WAIT的发生场景

先从一例线上故障说起。在一次升级线上应用服务之后,我们发现该服务的可用性变得时好时坏,一段时间可以对外提供服务,一段时间又不可以,大家都百思不得其解。运维同学登录到机器上,使用netstat命令查看后才发现,机器上有成千上万处于TIME_WAIT状态的链接。

为什么?

我们这个应用服务器需要通过发起tcp链接对外提供服务(相当于客户端)。每个链接会占用一个本地端口,当在高并发的情况下,TIME_WAIT状态的链接过多,多到把本地端口全耗尽,应用服务对外表现的症状就是不能提供服务了。当过了一段时间后,处于time_wait的一部分链接被回收,释放出本地端口可以被使用,服务又可以对外提供服务了。这样周而复始,便会出现了一会可以,一会不可以。

那么为什么会出现这么多的TIME_WAIT呢?

tcp链接终止时,主机1(主动关闭的一方)会先发出FIN报文,主机2进入CLOSE_WAIT状态,并发送一个ACK应答,同时,主机2调用read调用获得EOF,并将此结果通知应用程序进行主动关闭操作,发送FIN报文。主机1在收到FIN报文后,进入到TIME_WAIT状态。同时发送ACK报文,经过2MSL进入到CLOSED状态。

和大多数BSD派生的系统一样,linux系统里有一个硬编码的字段,名称为TCP_TIMEWAIT_LEN,其值为60s,也就是说,Linux系统停留在TIME_WAIT的状态的固定时间是60s。

注意:只有主动发起关闭的一方会进入time_wait状态。

TIME_WAIT的作用

主要有两个作用:

一、为了确保最后的ack能让被动关闭方接收,从而帮助其正常关闭。

tcp在设计的时候,做了充分的容错性设计,比如,tcp假设报文会出错,重传机制。如果图中的主机1的ACK报文没有传输成功,那么主机2就会重新发送FIN报文。这时候的主机如果没有time_wait的状态,而是直接进入closed状态,它就失去了当前状态的上下文,只能回复一个RST操作,从而导致被动关闭方出现错误。

如果处于time_wait状态,主机1就可以在接收到重传后的FIN报文,重新发出一个ACk报文,使得主机2可以进入正常的CLOSED状态。

二、为了旧连接的数据能正常消失

我们知道,在网络中,经常会发生报文经过一段时间才能到达目的地的情况,产生的原因是多种多样的,如路由器重启,链路突然故障等等,如果迷路报文到达时,发现tcp连接四元祖(源IP,port 目的IP,PORT)所代表的连接不存在了,那么很简单,这个报文会被丢弃。

我们考虑这样一个场景,在原连接中断后,又重新创建出一个原连接的化身(四元组相同),这时候迷路的报文到达了,新连接就接收到了这个脏数据。

所以,tcp就设计出了这么一个机制,在2MSL时间后,在进入CLOSED状态。

重点:2MSL的时间是从主机1接收到FIN后发送ACK开始计时的。

如果在TIME_WAIT时间内,因为主机1的ACK没有传输到主机2,主机1又接收到主机2的FIN报文,那么2MSL的时间会重新计时。

道理很简单:因为2MSL的时间就是为了让旧连接所有的报文都能自然消亡,现在主机1重新发送了ACK报文,自然需要重新计时。

TIME_WAIT的危害

一、内存资源占用,这个目前看来不是太严重

二、对端口资源的占用,一个TCP连接需要至少消耗一个本地端口,端口资源也是有限的,一般可以开启的端口为32768~61000,也可以通过net.ipv4.ip_local_port_range指定,如果TIME_WAIT状态过多,会导致无法创建新连接。

如何优化TIME_WAIT

在高并发的情况下,如何优化time_wait?

net.ipv4.tcp_max_tw_buckets

一个暴力的方法是通过sysctl命令,将系统值调小。这个值默认是18000,当系统中处于time_wait的连接一旦超过这个值,系统就会将所有time_wait连接状态重置,并且只打印出警告信息。这个方法比较暴力,治标不治本,带来的问题远比解决的问题要多,不推荐使用

调低TCP_TIMEWAIT_LEN的值,重新编译系统

这个方法是一个不错的方法,缺点是需要一些内核方面的知识,能够重新编译内核。我想这不是大部分人能接收的方式。

SO_LINGER的设置

英文单词linger的意思为停留,我们可以设置套接字选项,来设置调用close或者shutdown关闭连接时的行为。

int setsockopt(int sockfd, int level, int optname, const void *optval,
                    socklen_t optlen);

struct linger {
int l_onoff; /* 0=off, nonzero=on */
int l_linger; /* linger time, POSIX specifies units as seconds */}

linger参数有几种可能

1.如果l_onoff为0,那么关闭本选项。l_linger的值被忽略,这对应了默认行为,close或者shutdown立即返回。如果在套接字发送缓冲区中有数据存留,系统会尝试就这些数据发送出去。

2.如果l_onoff非0,且l_linger=0,那么调用close后,会立刻发送RST给对端,该TCP连接跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭。这种关闭的方式称为,强制关闭。在这种情况下,缓存区中的数据不会被发送,被动关闭方也不知道对端已经断开。只有当被动关闭方处于recv调用时,接收到RST报文时,会立刻得到一个connet reset by peer 的异常。

struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsockopt(s,SOL_SOCKET,SO_LINGER, &so_linger,sizeof(so_linger));

3.如果l_onoff为非0,且l_linger也非0,那么调用close后,调用close的线程就会被阻塞,直到数据被发送出去,或者设置的l_linger的计时时间到。

第2种可能为跨越TIME_WAIT状态提供了一种可能,不过是一个非常危险的行为,不值得提倡。

net.ipv4.tcp_tw_recycle:

是客户端和服务器端都可以复用,但是容易造成端口接收数据混乱,4.12内核直接砍掉了

net.ipv4.tcp_tw_reuse:更安全的设置

Linux对这个参数的解释为:

Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint. Default value is 0. It should not be changed without advice/request of technical experts.

大意是从协议角度理解是安全可控的,可以复用处于TIME_WAIT的连接。

那么什么是协议角度理解的安全可控呢?主要有两点

1.只适用于连接发起方(c/s模型中的客户端)

2.对应的TIME_WAIT状态的连接创建时间超过1s才可以用。

使用这个选项,还有一个前提,需要打开对TCP时间戳的支持,即net.ipv4.tcp_timestamps=1(默认即为1)

要知道,TCP协议也在与时俱进,RFC1323中实现了TCP扩展规范,以便保证TCP高可用,并引入了新的TCP选项,两个4字节的时间戳字段,用于记录TCP发送方的当前时间戳和从对端接收到的最新时间戳。由于引入了时间戳,我们在前面提到的2MSL问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。

 

net.ipv4.tcp_tw_reuse 要慎用,当客户端与服务端主机时间不同步时,客户端的发送的消息会被直接拒绝掉? 

待确认

 

SO_REUSEADDR套接字选项和上面的有什么关系?

其实一点关系也没有。

tcp_tw_reuse是内核选项,主要用在连接的发起方。TIME_WAIT状态的连接创建时间超过1s后,新的连接才可以被复用,注意,这里是连接的发起方;

SO_REUSEADDR是用户态选项,用来告诉操作系统内核,如果端口已经被占用,但是TCP连接状态处于TIME_WAIT,可以重用端口。如果端口忙,而TCP处于其它状态,重用端口时依旧得到Address already in use的错误信息。注意,这里一般都是连接的服务方。

在所有TCP服务器程序中,调用bind之前请设置SO_REUSEADDR套接字选项,这不会产生危害,相反,它会帮助我们在很快时间内重启服务端程序,而这一点恰恰是很多场景所需要的。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值