net.ipv4.tcp_tw_recycle和net.ipv4.tcp_tw_reuse参数介绍

背景

在对服务器进行内核参数调优时,会遇到这两个参数:net.ipv4.tcp_tw_recyclenet.ipv4.tcp_tw_reuse。这两个参数默认是禁用的,有的文档会建议开启,有的文档会建议关闭,那么这两个参数具体是干什么用的呢?

在介绍这个参数之前,先来说一下TIME_WAIT状态存在的意义是什么。

什么是TIME_WAIT状态

file

  • 第一次挥手: 客户端发起挥手请求,向服务端发送标志位是FIN报文段,设置序列号seq,此时,客户端进入FIN_WAIT_1状态,这表示客户端没有数据要发送给服务端了。
  • 第二次分手:服务端收到了客户端发送的FIN报文段,向客户端返回一个标志位是ACK的报文段,ack设为seq加1,客户端进入FIN_WAIT_2状态,服务端告诉客户端,我确认并同意你的关闭请求,服务端进入CLOSED_WAIT状态。
  • 第三次分手: 服务端向客户端发送标志位是FIN的报文段,请求关闭连接,同时服务端进入LAST_ACK状态。
  • 第四次分手 : 客户端收到服务端发送的FIN报文段,向服务端发送标志位是ACK的报文段,然后客户端进入TIME_WAIT状态。服务端收到客户端的ACK报文段以后,就关闭连接。此时,客户端在经过2MSL一段时间后,自动进入CLOSE状态,关闭连接。

两个方向都需要一个FIN和一个ACK,因此通常被称为四次挥手。TIME_WAIT 是「主动关闭方」断开连接时的最后一个状态,该状态会持续 2MSL(Maximum Segment Lifetime) 时长,之后进入CLOSED 状态。

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

为什么需要TIME_WAIT状态

防止相同四元组接收到历史数据

先介绍下序列号SEQ和初始序列号ISN。

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

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

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

file

TCP是根据四元组来确定连接的,即源IP地址、目的IP地址、源端口、目的端口。服务端关闭连接之前发送的SEQ=3的报文由于网络原因被延迟了,后面服务端以相同的四元组打开新连接,这时SEQ=3的报文发送到了客户端,而该报文的序列号刚好在客户端接收窗口内,因此客户端会正常接收这个数据报文,但是这个数据报文是上一个连接残留下来的,这样就产生数据错乱等严重的问题。

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

保证被动关闭连接一方可以正确的关闭

假设客户端(主动关闭方)最后一次 ACK 报文丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSED 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,而这时客户端已经进入到关闭状态了,在收到服务端重传的 FIN 报文后,就会回 RST 报文。服务端收到这个 RST 并将其解释为一个错误(Connection reset by peer),异常终止。

file

为了防止这种情况出现,客户端必须等待足够长的时间确保对端收到 ACK,如果对端没有收到 ACK,那么就会触发 TCP 重传机制,服务端会重新发送一个 FIN,这样一去一来刚好两个 MSL 的时间。

net.ipv4.tcp_tw_recycle

介绍

查看man手册中的介绍:tcp_tw_recycle (Boolean; default: disabled; since Linux 2.4) Enable fast recycling of TIME_WAIT sockets. Enabling this option is not recommended since this causes problems when working with NAT (Network Address Translation).

开启该参数可以快速回收TIME_WAIT状态的数据包,不建议在NAT环境中开启,可能会导致多种问题。具体是什么问题呢?

开启导致的问题

访问服务卡顿,登录服务器也会卡顿。登录后查看系统资源使用正常,ping包正常不丢失,延迟也正常。抓包显示有大量的SYN包,没有ACK,即有大量的SYN包被丢弃了。

首先查看端口是否被耗尽,这个是由net.ipv4.ip_local_port_range参数定义的:

sysctl -a | grep net.ipv4.ip_local_port_range

netstat -tuln | wc -l
ss -tuln | wc -l

file

可以确认端口是很充足的。

查看服务器内核参数,net.ipv4.tcp_timestampsnet.ipv4.tcp_tw_recycle被设置为1,都是开启的。

sysctl -a | grep net.ipv4.tcp_timestamps
net.ipv4.tcp_timestamps = 1
sysctl -a | grep net.ipv4.tcp_tw_recycle
net.ipv4.tcp_tw_recycle = 1

查看net.ipv4.tcp_timestamps参数的定义:tcp_timestamps (Boolean; default: enabled; since Linux 2.2) Enable RFC 1323 TCP timestamps.

tcp_timestamp 是 RFC1323 定义的优化选项,主要用于 TCP 连接中 RTT(Round Trip Time) 的计算,另一个是能防止序列号回绕(PAWS)。开启后,TCP 头部就会使用时间戳选项。(默认开启)

PAWS

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

防回绕序列号算法要求连接双方维护最近一次收到的数据包的时间戳(Recent TSval),每收到一个新数据包都会读取数据包中的时间戳值跟 Recent TSval 值做比较,如果发现收到的数据包中时间戳不是递增的,则表示该数据包是过期的,就会直接丢弃这个数据包。

tcp_tw_recycle是依赖tcp_timestamps参数的。开启了tcp_tw_recycle就不用等两个 MSL 就关闭连接。它的副作用是会拒绝所有比这个客户端时间戳更靠前的网络包。除非TIME-WAIT状态已经过期。

tcp报文携带了时间戳信息,在服务器看来,同一客户端的时间戳必然是线性增长的。但是在NAT环境中,客户端访问后端服务时都会通过路由器做SNAT,这样不能保证所有客户端的时间戳都是一致的,一旦有客户端断开连接,服务器就会丢弃那些时间戳较小的客户端的SYN包,这也就导致了服务卡顿。

net.ipv4.tcp_tw_reuse

也需要依赖时间戳,客户端(连接发起方) 在调用 connect() 函数时,内核会随机找一个 TIME_WAIT 状态超过 1 秒的连接给新的连接复用。那么开启后有什么问题呢?

延迟的RST报文

tcp_validate_incoming 函数是验证接收到的 TCP 报文是否合格的函数,其中第一步就会进行 PAWS 检查,由 tcp_paws_discard 函数负责。当 tcp_paws_discard 返回 true,就代表报文是一个历史报文,于是就要丢弃这个报文。但是在丢掉这个报文的时候,会先判断是不是 RST 报文,如果不是 RST 报文,才会将报文丢掉。也就是说,即使 RST 报文是一个历史报文,并不会被丢弃。

即:RST 报文的时间戳即使过期了,只要 RST 报文的序列号在对方的接收窗口内,也是能被接受的。

static bool tcp_validate_incoming(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th, int syn_inerr)
{
    struct tcp_sock *tp = tcp_sk(sk);

    /* RFC1323: H1. Apply PAWS check first. */
    if (tcp_fast_parse_options(sock_net(sk), skb, th, tp) &&
        tp->rx_opt.saw_tstamp &&
        tcp_paws_discard(sk, skb)) {
        if (!th->rst) {
            ....
            goto discard;
        }
        /* Reset is accepted even if it did not pass PAWS. */
    }

假设有这个场景:

  1. 客户端向一个还没有被服务端监听的端口发起了 HTTP 请求,接着服务端就会回 RST 报文给对方,但是 RST 报文被网络阻塞了。
  2. 由于客户端迟迟没有收到 TCP 第二次握手,于是重发了 SYN 包,与此同时服务端已经开启了服务,监听了对应的端口。于是接下来,客户端和服务端就进行了 TCP 三次握手、数据传输(HTTP应答-响应)、四次挥手。
  3. 因为客户端开启了 tcp_tw_reuse,于是快速复用 TIME_WAIT 状态的端口,又与服务端建立了一个与刚才相同的四元组的连接。
  4. 接着,前面被网络延迟 RST 报文这时抵达了客户端,而且 RST 报文的序列号在客户端的接收窗口内,由于防回绕序列号算法不会防止过期的 RST,所以 RST 报文会被客户端接受了,于是客户端的连接就断开了。服务就无法访问了。

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

那么为什么PAWS会让过期的RST报文通过,在RFC 1323中有一句:It is recommended that RST segments NOT carry timestamps, and that RST segments be acceptable regardless of their timestamp. Old duplicate RST segments should be exceedingly unlikely, and their cleanup function should take precedence over timestamps.

建议 RST 段不携带时间戳,并且无论其时间戳如何,RST 段都是可接受的。老的重复的 RST 段应该是极不可能的,因为存在2MSL,足以让连接中的报文在网络中自然消失,因此清除功能应优先于时间戳。上面的场景是开启了 tcp_tw_reuse 参数,跳过了 TIME_WAIT 状态,没有等2MSL。

综上,开启net.ipv4.tcp_tw_reuse参数会让客户端快速复用处于 TIME_WAIT 状态的端口,相当于跳过了 TIME_WAIT 状态。TCP是一个端到端的状态平衡协议。TIME_WAIT 和对端的 LAST_ACK 是一对对等状态。tcp_tw_reuse 简单粗暴的打破了这种平衡。导致一端的状态和另一端不匹配。可能会导致下面两个问题:

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

总结

在linux系统中,不同版本的内核这两个参数的默认值是不同的。3.10版本中两个参数都是0。

file

在 Linux 4.12 版本后,直接取消了net.ipv4.tcp_tw_recycle参数。5.4版本中net.ipv4.tcp_tw_reuse值为2。2代表着:enable for loopback traffic only,只有环回口的流量才启用。

file

这两个参数最好还是都不要开启,如果想达到快速回收TIME_WAIT状态的效果,还是增加更多的四元组数目,比如,服务器可用端口,或服务器IP,让服务器能容纳足够多的TIME-WAIT状态连接。减少TIME-WAIT状态的TCP连接,最有效的是使用长连接,不要用短连接,尤其是负载均衡跟web服务器之间。

0 0 投票数
文章评分
订阅评论
提醒
guest

0 评论
内联反馈
查看所有评论

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部
0
希望看到您的想法,请您发表评论x