以前在面试中,有人问过我,你知道TCP关闭时的TIME_WAIT
状态吗?知道TCP三次握手
和四次挥手
的我,当然说知道。但其实随着问题的深入,我才发现我并不懂TIME_WAIT
。
三次握手
四次挥手
目的
为什么四次挥手的过程中,主机1
收到FIN
后,不直接进入CLOSED
状态,而是进入到TIME_WAIT
状态,等待2MSL
时间后才进入到CLOSED
状态。
主要有以下两个目的:
考虑这样一个场景,
主机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
发送FIN
报文后,进入到LAST_ACK
状态,等待主机1的ACK
报文后,才进入到CLOSED
状态。假设
主机1
正常接收到主机2
的FIN
报文,然后发送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 Balancer
到Web Server
的连接,目的IP和目的Port是固定的 (Web Server
的IP和绑定的Port), 源IP也是固定的(Load Balancer
的IP),源Port的数量也由net.ipv4.ip_local_port_range
决定,一般是30000。这意味着在Load Balancer
到Web 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
报文,直到:
- 主动放弃,关闭连接
- 终于收到
ACK
报文,关闭连接 - 收到
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
《TCP/IP 卷1》