为什么 TCP 需要 TIME_WAIT 状态
2018-12-11 计算机网络
TCP 状态机
下图所示的状态机展示了,通信双方建立 TCP 连接之后的状态转换过程。
通信双方主动发起关闭连接的一端,存在
TIME_WAIT
状态,被动接受关闭连接的一端,会进入CLOSE_WAIT
状态。
处于 TIME_WAIT
状态的一端,主要浪费两种资源:
- 端口号 (主要资源)
- 系统资源 (文件描述符、内存资源、CPU 资源、线程资源),对于现代化硬件来说,这点资源可以忽略不计
两个问题
这里以客户端主动关闭连接为例,先来看下经典的 “四次挥手” 过程:
- 第一次挥手: 当客户端没有要发送的数据时,向服务端发送
FIN
消息,客户端进入FIN_WAIT_1
状态 - 第二次挥手: 服务端收到客户端的
FIN
消息之后,进入CLOSE_WAIT
状态,然后向客户端发送ACK
消息,客户端收到ACK
消息之后进入FIN_WAIT_2
状态 - 第三次挥手: 当服务端没有要发送的数据时,向客户端发送
FIN
消息,然后服务端进入LAST_ACK
状态 - 第四次挥手: 客户端收到服务端的
FIN
消息之后,进入TIME_WAIT
状态,然后向服务端发送ACK
消息,服务端收到ACK
消息进入CLOSED
状态 - 客户端等大等待 2 个 MSL 时间后进入
CLOSED
状态
客户端需要维护一个 TIME_WAIT
状态长达 2 个 MSL 时间,以 Linux 5.0 代码为例,也就是 2 分钟。
// https://github.com/torvalds/linux/blob/1c163f4c7b3f621efff9b28a47abb36f7378d783/include/net/tcp.h#L121
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds */
为什么是 2 个 MSL 时间呢?
因为发送方要考虑数据报文 从发送方到接收方的 MSL, 反过来说,接收方也要考虑 ACK 报文从接收方到发送方的 MSL,所以一共是 2 个 MSL (通信双方各一个)。
之所以采用这种机制,主要是为了避免发生下面两个问题:
- 防止被动关闭连接的一端,延迟的数据被后续使用 相同四元组的 TCP 连接 接收
- 防止被动关闭连接的一端,发出的
FIN
消息没有收到对应的ACK
消息,而在新连接到来的时候发送RST
消息
下面就这两个问题来进行分析说明。
第一个问题
防止被动关闭连接的一端,延迟的数据被后续使用 相同四元组的新连接 接收。
简单来说,就是防止旧的 TCP 连接,因为延迟到达的报文,而干扰到了复用相同四元组的新连接。
$$ TCP 四元组 = 客户端 IP + 客户端端口 + 服务端 IP + 服务端端口 $$
下面来举例说一下什么场景下会出现这种情况。
- 假设在客户端在主动关闭连接前,服务端发送了一个
seq = 1001
的数据包 A,但是由于网络原因一直没有送达到客户端 (也就是以说,该数据包延迟了) - 如果客户端没有
TIME_WAIT
状态,那么客户端第四次挥手后,此时客户端会直接进入CLOSED
状态 - 延迟的数据包 A 依然没有到来
- 这时客户端又向服务端发起新的连接,很巧的是,这个新连接和刚才 (已关闭) 的旧连接使用了同样的端口 (复用),于是新连接和旧连接的四元组变成了一样的
- 新连接建立完成后,旧连接上面的延迟数据包 A 到了 (新连接),这时就会产生严重的问题 …
通过 TIME_WAIT
状态,发起主动关闭连接的一端会等待 2 个 MSL 时间,这个时间足够长,可以最大限度消除延迟的数据包可能对新 (复用端口) 的连接造成影响, TIME_WAIT
状态下接收到的延迟数据包会被直接丢弃。
这里考虑一个极端的 (小概率) 问题: 经过 2 个 MSL 时间之后,延迟的数据包 A 到达了,并且其 Seq
正好位于新连接的接收窗口内,那么新连接 (TCP 传输层) 会直接将整个数据包转交到上层应用吗?
根据应用层安全 (是否加密) 程度,分为两种情况:
- 未加密,那么就会干扰应用数据,例如源数据为 “我的头像牛X吗?”,因为延迟的数据包,在 头 字后面插入了一个逗号,变成了 “我的头,像牛X吗?”
- 已加密 (例如使用了 HTTPS),TLS 会校验数据完整性,那么显然数据包 A 是无法通过校验的,然后被直接丢弃,HTTPS 连接就此中断
第二个问题
防止被动关闭连接的一端,发出的
FIN
消息没有收到对应的ACK
消息,而在新连接到来的时候发送RST
消息。
简单来说,就是防止旧的 TCP 连接在第四次挥手中发出的 ACK
消息没有被对端收到,而导致复用相同四元组的新连接在和对端建立连接时被拒绝。
下面来举例说一下什么场景下会出现这种情况。
- 客户端第四次挥手时向服务端发送
ACK
消息,但是这个ACK
消息一直因为网络原因一直未送达,所以服务端的状态停留在了LAST-ACK
,而不是正常的CLOSED
状态 - 如果客户端没有
TIME_WAIT
状态,那么客户端第四次挥手后,此时客户端会直接进入CLOSED
状态,但是此时服务端认为这条连接依然是有效的 (还未彻底断开) - 这时客户端又向服务端发起新的连接,很巧的是,这个新连接和刚才 (已关闭) 的旧连接使用了同样的端口 (复用),于是新连接和旧连接的四元组变成了一样的
- 服务端收到了客户端的新链接建立时发送的
SYNC
消息,在服务端看来,这其实是一条旧的有效连接 (因为新连接和旧连接的四元组是一样的),所以会直接返回RST
消息,拒绝新连接的建立 (连接过程到此终止)
通过 TIME_WAIT
状态,发起主动关闭连接的一端会等待 2 个 MSL 时间,这个时间足够长,那么服务端可能会出现两种情况:
- 收到了客户端的
ACK
消息,正常关闭连接,进入CLOSE
状态 - 没有收到客户端的
ACK
消息,重新发送FIN
消息关闭连接并等待新的ACK
消息 (重新执行第三次、四次挥手)
问题场景
分析完了 TIME_WAIT
状态的作用之外,什么场景下会出现大量的 TIME_WAIT
状态连接呢?
通信双方主动发起关闭连接的一端,存在 TIME_WAIT 状态,最经典的场景就是 并发压力测试
。
当我们在本地 (客户端) 启动并发压力测试时,通常会设置成百上千的并发连接去访问服务端接口,这些连接会快速且大量消耗 TCP 连接资源,每个连接在完成接口请求后会理解进入 TIME_WAIT
状态。
例如,在 Linux 系统中,可以使用 netstat 命令来查看各个不同状态的网络连接:
$ netstat -ant | grep TIME_WAIT
# 输出如下
tcp6 0 0 1.1.1.1:443 127.0.0.1:59490 TIME_WAIT
tcp6 0 0 1.1.1.1:443 127.0.0.1:59472 TIME_WAIT
tcp6 0 0 1.1.1.1:443 127.0.0.1:59480 TIME_WAIT
tcp6 0 0 1.1.1.1:443 127.0.0.1:59514 TIME_WAIT
tcp6 0 0 1.1.1.1:443 127.0.0.1:59484 TIME_WAIT
tcp6 0 0 1.1.1.1:443 127.0.0.1:59520 TIME_WAIT
这类问题如何解决,后面再专门写一篇高性能网络编程中 TCP 调优的文章 :-)。
❓ 思考
如果系统中出现大量的 CLOSE_WAIT
状态连接,可能原因是什么呢?如何解决?