TIME_WAIT 是 TCP 所有状态中最不好理解的一种状态。
TCP三次握手四次挥手首先,我们需要明确,只有主动断开的那一方才会进入 TIME_WAIT 状态,且会在那个状态持续 2 个 MSL(Max Segment Lifetime)。
为了讲清楚 TIME_WAIT,需要先介绍一下 MSL 的概念。
MSL:Max Segment Lifetime
MSL(报文最大生存时间)是 TCP 报文在网络中的最大生存时间。这个值与 IP 报文头的 TTL 字段有密切的关系。
IP 报文头中有一个 8 位的存活时间字段(Time to live, TTL)如下图。 这个存活时间存储的不是具体的时间,而是一个 IP 报文最大可经过的路由数,每经过一个路由器,TTL 减 1,当 TTL 减到 0 时这个 IP 报文会被丢弃。
TTL 经过路由器不断减小的过程如下图所示,假设初始的 TTL 为 12,经过下一个路由器 R1 以后 TTL 变为 11,后面每经过一个路由器以后 TTL 减 1
从上面可以看到 TTL 说的是「跳数」限制而不是「时间」限制,尽管如此我们依然假设最大跳数的报文在网络中存活的时间不可能超过 MSL 秒。
Linux 的套接字实现假设 MSL 为 30 秒,因此在 Linux 机器上 TIME_WAIT 状态将持续 60秒。
构造一个 TIME_WAIT
要构造一个 TIME_WAIT 非常简单,只需要建立一个 TCP 连接,然后断开某一方连接,主动断开的那一方就会进入 TIME_WAIT 状态,我们用 Linux 上开箱即用的 nc 命令来构造一个。
过程如下图:
在机器 c2 上用nc -l 8888启动一个 TCP 服务器
在机器 c1 上用 nc c2 8888 创建一条 TCP 连接
在机器 c1 上用 Ctrl+C 停止 nc 命令,随后在用netstat -atnp | grep 8888查看连接状态。
TIME_WAIT 存在的原因是什么?
第一个原因是:数据报文可能在发送途中延迟但最终会到达,因此要等老的“迷路”的重复报文段在网络中过期失效,这样可以避免用相同源端口和目标端口创建新连接时收到旧连接姗姗来迟的数据包,造成数据错乱。
比如下面的例子
假设客户端 10.211.55.2 的 61594 端口与服务端 10.211.55.10 的 8080 端口一开始建立了一个 TCP 连接。
假如客户端发送完 FIN 包以后不等待直接进入 CLOSED 状态,老连接 SEQ=3 的包因为网络的延迟。过了一段时间相同的 IP 和端口号又新建了另一条连接,这样 TCP 连接的四元组就完全一样了。
恰好 SEQ 因为回绕等原因也正好相同,那么 SEQ=3 的包就无法知道到底是旧连接的包还是新连接的包了,造成新连接数据的混乱。
TIME_WAIT 等待时间是 2 个 MSL,已经足够让一个方向上的包最多存活 MSL 秒就被丢弃,保证了在创建新的 TCP 连接以后,老连接姗姗来迟的包已经在网络中被丢弃消逝,不会干扰新的连接。
第二个原因是确保可靠实现 TCP 全双工终止连接。
关闭连接的四次挥手中,最终的 ACK 由主动关闭方发出,如果这个 ACK 丢失,对端(被动关闭方)将重发 FIN,如果主动关闭方不维持 TIME_WAIT 直接进入 CLOSED 状态,则无法重传 ACK,被动关闭方因此不能及时可靠释放。
如果四次挥手的第 4 步中客户端发送了给服务端的确认 ACK 报文以后不进入 TIME_WAIT 状态,直接进入 CLOSED状态,然后重用端口建立新连接会发生什么呢?
如下图所示
主动关闭方如果马上进入 CLOSED 状态,被动关闭方这个时候还处于LAST-ACK状态,主动关闭方认为连接已经释放,端口可以重用了,如果使用相同的端口三次握手发送 SYN 包,会被处于 LAST-ACK状态状态的被动关闭方返回一个 RST,三次握手失败。
为什么时间是两个 MSL?
1 个 MSL 确保四次挥手中主动关闭方最后的 ACK 报文最终能达到对端
1 个 MSL 确保对端没有收到 ACK 重传的 FIN 报文可以到达
2MS = 去向 ACK 消息最大存活时间(MSL) + 来向 FIN 消息的最大存活时间(MSL)
TIME_WAIT 的问题
在一个非常繁忙的服务器上,如果有大量 TIME_WAIT 状态的连接会怎么样呢?
连接表无法复用
socket 结构体内存占用
连接表无法复用 因为处于 TIME_WAIT 的连接会存活 2MSL(60s),意味着相同的TCP 连接四元组(源端口、源 ip、目标端口、目标 ip)在一分钟之内都没有办法复用,通俗一点来讲就是“占着茅坑不拉屎”。
假设主动断开的一方是客户端,对于 web 服务器而言,目标地址、目标端口都是固定值(比如本机 ip + 80 端口),客户端的 IP 也是固定的,那么能变化的就只有端口了,在一台 Linux 机器上,端口最多是 65535 个( 2 个字节)。
如果客户端与服务器通信全部使用短连接,不停的创建连接,接着关闭连接,客户端机器会造成大量的 TCP 连接进入 TIME_WAIT 状态。
可以来写一个简单的 shell 脚本来测试一下,使用 nc 命令连接 redis 发送 ping 命令以后断开连接。
如果在 60s 内有超过 65535 次 redis 短连接操作,就会出现端口不够用的情况,这也是使用连接池的一个重要原因。
应对 TIME_WAIT 的各种操作
针对 TIME_WAIT 持续时间过长的问题,Linux 新增了几个相关的选项,net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_tw_recycle。
下面我们来说明一下这两个参数的用意。 这两个参数都依赖于 TCP 头部的扩展选项:timestamp
TCP 头部时间戳选项(TCP Timestamps Option,TSopt)
除了我们之前介绍的 MSS、Window Scale 还有以一个非常重要的选项:时间戳(TCP Timestamps Option,TSopt)
它由四部分构成:类别(kind)、长度(Length)、发送方时间戳(TS value)、回显时间戳(TS Echo Reply)。
时间戳选项类别(kind)的值等于 8,用来与其它类型的选项区分。长度(length)等于 10。两个时间戳相关的选项都是 4 字节。
如下图所示:
是否使用时间戳选项是在三次握手里面的 SYN 报文里面确定的。
下面的包是 curl github.com 抓包得到的结果:
发送方发送数据时,将一个发送时间戳 1734581141 放在发送方时间戳TSval中
接收方收到数据包以后,将收到的时间戳 1734581141 原封不动的返回给发送方,放在TSecr字段中,同时把自己的时间戳 3303928779 放在TSval中
后面的包以此类推
有几个需要说明的点:
1. 时间戳是一个单调递增的值,与我们所知的 epoch 时间戳不是一回事。这个选项不要求两台主机进行时钟同步
2. timestamps 是一个双向的选项,如果只要有一方不开启,双方都将停用 timestamps。
比如下面是curl www.baidu.com得到的包
可以看到客户端发起 SYN 包时带上了自己的TSval,服务器回复的SYN+ACK 包没有TSval和TSecr,从此之后的包都没有带上时间戳选项了。
有了这个选项,我们来看一下 tcp_tw_reuse 选项。
tcp_tw_reuse 选项
缓解紧张的端口资源,一个可行的方法是重用“浪费”的处于 TIME_WAIT 状态的连接,当开启 net.ipv4.tcp_tw_reuse 选项时,处于 TIME_WAIT 状态的连接可以被重用。
下面把主动关闭方记为 A, 被动关闭方记为 B,它的原理是:
如果主动关闭方 A 收到的包时间戳比当前存储的时间戳小,说明是一个迷路的旧连接的包,直接丢弃掉
如果因为 ACK 包丢失导致被动关闭方还处于LAST-ACK状态,并且会持续重传 FIN+ACK。这时 A 发送SYN 包想三次握手建立连接,此时 A 处于SYN-SENT阶段。当收到 B 的 FIN 包时会回以一个 RST 包给 B,B 这端的连接会进入 CLOSED 状态,A 因为没有收到 SYN 包的 ACK,会重传 SYN,后面就一切顺利了。
tcp_tw_recyle 选项
tcp_tw_recyle 是一个比 tcp_tw_reuse 更激进的方案, 系统会缓存每台主机(即 IP)连接过来的最新的时间戳。
对于新来的连接,如果发现 SYN 包中带的时间戳与之前记录的来自同一主机的同一连接的分组所携带的时间戳相比更旧,则直接丢弃;如果更新则接受复用 TIME-WAIT 连接。
这种机制在客户端与服务端一对一的情况下没有问题,如果经过了 NAT 或者负载均衡,问题就很严重了。
什么是 NAT呢?
NAT(Network Address Translator)的出现是为了缓解 IP 地址耗尽的临时方案,IPv4 的地址是 32 位,全部利用最 多只能提 42.9 亿个地址,去掉保留地址、组播地址等剩下的只有 30 多亿,互联网主机数量呈指数级的增长,如果给每个设备都分配一个唯一的 IP 地址,那根本不够。于是 1994 年推出的 NAT 规范,NAT 设备负责维护局域网私有 IP 地址和端口到外网 IP 和端口的映射规则。
它有两个明显的优点:
出口 IP 共享:通过一个公网地址可以让许多机器连上网络,解决 IP 地址不够用的问题
安全隐私防护:实际的机器可以隐藏自己真实的 IP 地址 当然也有明显的弊端:NAT 会对包进行修改,有些协议无法通过 NAT。
当 tcp_tw_recycle 遇上 NAT 时,因为客户端出口 IP 都一样,会导致服务端看起来都在跟同一个 host 打交道。
不同客户端携带的 timestamp 只跟自己相关,如果一个时间戳较大的客户端 A 通过 NAT 与服务器建连,时间戳较小的客户端 B 通过 NAT 发送的包服务器认为是过期重复的数据,直接丢弃,导致 B 无法正常建连和发数据。
小结
TIME_WAIT 状态是最容易造成混淆的一个概念,这个状态存在的意义是:
1. 可靠的实现 TCP 全双工的连接终止(处理最后 ACK 丢失的情况)
2. 避免当前关闭连接与后续连接混淆(让旧连接的包在网络中消逝)
思考题
假设 MSL 是 60s,请问系统能够初始化一个新连接然后主动关闭的最大速率是多少(忽略1~1024区间的端口)?
2MSL = 120s,(65535 - 1024) / 120 = 537.6 次/秒
每120秒可以初始化(65535-1024 )个
“时间戳是一个单调递增的值,与我们所知的 epoch 时间戳不是一回事” 这个epoch和时间戳分别是什么差异?
不是一回事,跟时间没有什么关系,只是随着时钟信号CPU中断递增。
SO_REUSEADDR是针对服务端的,tcp_tw_reuse和tcp_tw_recyle是针对客户端的,可以这样理解吗?
SO_REUSEADDR 两端都可以用,不过服务端上因为经常要固定端口,不设置,下次重启就bind 失败 。
tcp_tw_reuse和tcp_tw_recyle 也是主要用于繁忙的“服务端”,“客户端”和“服务端”这个说法是在不同的场景下可以互相转换的,服务端也可以发起请求充当客户端。
参考
https://juejin.cn/book/6844733788681928712/section/6844733788837117959
从SO_REUSEADDR选项说起
https://zhuanlan.zhihu.com/p/31329253
网友评论