1、为什么 TIME_WAIT 等待的时间是 2MSL?
MSL 是 Maximum Segment Lifetime,报⽂最⼤⽣存时间, 它是任何报⽂在⽹络上存在的最⻓时间,超过这个时间报⽂将被丢弃。因为 TCP 报⽂基于是 IP 协议的,⽽ IP 头中有⼀个 TTL 字段,是 IP 数据报可以经过的最⼤路由数,每经过⼀个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报⽂通知源主机。
MSL 与 TTL 的区别: MSL 的单位是时间,⽽ TTL 是经过路由跳数。所以 MSL 应该要⼤于等于 TTL 消耗为 0 的时间,以确保报⽂已被⾃然消亡。
TIME_WAIT 等于2 倍的 MSL,比较合理的解释是: 网络中可能存在来⾃发送⽅的数据包,当这些发送⽅的数据包被接收⽅处理后又会向对方发送响应,所以⼀来⼀回需要等待 2 倍的时间。
⽐如如果被动关闭⽅没有收到断开连接的最后的 ACK 报⽂,就会触发超时重发 Fin 报⽂,另⼀⽅接收到 FIN 后,会重发 ACK 给被动关闭⽅, ⼀来⼀去正好 2 个 MSL。
2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK没有传输到服务端,客户端⼜接收到了服务端重发的 FIN 报⽂,那么 2MSL 时间将重新计时。
在 Linux 系统⾥ 2MSL 默认是 60 秒,那么⼀个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
其定义在 Linux 内核代码⾥的名称为TCP_TIMEWAIT_LEN:
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
state, about 60 seconds */
如果要修改 TIME_WAIT 的时间⻓度,只能修改 Linux 内核代码⾥ TCP_TIMEWAIT_LEN 的值,并᯿新编译 Linux内核。
2、为什么需要 TIME_WAIT 状态?
主动发起关闭连接的⼀⽅,才会有 TIME-WAIT 状态。
需要 TIME-WAIT 状态,主要是两个原因:
- 防⽌具有相同「四元组」的「旧」数据包被收到;
- 保证「被动关闭连接」的⼀⽅能被正确的关闭,即保证最后的 ACK 能让被动关闭⽅接收,从⽽帮助其正常关闭;
(1)原因⼀:防止旧连接的数据包
假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发⽣什么呢?
- 如上图⻩⾊框框服务端在关闭连接之前发送的 SEQ = 301 报⽂,被⽹络延迟了。
- 这时有相同端⼝的 TCP 连接被复⽤后,被延迟的 SEQ = 301 抵达了客户端,那么客户端是有可能正常接收这个过期的报⽂,这就会产⽣数据错乱等严重的问题。
所以,TCP 就设计出了这么⼀个机制,经过 2MSL 这个时间,⾜以让两个⽅向上的数据包都被丢弃,使得原来连接的数据包在⽹络中都⾃然消失,再出现的数据包⼀定都是新建⽴连接所产⽣的。
(2)原因⼆:保证连接正确关闭
TIME-WAIT 另一个作⽤是等待⾜够的时间以确保最后的 ACK 能让被动关闭⽅接收,从⽽帮助其正常关闭。
假设 TIME-WAIT 没有等待时间或时间过短,断开连接会造成什么问题呢?
- 如上图红⾊框框客户端四次挥⼿的最后⼀个 ACK 报⽂如果在⽹络中被丢失了,此时如果客户端 TIME-WAIT 过短或没有,则就直接进⼊了 CLOSED 状态了,那么服务端则会⼀直处在 LASE_ACK 状态。
- 当客户端发起建⽴连接的 SYN 请求报⽂后,服务端会发送 RST 报⽂给客户端,连接建⽴的过程就会被终⽌。
如果 TIME-WAIT 等待足够长的情况就会遇到两种情况:
- 服务端正常收到四次挥⼿的最后⼀个 ACK 报⽂,则服务端正常关闭连接。
- 服务端没有收到四次挥⼿的最后⼀个 ACK 报⽂时,则会᯿发 FIN 关闭连接报⽂并等待新的 ACK 报⽂。
所以客户端在 TIME-WAIT 状态等待 2MSL 时间后,就可以保证双⽅的连接都可以正常的关闭。
3、TIME_WAIT 过多有什么危害?
如果服务器有处于 TIME-WAIT 状态的 TCP,则说明是由服务器⽅主动发起的断开请求。
过多的 TIME-WAIT 状态主要的危害有两种:
- 第⼀是内存资源占用;
- 第⼆是对端⼝资源的占⽤,⼀个 TCP 连接⾄少消耗⼀个本地端口;
第⼆个危害是会造成严᯿的后果的,要知道,端⼝资源也是有限的,⼀般可以开启的端⼝为 32768~61000 ,也可以通过如下参数设置指定。
net.ipv4.ip_local_port_range
如果发起连接⼀⽅的 TIME_WAIT 状态过多,占满了所有端⼝资源,则会导致⽆法创建新连接。
客户端受端⼝资源限制:
- 客户端TIME_WAIT过多,就会导致端⼝资源被占⽤,因为端⼝就65536个,被占满就会导致⽆法创建新的连接。
服务端受系统资源限制:
- 由于⼀个四元组表示 TCP 连接,理论上服务端可以建⽴很多连接,服务端确实只监听⼀个端⼝ 但是会把连接扔给处理线程,所以理论上监听的端⼝可以继续监听。但是线程池处理不了那么多⼀直不断的连接了。所以当服务端出现⼤量 TIME_WAIT 时,系统资源被占满时,会导致处理不过来新的连接。
4、如何优化 TIME_WAIT?
各种方法都是有利有弊的。主要方法如下:
- 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;
- 输入net.ipv4.ip_local_port_rangenet.ipv4.tcp_max_tw_buckets
- 程序中使⽤ SO_LINGER ,应⽤强制使⽤ RST 关闭。
(1)方式⼀:net.ipv4.tcp_tw_reuse 和 tcp_timestamps
如下的 Linux 内核参数开启后,则可以复⽤处于 TIME_WAIT 的 socket 为新的连接所⽤。
有⼀点需要注意的是,tcp_tw_reuse 功能只能⽤客户端(连接发起⽅),因为开启了该功能,在调⽤ connect()函数时,内核会随机找⼀个 time_wait 状态超过 1 秒的连接给新的连接复⽤。
net.ipv4.tcp_tw_reuse = 1
使⽤这个选项,还有⼀个前提,需要打开对 TCP 时间戳的⽀持,即
net.ipv4.tcp_timestamps=1(默认即为 1)
这个时间戳的字段是在 TCP 头部的「选项」⾥,⽤于记录 TCP 发送⽅的当前时间戳和从对端接收到的最新时间戳。
由于引⼊了时间戳,我们在前⾯提到的 2MSL 问题就不复存在了,因为᯿复的数据包会因为时间戳过期被⾃然丢弃。
(2)方式⼆:net.ipv4.tcp_max_tw_buckets
这个值默认为 18000,当系统中处于 TIME_WAIT 的连接⼀旦超过这个值时,系统就会将后⾯的 TIME_WAIT 连接状态重置。
这个⽅法过于暴⼒,而且治标不治本,带来的问题远⽐解决的问题多,不推荐使⽤。
(3)方式三:程序中使⽤ SO_LINGER
可以通过设置 socket 选项,来设置调⽤ close 关闭连接⾏为。
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 后,会⽴该发送⼀个 RST 标志给对端,该 TCP 连接将跳过四次挥⼿,也就跳过了 TIME_WAIT 状态,直接关闭。
但这为跨越 TIME_WAIT 状态提供了⼀个可能,不过是⼀个⾮常危险的⾏为,不值得提倡。
整理自小林coding所著的《图解网络》,仅做学习用,侵删