在谈time_wait状态前,我们来看下Tcp的四次挥手吧:
简单解释一下这张网图吧:
- 客户端传完数据,想拔*无情,此时就会发送TCP首部
FIN
标志位置为1
的报文,跟服务端说我完事了。之后客户端进入了FIN_WAIT_1
状态。 - 此时服务端收到报文后,发送确认报文
ACK
,表示老娘知道你一滴都没有了,那我也就准备收拾收拾结束了。进入了CLOSE_WAIT
状态。 表示等着进程调用close 函数关闭连接。 - 客户端收到了
ACK
之后,状态就从FIN_WAIT1
变成了FIN_WAIT2
。现在客户端的发送通道就关了。但是此时还可以接收数据。 - 当服务端进入
CLOSE_WAIT
之后,他还会继续处理数据,等到等到进程的 read 函数返回 0 后,就会调用closLAST_ACKe
函数,开始发送FIN
报文,进入LAST_ACK
状态 - 客户端收到FIN报文后,会回复
ACK
报文给服务端,然后终于到了TIME_WAIT
状态(等死我了)。在 Linux 系统下大约等待 1 分钟后,TIME_WAIT 状态的连接才会彻底关闭。 - 当服务端收到最后的ACK之后,两边就关啦。(光灯睡觉)
这时候你会发现一件事情:主动关闭连接的,才有 TIME_WAIT 状态。
是的,没错,你真棒!
为什么要有TIME_WAIT呢?
TIME_WAIT
是主动方四次挥手的最后一个状态,也是最常遇见的状态。
- 保证连接正确关闭
TIME-WAIT 一个作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。
明明客户端发完最后的ACK了,我客户端凭啥还要等啊?哦吼,这不就暴露问题了吗?客户端发了ACK,服务端没收到怎么办?那就再发个FIN给客户端。这时候就会重传tcp_orphan_retries 次,这时候没有TIME_WAIT岂不是很尬?你客户端连接收通道都关了我服务端传给谁啊???那服务端还关不关了?就一直卡在了LASTACK
状态了。
- 防止旧连接的数据包
TIME-WAIT 还有一个作用是防止收到历史数据,从而导致数据错乱的问题。
试想一个场景没有 TIME_WAIT
我直接关了,在关闭之前服务端一个报文因为网络原因延迟了,到达的时候这个接口被重新握手启动了。
新的客户端收到了延迟的数据包,那就尴尬了。
所以,TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的数据包都被丢弃,
使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
为什么TIME_WAIT是2MSL呢?
之前有提到一手:在 Linux 系统下大约等待 1 分钟后,TIME_WAIT 状态的连接才会彻底关闭。
那么为什么 TIME_WAIT 状态要保持 60 秒呢?
因为FIN_WAIT2,TIME_WAIT状态都需要保持 2MSL 时长。MSL 全称是 Maximum
Segment Lifetime,它定义了一个报文在网络中的最长生存时间(报文每经过一次路由器的转发,IP头部的 TTL 字段就会减 1,减到 0 时报文就被丢弃,这就限制了报文的最长存活时间)。
为什么是 2 MSL 的时长呢?这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。
为什么不是更长的时间呢?丢包率1%算是糟糕了吧,连续丢两次呢?我靠,1/1W的概率,还是丢了吧。
因此,TIME_WAIT 和 FIN_WAIT2 状态的最大时长都是 2 MSL,由于在 Linux 系统中,MSL 的值固定为 30 秒,所以它们都是 60 秒。
那要怎么优化TIME_WAIT状态呢?
为什么要优化TIME_WAIT?
之前说到了TIME_WAIT的两个作用,还挺得劲,那为什么要优化它呢?(不优化他你怎么面试呢?)
话不是这么说的,你看哈虽然 TIME_WAIT 状态有存在的必要,但它毕竟会消耗系统资源。如果发起连接一方的 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。
- 客户端受端口资源限制:如果客户端 TIME_WAIT 过多,就会导致端口资源被占用,因为端口就65536个,被占满就会导致无法创建新的连接;
- 服务端受系统资源限制:由于一个四元组表示TCP连接,理论上服务端可以建立很多连接,服务端确实只监听一个端口,但是会把连接扔给处理线程,所以理论上监听的端口可以继续监听。但是线程池处理不了那么多一直不断的连接了。所以当服务端出现大量 TIME_WAIT时,系统资源被占满时,会导致处理不过来新的连接;
怎么优化TIME_WAIT
Linux 提供了 tcp_max_tw_buckets 参数,当 TIME_WAIT 的连接数量超过该参数时,新关闭的连接就不再经历 TIME_WAIT 而直接关闭:
调整timewait最大个数
echo 5000>/proc/sys/net/ipv4/tcp_max_tw_buckets
当服务器的并发连接增多时,相应地,同时处于 TIME_WAIT 状态的连接数量也会变多,此时就应当调大 tcp_max_tw_buckets 参数,减少不同连接间数据错乱的概率。
tcp_max_tw_buckets 也不是越大越好,毕竟内存和端口都是有限的
有一种方式可以在建立新连接时,复用处于 TIME_WAIT 状态的连接,那就是打开 tcp_tw_reuse 参数。但是需要注意,该参数是只用于客户端(建立连接的发起方),因为是在调用connect() 时起作用的,而对于服务端(被动连接方)是没有用的。
打开tcp_tw_reuse功能
echo 1>/proc/sys/net/ipv4/tcp_tw_reuse
tcp_tw_reuse 从协议角度理解是安全可控的,可以复用处于 TIME_WAIT 的端口为新的连接所用。
什么是协议角度理解的安全可控呢?主要有两点:
- 只适用于连接发起方,也就是 C/S 模型中的客户端;
- 对应的 TIME_WAIT 状态的连接创建时间超过 1 秒才可以被复用。