Unix/Linux编程:如何解决TIME_WAIT太多

TIME_WAIT发生的场景

  • 现象:服务的可用性时好时坏,一段时间可以对外提供服务,一段时间又不可以。
  • 查询:通过netstat命令查看,发生主机上有成千上万处于TIME-WAIT状态的连接
  • 分析:当前这个服务需要荣光发起TCP连接对外提供服务。每个连接会占用一个本地接口,当在高并发的情况下,TIME_WAIT状态的连接过多,多到把本机可用的端口耗尽,应用服务对外表现的症状,就是不能正常工作了。当过了一段时间之后,处于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(maximun segment lifetime)的两倍,一般称之为2MSL。和大多数BSD派生的系统一样,Linux系统里有一个硬编码的字段,名称为TCP_TIMEWAIT_LEN,其值为60秒。也就是说,Linux系统停留在TIME_WAIT的时间为固定的60s
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-        WAIT state, about 60 seconds	*/
  • 过来这个时间后,主机1就会进入CLOSE状态。

注意:只有发起连接终止的一方会进入TIME_WAIT状态

为什么不直接进入 CLOSED 状态,而要停留在 TIME_WAIT 这个状态?(TIME_WAIT 的作用?)

(1)首先,这样做是为了确保最后的ACK能让被动关闭方接收,从而帮助其正常关闭

  • TCP在设计的时候,做了充分的容错性设计。比如,TCP假设报文会出错,需要重传。在这里,如果图中主机1的ACK报文没有传输成功,那么主机2就会重新发送FIN报文
  • 如果主机1没有维护TIME_WAIT状态,而直接进入CLOSE状态,它就失去了当前状态的上下文,只能回复一个RST操作,从而导致被动关闭方出现错误
  • 现在主机1知道自己出于TIME_WAIT的状态,就可以在接收到FIN报文之后,重新发出一个ACK报文,使得主机2可以进入正常的CLOSE状态。

(2)其次,和连接“化身”和报文迷走有关系,为了让旧连接的重复分节在网络中自然消失

  • 我们知道,在网络中,经常会发生报文经过一段时间才能到达目的地的情况,产生的原因是多种多样的,比如路由器重启、链路突然出现故障等。如果迷失报文到达时,发现TCP连接四元组(源 IP,源端口,目的 IP,目的端口)所代表的连接不复存在,那么很简单,这个报文自然丢弃
  • 我们考虑这样一个场景,在原连接中断后,又重新创建了一个原连接的“化身”,,说是化身其实是因为这个连接和原先的连接四元组完全相同,如果迷失报文经过一段时间也到达,那么这个报文会被误认为是连接“化身”的一个 TCP 分节,这样就会对 TCP 通信产生影响。
    在这里插入图片描述
  • 所以,TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的分组都被丢弃,使得原来连接的分组在网络中都自然消失,再出现的分组一定都是新化身所产生的。

注意,2MLS的时间是从主机1收到FIN后发送ACK开始计时的;如果TIME_WAIT时间内,因为主机1的ACK没有传输到主机2,主机1又开始接收到了主机2重发的FIN报文,那么2MSL时间将重新计时。道理很简单,因为2MSL的时间,是为了让旧连接的所有报文能自然消亡,现在主机1重新发送了ACK报文,自然需要重新计时,以防止这个ACK报文对新可能的连接化身造成干扰

既然如此,为什么还要解决TIME_WAIT呢?

过多的TIME_WAIT的主要危害有两种:

  • 对内存资源的占用。这个目前看来不太严重,基本可以忽略
  • 对端口资源的占用。一个TCP连接至少消耗一个本地端口,要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000 ,也可以通过net.ipv4.ip_local_port_range指定,如果TIME_WAIT状态过多,会导致无法创建新连接

那怎么优化过多的TIME_WAIT呢?

(1)net.ipv4.tcp_max_tw_buckets

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

(2)调低TCP_TIMEWAIT_LEN,重新编译系统

  • 这个方法是一个不错的方法,缺点是需要“一点”内核方面的知识,能够重新编译内核。
  • 太过麻烦,不方便使用

(3)SO_LINGER的设置

  • linger可以翻译为停留。我们可以通过1设置套接字选项,来设置调用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”的异常。
    • 如果l_onoff为非 0, 且l_linger的值也非 0,那么调用 close 后,调用 close 的线程就将阻塞,直到数据被发送出去,或者设置的l_linger计时时间到。
  • 第二种可能为跨越 TIME_WAIT 状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。

(3)net.ipv4.tcp_tw_reuse:更安全的设置

  • 那么 Linux 有没有提供更安全的选择呢?当然有。这就是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的套接字为新的连接所用
  • 那么什么是协议角度理解的安全可控呢?主要有两点:
    • 只适用于连接发起方(C/S 模型中的客户端);
    • 对应的TIME_WAIT状态的连接创建时间超过1s才可以被复用
  • 使用这个选项,还有一个前提,需要打开对TCP时间戳的支持,即net.ipv4.tcp_timestamps=1(默认即为 1)。
  • 要知道,TCP协议也在与时俱进,RFC1323中实现了TCP扩展规范,以便保证TCP的高可用,并引入了新的TCP选项,两个4字节的时间戳字段,用于记录TCP发送方的当前时间戳和从对端接收到的最新时间戳。由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。

总结

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

Guess you like

Origin blog.csdn.net/zhizhengguan/article/details/121733208