浅谈tcp_tw_reuse

既然打开 net.ipv4.tcp_tw_reuse 参数可以快速复用处于 TIME_WAIT 状态的 TCP 连接,那为什么 Linux 内核默认是关闭状态呢?
tcp_tw_reuse 的作用是让客户端很快的复用 time_wait 的端口,相当于跳过了这个状态,所以默认关闭

【如果 TIME_WAIT 状态持续时间过短或者没有,会有什么问题?】
因为开启 tcp_tw_reuse 参数可以快速复用处于 TIME_WAIT 状态的 TCP 连接时,相当于缩短了 TIME_WAIT 状态的持续时间。
使用 tcp_tw_reuse 快速复用处于 TIME_WAIT 状态的 TCP 连接时,是需要保证 net.ipv4.tcp_timestamps 参数是开启的(默认是开启的),而 tcp_timestamps 参数可以避免旧连接的延迟报文,这不是解决了没有 TIME_WAIT 状态时的问题了吗?

什么是TIME_WAIT状态?

TCP四次挥手过程,如下图:
浅谈tcp_tw_reuse

  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
  • 服务器收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。

你可以看到,两个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。

这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。

可以看到,TIME_WAIT 是「主动关闭方」断开连接时的最后一个状态,该状态会持续 2MSL(Maximum Segment Lifetime) 时长,之后进入CLOSED 状态。

MSL 指的是 TCP 协议中任何报文在网络上最大的生存时间,任何超过这个时间的数据都将被丢弃。虽然 RFC 793 规定 MSL 为 2 分钟,但是在实际实现的时候会有所不同,比如 Linux 默认为 30 秒,那么 2MSL 就是 60 秒。

MSL 是由网络层的 IP 包中的 TTL 来保证的,TTL 是 IP 头部的一个字段,用于设置一个数据报可经过的路由器的数量上限。报文每经过一次路由器的转发,IP 头部的 TTL 字段就会减 1,减到 0 时报文就被丢弃。

MSL 与 TTL 的区别:MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。

TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了。

为什么要设计TIME_WAIT状态?

设计 TIME_WAIT 状态,主要有两个原因:

  • 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
  • 保证「被动关闭连接」的一方,能被正确的关闭;

原因一:防止历史连接中的数据,被后面相同四元组的连接错误的接收

为了能更好的理解这个原因,我们先来了解序列号(SEQ)和初始序列号(ISN)。

  • 序列号,是 TCP 一个头部字段,标识了 TCP 发送端到 TCP 接收端的数据流的一个字节,因为 TCP 是面向字节流的可靠协议,为了保证消息的顺序性和可靠性,TCP 为每个传输方向上的每个字节都赋予了一个编号,以便于传输成功后确认、丢失后重传以及在接收端保证不会乱序。序列号是一个 32 位的无符号数,因此在到达 4G 之后再循环回到 0。
  • 初始序列号,在 TCP 建立连接的时候,客户端和服务端都会各自生成一个初始序列号,它是基于时钟生成的一个随机数,来保证每个连接都拥有不同的初始序列号。初始化序列号可被视为一个 32 位的计数器,该计数器的数值每 4 微秒加 1,循环一次需要 4.55 小时。

通过前面我们知道,序列号和初始化序列号并不是无限递增的,会发生回绕为初始值的情况,这意味着无法根据序列号来判断新老数据。

假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢?
浅谈tcp_tw_reuse

  • 服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了。
  • 接着,服务端以相同的四元组重新打开了新连接,前面被延迟的 SEQ = 301 这时抵达了客户端,而且该数据报文的序列号刚好在客户端接收窗口内,因此客户端会正常接收这个数据报文,但是这个数据报文是上一个连接残留下来的,这样就产生数据错乱等严重的问题。

为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。

原因二:保证「被动关闭连接」的一方,能被正确的关闭

如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。

假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSED 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,而这时客户端已经进入到关闭状态了,在收到服务端重传的 FIN 报文后,就会回 RST 报文。
浅谈tcp_tw_reuse
服务端收到这个 RST 并将其解释为一个错误(Connection reset by peer),这对于一个可靠的协议来说不是一个优雅的终止方式。

为了防止这种情况出现,客户端必须等待足够长的时间确保对端收到 ACK,如果对端没有收到 ACK,那么就会触发 TCP 重传机制,服务端会重新发送一个 FIN,这样一去一来刚好两个 MSL 的时间。
浅谈tcp_tw_reuse
但是你可能会说重新发送的 ACK 还是有可能丢失啊,没错,但 TCP 已经等待了那么长的时间了,已经算仁至义尽了。

tcp_tw_reuse是什么?

在 Linux 操作系统下,TIME_WAIT 状态的持续时间是 60 秒,这意味着这 60 秒内,客户端一直会占用着这个端口。要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000 ,也可以通过如下参数设置指定范围:

net.ipv4.ip_local_port_range

那么,如果如果主动关闭连接方的 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。

不过,Linux 操作系统提供了两个可以系统参数来快速回收处于 TIME_WAIT 状态的连接,这两个参数都是默认关闭的:

  • net.ipv4.tcp_tw_reuse,如果开启该选项的话,客户端(连接发起方) 在调用 connect() 函数时,内核会随机找一个 TIME_WAIT 状态超过 1 秒的连接给新的连接复用,所以该选项只适用于连接发起方。

  • net.ipv4.tcp_tw_recycle,如果开启该选项的话,允许处于 TIME_WAIT 状态的连接被快速回收,该参数在 NAT 的网络下是不安全的! 详细见这篇文章介绍:字节面试:SYN 报文什么时候情况下会被丢弃?

要使得上面这两个参数生效,有一个前提条件,就是要打开 TCP 时间戳,即 net.ipv4.tcp_timestamps=1(默认即为 1)。

开启了 tcp_timestamps 参数,TCP 头部就会使用时间戳选项,它有两个好处,一个是便于精确计算 RTT ,另一个是能防止序列号回绕(PAWS),我们先来介绍这个功能。

序列号是一个 32 位的无符号整型,上限值是 4GB,超过 4GB 后就需要将序列号回绕进行重用。这在以前网速慢的年代不会造成什么问题,但在一个速度足够快的网络中传输大量数据时,序列号的回绕时间就会变短。如果序列号回绕的时间极短,我们就会再次面临之前延迟的报文抵达后序列号依然有效的问题。

为了解决这个问题,就需要有 TCP 时间戳。

试看下面的示例,假设 TCP 的发送窗口是 1 GB,并且使用了时间戳选项,发送方会为每个 TCP 报文分配时间戳数值,我们假设每个报文时间加 1,然后使用这个连接传输一个 6GB 大小的数据流。
浅谈tcp_tw_reuse

为什么tcp_tw_reuse默认是关闭的

第一个问题
我们知道开启 tcp_tw_reuse 的同时,也需要开启 tcp_timestamps,意味着可以用时间戳的方式有效的判断回绕序列号的历史报文。

但是在看我看了防回绕序列号算法的源码后,发现对于 RST 报文的时间戳即使过期了,只要 RST 报文的序列号在对方的接收窗口内,也是能被接受的。

浅谈tcp_tw_reuse

  • 客户端向一个还没有被服务端监听的端口发起了 HTTP 请求,接着服务端就会回 RST 报文给对方,很可惜的是 RST 报文被网络阻塞了。

  • 由于客户端迟迟没有收到 TCP 第二次握手,于是重发了 SYN 包,与此同时服务端已经开启了服务,监听了对应的端口。于是接下来,客户端和服务端就进行了 TCP 三次握手、数据传输(HTTP应答-响应)、四次挥手。

  • 因为客户端开启了 tcp_tw_reuse,于是快速复用 TIME_WAIT 状态的端口,又与服务端建立了一个与刚才相同的四元组的连接。

  • 接着,前面被网络延迟 RST 报文这时抵达了客户端,而且 RST 报文的序列号在客户端的接收窗口内,由于防回绕序列号算法不会防止过期的 RST,所以 RST 报文会被客户端接受了,于是客户端的连接就断开了。

上面这个场景就是开启 tcp_tw_reuse 风险,因为快速复用 TIME_WAIT 状态的端口,导致新连接可能被回绕序列号的 RST 报文断开了,而如果不跳过 TIME_WAIT 状态,而是停留 2MSL 时长,那么这个 RST 报文就不会出现下一个新的连接。

 
第二个问题
开启 tcp_tw_reuse 来快速复用 TIME_WAIT 状态的连接,如果第四次挥手的 ACK 报文丢失了,有可能会导致被动关闭连接的一方不能被正常的关闭,如下图:
浅谈tcp_tw_reuse

总结:

tcp_tw_reuse 的作用是让客户端快速复用处于 TIME_WAIT 状态的端口,相当于跳过了 TIME_WAIT 状态,这可能会出现这样的两个问题:

  • 历史 RST 报文可能会终止后面相同四元组的连接,因为 PAWS 检查到即使 RST 是过期的,也不会丢弃。
  • 如果第四次挥手的 ACK 报文丢失了,有可能被动关闭连接的一方不能被正常的关闭;

虽然 TIME_WAIT 状态持续的时间是有一点长,显得很不友好,但是它被设计来就是用来避免发生乱七八糟的事情。

上一篇:playwright--自动化(三): 跳过检测 使用正常谷歌 指定用户数据 下载文件


下一篇:面试必备之手写节流和防抖函数