网络编程:TIME_WAITE

一、TIME_WAIT

1、TIME_WAIT发生的场景
故障:一次升级线上应用服务后,发现该服务的可用性时好时坏,一段时间可以对外提供服务,一段时间突然又不可以了。使用netstat命令发现主机有成千上万处于TIME_WAIT状态的连接。
为啥?该应用服务需要通过发起TCP连接对外提供服务。每个连接会占用一个本地端口,当在高并发的情况下,TIME_WAIT状态的连接过多,多到本机可用的端口耗尽,应用服务对外表现的症状,就是不能正常工作,但当TIME_WAIT的连接被系统收回并关闭,就有可用了
TCP连接的四次挥手:

TCP 连接终止时,主机 1 先发送 FIN 报文,主机 2 进入 CLOSE_WAIT 状态,并发送一个 ACK 应答,同时,主机 2 通过 read 调用获得 EOF,并将此结果通知应用程序进行主动关闭操作,发送 FIN 报文。主机 1 在接收到 FIN 报文后发送 ACK 应答,此时主机 1 进入 TIME_WAIT 状态。主机 1 在 TIME_WAIT 停留持续时间是固定的,是最长分节生命期 MSL(maximum segment lifetime)的两倍,一般称之为 2MSL。和大多数 BSD 派生的系统一样,Linux 系统里有一个硬编码的字段,名称为TCP_TIMEWAIT_LEN,其值为 60 秒。也就是说,Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds  */

注:只有发起连接终止的一方会进入TIME_WAIT状态
2、TIME_WAIT的作用
为什么不直接进入 CLOSED 状态,而要停留在 TIME_WAIT 这个状态?
两个方面:
1)首先这样做是为了确保最后的ACK能让被动关闭方接收,从而帮助其正常关闭
2)为了让旧连接的重复分节在网络中自然消失。
考虑这样一个场景,在原连接中断后,又重新创建了一个原连接的“化身”,说是化身其实是因为这个连接和原先的连接四元组完全相同,如果迷失报文经过一段时间也到达,那么这个报文会被误认为是连接“化身”的一个 TCP 分节,这样就会对 TCP 通信产生影响。
所以,TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的分组都被丢弃,使得原来连接的分组在网络中都自然消失,再出现的分组一定都是新化身所产生的。
重点:2MSL 的时间是从主机 1 接收到 FIN 后发送 ACK 开始计时的;如果在 TIME_WAIT 时间内,因为主机 1 的 ACK 没有传输到主机 2,主机 1 又接收到了主机 2 重发的 FIN 报文,那么 2MSL 时间将重新计时。道理很简单,因为 2MSL 的时间,目的是为了让旧连接的所有报文都能自然消亡,现在主机 1 重新发送了 ACK 报文,自然需要重新计时,以便防止这个 ACK 报文对新可能的连接化身造成干扰。

3、TIME_WAIT的危害
主要危害有两种:
1)第一是内存资源占用
2)第二是对端口资源的占用,一个TCP连接至少消耗一个本地端口,端口资源又是有限的,一般可以开启的端口为32768~61000 ,也可以通过net.ipv4.ip_local_port_range指定,如果 TIME_WAIT 状态过多,会导致无法创建新连接。

4、如何优化TIME_WAIT?
net.ipv4.tcp_max_tw_buckets
暴力的方法:通过syctl命令,将系统值调小,这个值默认为18000,当系统中处于TIME_WAIT的连接一旦超过这个值时,系统就会将所有的TIME_WAIT连接状态重置,并且只打印警告信息,但该方法治标不治本,带来的问题远多于解决的问题,不推荐使用

降低TCP_TIMEWAIT_LEN,重新编译系统
方法不错,但缺点是需要内核方面的知识,能够重新编译内核。

SO_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参数的几种可能:

  • 如果l_onoff为 0,那么关闭本选项。l_linger的值被忽略,这对应了默认行为,close 或 shutdown 立即返回。如果在套接字发送缓冲区中有数据残留,系统会将试着把这些数据发送出去。
  • 如果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));
  • 如果l_onoff为非 0, 且l_linger的值也非 0,那么调用 close 后,调用 close 的线程就将阻塞,直到数据被发送出去,或者设置的l_linger计时时间到。

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

net.ipv4.tcp_tw_reuse:更安全的设置
Linux 系统对于net.ipv4.tcp_tw_reuse的解释如下:

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才可以被复用

小结:

TIME_WAIT 的引入是为了让 TCP 报文得以自然消失,同时为了让被动关闭方能够正常关闭;
不要试图使用SO_LINGER设置套接字选项,跳过 TIME_WAIT;
现代 Linux 系统引入了更安全可控的方案,可以帮助我们尽可能地复用 TIME_WAIT 状态的连接。

posted @ 2022-03-08 23:50  牛犁heart  阅读(192)  评论(0编辑  收藏  举报