以前在面试中,有人问过我,你知道TCP关闭时的TIME_WAIT状态吗?知道TCP三次握手四次挥手的我,当然说知道。但其实随着问题的深入,我才发现我并不懂TIME_WAIT

三次握手



四次挥手



目的

为什么四次挥手的过程中,主机1收到FIN后,不直接进入CLOSED状态,而是进入到TIME_WAIT状态,等待2MSL时间后才进入到CLOSED状态。

主要有以下两个目的:

  1. 考虑这样一个场景,主机1连接关闭后,还有报文因为各种原因,到达主机1,如果这时候发现TCP连接四元组(源IP,源端口,目的IP,目的端口)代表的连接已经不存在,那么很简单,这个报文被丢弃掉。
    但存在这样的可能性,之前的连接关闭后,马上又在主机1主机2之间,创建了一个拥有相同TCP连接四元组的连接,此时上一个连接的报文延迟到达主机1,会被新的连接认为是属于自己的报文,造成了干扰。



    所以主机1被要求停留在TIME_WAIT状态2MSL时间,经过2MSL这个时间,足以保证原来旧连接的报文不会存在在网络中,再出现的报文一定是新的连接的。

    Linux配置的2MSL时间是60秒。

    #define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy     TIME-WAIT state, about 60 seconds     */
  2. 另一个主要的目的是,确保对端已经正常关闭连接。当主机2发送FIN报文后,进入到LAST_ACK状态,等待主机1的ACK报文后,才进入到CLOSED状态。

    假设主机1正常接收到主机2FIN报文,然后发送ACK报文后,直接进入到CLOSED状态,而不是停留在TIME_WAIT状态。如果此时的ACK报文丢失,主机2一直没有收到ACK报文,那么主机2就会重发FIN报文。

    但此时主机1已经进入CLOSED状态,连接已经完全关闭了,整个连接的上下文信息都已经失去,主机1无法识别到收到的FIN报文,只能回复一个RST报文,从而导致主机2出现错误。

    还有一种可能出现的情况是,当主机1最后发送的ACK报文丢失后,还是进入到CLOSED状态,此时主机2仍然处在LAST_ACK状态,认为之前的连接是有效的。如果此时主机1使用相同的TCP连接四元组,重新建立与主机2的连接,当主机2收到三次握手的SYN报文时,会回复一个RST报文。(因为主机2还存在LAST_ACK状态,SYN报文不是该状态下,期望收到的报文类型)



TIME_WAIT过多造成的问题

因为连接要保持在TIME_WAIT状态长达1分钟,在一台处理很多连接的服务器上,可能会出现大量保持在TIME_WAIT的连接,从而造成一些问题。

对连接表(Connection Table)的占用

系统使用连接表维护着每个连接。在连接表中,相同的TCP四元组(源IP,源Port,目的IP,目的Port)代表的连接只能有唯一一个。

一个常见的场景

我们的网络服务(Web Server)通常部署在负载均衡器(Load Balancer)后面,不直接面向用户。



对于Load BalancerWeb Server的连接,目的IP和目的Port是固定的 (Web Server的IP和绑定的Port), 源IP也是固定的(Load Balancer的IP),源Port的数量也由net.ipv4.ip_local_port_range决定,一般是30000。这意味着在Load BalancerWeb Server,只能建立30000个连接。

Web Server数据库的连接也存在上面的问题。

对端口的占用

大多数TCP实现(如伯克利版)强加了更为严格的限制。在2MSL等待期间,套接字中使用的本地端口在默认情况下不能再被使用。

客户执行主动关闭并进入TIME_WAIT是正常的。服务器通常执行被动关闭,不会进入 TIME_WAIT状态。这暗示如果我们终止一个客户程序,并立即重新启动这个客户程序,则这个新客户程序将不能重用相同的本地端口。这不会带来什么问题,因为客户使用本地端口,而并不关心这个端口号是什么。 然而,对于服务器,情况就有所不同,因为服务器使用熟知端口。如果我们终止一个已经建立连接的服务器程序,并试图立即重新启动这个服务器程序,服务器程序将不能把它的这个熟知端口赋值给它的端点,因为那个端口是处于2MSL连接的一 部分 。 在重新启动服务器程序前,它需要在1 ~ 4分钟。 - 《TCP/IP 卷1》

如果服务器程序主动关闭,处于TIME_WAIT状态时,使用相同的端口,重启服务器程序,程序会抛出can't bind local address: Address already in use

内存和CPU占用

一个保持在TIME_WAIT状态的连接,此时连接的套接字已经被关闭,连接占用的内存很小,过多的TIME_WAIT不会带来过多的内存占用,所以可以基本忽略内存的问题。

CPU占用发生在寻找一个可用的Port时,此时会加锁,然后循环去寻找一个空闲的端口,但系统的使用的数据结构和优化,使得这部分的开销也比较小。

解决方法

阅读了上面,我们也知道了,TIME_WAIT过多,造成的问题主要是对连接表的占用,那么如何解决呢?

主要解决方法:

  • 关闭socket lingering
  • net.ipv4.tcp_tw_reuse

关闭Socket lingering

正常情况下,当连接的其中一方调用close()时,系统会将套接字的发送缓冲区里的数据发送出去,然后最终进入到TIME_WAIT状态。

可以选择修改套接字的linger

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

来改变这种行为。

  • l_onoff = 0时,l_linger的值被忽略,这时候采用的是默认行为。close()调用立即返回,如果套接字的发送缓冲区里有数据,后台会将数据发送出去。
  • l_onoff = 1时,l_linger=0时,调用close()后,会跳过四次挥手,不会发送FIN报文,而是发送RST报文给对端,这时候直接进入CLOSED状态,也就不会有TIME_WAIT状态了。发送缓冲区里的数据也会被直接丢弃掉。当被动关闭方正阻塞在recv()调用上时,接收到RST报文时,会立刻得到一个“connet reset by peer”的异常。
  • l_onoff = 1时,l_linger非0时,调用close()后,如果发送缓冲区里还有数据,此时close()会阻塞住,等待数据被发送出去并得到对端的确认,或者设置的l_linger超时时间到。如果在l_linger超时时间前,数据发送成功,会进入正常的关闭流程,最后会进入TIME_WAIT状态。否则,超时时间到,会直接丢弃掉还没发送的数据,跳过TIME_WAIT状态,直接进入到CLOSED状态。

关闭socket lingering,跳过TIME_WAIT,直接进入CLOSED状态,为解决TIME_WAIT过多,提供了一种方法,但这不是一种适用于任何情况的解决方法,斟酌使用。

net.ipv4.tcp_tw_reuse

Ubuntu在/proc/sys/net/ipv4/tcp_tw_reuse文件开启。

回到最开始的讨论,我们为什么需要在TIME_WAIT状态停留2MSL时间?

目的1是为了防止使用了相同TCP四元组的新连接接收到旧连接的报文,造成干扰。

TCP option增加了两个4 byte的时间戳字段(需要打开对TCP时间戳的支持,net.ipv4.tcp_timestamps = 1)。两个时间戳字段分别用来记录发送方的当前时间戳和从对端接收到的最后的时间戳。



还是就上面的图来说明下。启用了net.ipv4.tcp_tw_reuse后,发起连接的一方,可以复用连接创建时间超过1秒的,处在TIME_WAIT状态的连接。就像上图所示,复用TIME_WAIT连接,发起三次握手, 成功后,正常发送数据。因为报文里加入了时间戳, 上图中旧连接发送的SEQ=3报文的时间戳比新连接创建时的时间戳小,可以判断出是旧连接的报文,直接丢弃掉。

目的2是为了确保被关闭的一方,不会因为ACK报文的丢失,一直处在LAST_ACK状态。



被关闭的一方,在没收到ACK报文的情况下,会一直重复FIN报文,直到:

  1. 主动放弃,关闭连接
  2. 终于收到ACK报文,关闭连接
  3. 收到RST报文,关闭连接

如果一个使用相同TCP四元组的连接被创建,因为现在有了时间戳,处在LAST_ACK状态的被关闭的一方,对于新连接的SYN报文,不会像上图一样回复RST报文。因为通过SYN报文的时间戳和自己关闭时的时间戳对比,被关闭的一方可以知道,旧的连接已经关闭了,但是ACK报文丢失了,现在来的是新的连接的SYN报文。



被关闭的一方,这时候会重发FIN报文,发起连接方会回复RST报文(因为此时处在SYN_SENT状态),这样被关闭的一方就可以退出LAST_ACK状态,正常地关闭了。

发起连接方,因为刚开始的SYN报文没有得到SYN+ACK报文的响应,会重发一次,这时候就进入到正常的三次握手过程了。

总结

其实还可以通过,增加可使用的TCP四元组,连接双方使用更多的IP和Port,这样可以支持的连接就更多,来解决问题。

The TIME_WAIT state is our friend and is there to help us (i.e., to let old duplicate segments expire in the network). Instead of trying to avoid the state, we should understand it. - W. Richard Stevens 《Unix Network Programming》

参考资料

Coping with the TCP TIME-WAIT state on busy Linux servers

10 | TIME_WAIT:隐藏在细节下的魔鬼

《TCP/IP 卷1》

Last modification:April 8th, 2020 at 09:43 pm
如果觉得我的文章对你有用,请尽情赞赏 🐶