深入TCP协议——tcp_tw_reuse和tcp_tw_recycle

前情提要:深入理解Linux网络——TCP协议三次握手和四次挥手详细流程

我们已经知道TCP四次挥手中,主动方在收到被动方的FIN数据包之后会进入TIME_WAIT状态等待2MSL的时间后才进入CLOSED。在 Linux 操作系统下,TIME_WAIT 状态的持续时间是 60 秒,这意味着这 60 秒内,客户端一直会占用着这个端口,这是有一定的开销的。如果如果主动关闭连接方的 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。

不过,Linux 操作系统提供了两个可以系统参数来快速回收处于 TIME_WAIT 状态的连接(这两个参数都是默认关闭的),分别是net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle。

一、tcp_tw_reuse

如果开启该选项的话,客户端 在调用 connect() 函数时,内核会随机找一个 TIME_WAIT 状态超过 1 秒的连接给新的连接复用。这其实就是相当于缩短了 TIME_WAIT 状态的持续时间。

这里既然我们缩短了TIME_WAIT状态的时间,使其不是2MSL,那么因为引入TIME_WAIT解决的问题就再次出现了,所以tcp_tw_reuse也需要解决这两个问题:

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

首先看第一个问题,因为如果在历史连接上建立了新的连接,而网络中此时还有历史连接残留的数据包存活,那么它们有可能在新连接建立之后到达,那么就会导致接收到错误的数据。TIME_WAIT的解决方法就是等待2MSL之后才进入CLOSED,也就是说耗死了网络中残留的数据包,保证新连接建立时历史数据包全都死亡了。而前面我们提到了,使用tcp_tw_reuse只需要TIME_WAIT状态超过1秒就可以建立新的连接,所以它需要采用别的方式来解决这个历史数据报的问题。这里是引入了tcp_timestamps这个时间戳选项和防回绕序列号算法PAWS来解决。

防回绕序列号算法要求连接双方维护最近一次收到的数据包的时间戳(Recent TSval),每收到一个新数据包都会读取数据包中的时间戳值跟 Recent TSval 值做比较,如果发现收到的数据包中时间戳不是递增的,则表示该数据包是过期的,就会直接丢弃这个数据包。历史数据包显然时间戳必然是早于新连接建立后保存的时间戳的,所以就避免了接收旧连接残留数据的问题。

序列号本来也是递增的,正常来说延迟的数据包重新到达时序号是对不上的,因为这期间有其他新的数据包到达,会改变期待的序列号。而如果在延迟期间序列号兜了一圈又回到了原本的位置,那么延迟的数据包正好就能被正确的读取,这会导致数据错误,而时间戳保存了最近一次收到数据包的时间,所以就避免了这种情况。

即序列号会回绕,所以无法根据序列号来判断当前数据包是不是旧的数据包。

所以显然使用tcp_tw_reuse需要打开 TCP 时间戳选项,即 net.ipv4.tcp_timestamps=1(默认即为 1)。

但是其实tcp_tw_reuse还是存在风险:因为PAWS算法不会防止过期的RST,所以如果前面有残留的RST报文,在新连接建立之后到达,那么就会导致新连接被这个历史的RST包中断。如果此时不跳过 TIME_WAIT 状态,而是停留 2MSL 时长,那么这个 RST 报文就不会出现下一个新的连接。

至于第二个问题,tcp_tw_reuse并没办法解决,如果第四次挥手的 ACK 报文丢失了,而此时这个端口已经被另一个连接使用了,那么原服务端重发的FIN就会发送失败,从而接收到RST报文而异常关闭。

所以说tcp_tw_reuse还是存在风险的。

二、tcp_tw_recycle

这种机制也依赖时间戳选项,开启tcp_tw_recycel后服务器将会缓存每个套接字的最新时间戳,TIME-WAIT状态将会在超时重发(RTO)间隔后移除。不同的是tcp_tw_recycle使用的是per-host的PAWS机制,它是队对端IP做PAWS检查(tcp_tw_reuse是对四元组做PAWS检查),即Linux将会放弃所有来自远程端的timestamp时间戳小于上次记录的时间戳(也是远程端发来的)的任何数据包,除非TIME-WAIT状态已经过期**。**这个时候如果客户端网络环境是用了 NAT 网关,那么客户端环境的每一台机器通过 NAT 网关后,都会是相同的 IP 地址,在服务端看来,就好像只是在跟一个客户端打交道一样,无法区分出来(因为它是以对端IP为粒度的,而不是IP+端口)。当客户端NAT环境有多个设备,每个设备的时间肯定不会完全一致,所以访问服务端的时候,服务端观察到这个IP携带的timestamp不是单调递增的,所以会导致大量的包丢失(假如A、B机器都位于同个NAT网关下,A机器的时间要比B机器时间慢,那么当B机器发送了SYN数据包建立连接之后,A机器此时发送的SYN包就会被丢弃)。

实际上大多数情况下,都是tcp连接上的客户端主动发起fin关闭,即time_wait状态会出现在客户端,而服务端不会出现time_wait状态,这种情况服务端是不需要考虑优化回收time_wait的。但是现代的大多数应用部署在服务器上,它即会作为服务端对外提供服务,也会作为客户端去请求其他服务器上的接口,前者因为是作为服务端,很少出现time_wait状态,后者作为客户端很容易在服务器上产生大量TCP time_wait状态,我们想快速回收time_wait,主要是针对后者这种情况。此时如果我们想解决后者time_wait过多的情况,在服务器(注意是服务器,不是服务端)上开启tcp_tw_recycle快速回收端口,因为这台服务器上的应用同时又是作为服务端对外提供服务,tcp_tw_recycle触发的pre-host PAWS机制,会使得客户端超时异常。

所以net.ipv4.tcp_tw_recycle=1不能轻易开启,如果非要开启只能在以下两种场景:

  1. 并发内部调用,一般不存在客户端proxy(即客户端NAT)
  2. 自己对上下游应用有较强的可控性,可以随时调整上下游的参数一起去适配
  3. 这台服务器上的应用,只会作为客户端频繁请求其他IP(一般情况都是客户端主动关闭TCP,产生time_wait状态),没有作为服务端对外提供服务。

参考文章:

被微信问懵了:既然开启 net.ipv4.tcp_tw_reuse 可以复用连接,为什么内核不默认开启呢? - 知乎 (zhihu.com)

深挖由tcp_tw_recycle引发的业务超时问题 - 知乎 (zhihu.com)

你可能感兴趣的:(tcp/ip,网络,tcp/ip,网络,服务器)