miller
发布于

被抛弃的 tcp_recycle

原文地址 juejin.cn

本文从一次巧合发现高版本 Linux 不再支持 tcp_tw_recycle,深入研究了连接状态 TIME_WAIT 的原理,进而分析了 tcp_tw 族内核参数和如何应用它们对 Linux 的连接进行调优。

1、背景

最近准备搭建一个新的 kubernetes 集群,将内核从 3.18 更新到了 4.14 版本,并执行一些常规的优化操作。在执行 sysctl -p 操作时突然报错如下:

sysctl: cannot stat /proc/sys/net/ipv4/tcp_tw_recycle: No such file or directory复制代码

2、问题原因

Linux 从 4.12 内核版本开始移除了 tcp_tw_recycle 配置。

参考:[1]tcp:remove tcp_tw_recycle 4396e460

移除 sysctl.conf 中关于 net.ipv4.tcp_tw_recycle 的配置内容,再次尝试 sysctl -p 就不再提示报错了。

3、深入解析

tcp_tw_recycle 通常会和 tcp_tw_reuse 参数一起使用,用于解决服务器 TIME_WAIT 状态连接过多的问题。

3.1、TIME_WAIT 状态出现原因与查看

让我们回顾一下四次挥手的流程:

TIME_WAIT 永远是出现在主动发送断开连接请求的一方 (下文中我们称之为客户),划重点:这一点面试的时候经常会被问到。

客户在收到服务器端发送的 FIN(表示 "我们也要断开连接了") 后发送 ACK 报文,并且进入 TIME_WAIT 状态,等待 2MSL(MaximumSegmentLifetime 最大报文生存时间)。对于 Linux,字段为 TCP_TIMEWAIT_LEN 硬编码为 30 秒,对于 windows 为 2 分钟 (可自行调整)。

为什么客户端不直接进入 CLOSED 状态,而是要在 TIME_WAIT 等待那么久呢,基于如下考虑:

  1. 确保远程端处于关闭状态。也就是说需要确保客户端发出的最后一个 ACK 报文能够到达服务器。由于网络不可靠,有可能最后一个 ACK 报文丢失,如果服务器没有收到客户端的 ACK,则会重新发送 FIN 报文,客户端就可以在 2MSL 时间段内收到这个这个重发的报文,并且重发 ACK 报文。但如果客户端跳过 TIME_WAIT 阶段进入了 CLOSED,服务端始终无法得到响应,就会处于 LAST-ACK 状态,此时假如客户端发起了一个新连接,则会以失败告终。

异常流程如下:

  1. 防止上一次连接中的包,迷路后重新出现,影响新连接 (经过 2MSL, 上一次连接中所有的重复包都会消失),这一点和为啥要执行三次握手而不是两次的原因是一样的。

异常流程如下:

查看方式有两种:

(1)ss -tan state time-wait|wc -l

(2)netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

3.2、TIME_WAIT 的危害

对于一个处理大量连接的处理器 TIME_WAIT 是有危害的,表现如下:

  1. 占用连接资源

TIME_WAIT 占用的 1 分钟时间内,相同四元组 (源地址,源端口,目标地址,目标端口) 的连接无法创建,通常一个 ip 可以开启的端口为 net.ipv4.ip_local_port_range 指定的 32768-61000,如果 TIME_WAIT 状态过多,会导致无法创建新连接。

  1. 占用内存资源

这个占用资源并不是很多,可以不用担心。

3.3、TIME_WAIT 的解决

可以考虑如下方式:

  1. 修改为长连接,代价较大,长连接对服务器性能有影响。

  2. 增加可用端口范围 (修改 net.ipv4.ip_local_port_range); 增加服务端口,比如采用 80,81 等多个端口提供服务; 增加客户端 ip(适用于负载均衡,比如 nginx,采用多个 ip 连接后端服务器); 增加服务端 ip; 这些方式治标不治本,只能缓解问题。

  3. 将 net.ipv4.tcp_max_tw_buckets 设置为很小的值 (默认是 18000). 当 TIME_WAIT 连接数量达到给定的值时,所有的 TIME_WAIT 连接会被立刻清除,并打印警告信息。但这种粗暴的清理掉所有的连接,意味着有些连接并没有成功等待 2MSL,就会造成通讯异常。

  4. 修改 TCP_TIMEWAIT_LEN 值,减少等待时间,但这个需要修改内核并重新编译。

  5. 打开 tcp_tw_recycle 和 tcp_timestamps 选项。

  6. 打开 tcp_tw_reuse 和 tcp_timestamps 选项。

3.4、net.ipv4.tcp_tw_

需要明确两个点:

解决方式已经给出,那我们需要了解一下 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_tw_recycle 有啥区别

  1. 两个选项都需要打开对 TCP 时间戳的支持,即 net.ipv4.tcp_timestamps=1(默认即为 1)。

RFC 1323 中实现了 TCP 拓展规范,以便保证网络繁忙的情况下的高可用。并定义了一个新的 TCP 选项 - 两个四字节的 timestamp 字段,第一个是 TCP 发送方的当前时钟时间戳,第二个是从远程主机接收到的最新时间戳。

  1. 两个选项默认都是关闭状态,即等于 0。

3.4.1 - net.ipv4.tcp_tw_reuse:更安全的设置

将处于 TIME_WAIT 状态的 socket 用于新的 TCP 连接,影响连出的连接。

[2]kernel sysctl 官方指南中是这么写的:

Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint. Default value is 0.

It should not be changed without advice/request of technical experts.

协议安全主要指的是两点:

  1. 只适用于客户端 (连接发起方)

net/ipv4/inet_hashtables.c

static int __inet_check_established(struct inet_timewait_death_row *death_row,
                    struct sock *sk, __u16 lport,
                    struct inet_timewait_sock **twp)
{
    /* ……省略…… */
    sk_nulls_for_each(sk2, node, &head->chain) {
            if (sk2->sk_hash != hash)
                        continue;
                        
            if (likely(INET_MATCH(sk2, net, acookie,
                    saddr, daddr, ports, dif))) {
                        if (sk2->sk_state == TCP_TIME_WAIT) {
                            tw = inet_twsk(sk2);
                            if (twsk_unique(sk, sk2, twp))
                                break;
            }
            goto not_unique;
        }
    }
    /* ……省略…… */
}复制代码

2.TIME_WAIT 创建时间超过 1 秒才可以被复用

net/ipv4/tcp_ipv4.c

int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
{
    /* ……省略…… */
    if (tcptw->tw_ts_recent_stamp &&
        (!twp || (sock_net(sk)->ipv4.sysctl_tcp_tw_reuse &&
         get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
         /* ……省略…… */
         return 1;
    }
    return 0;
}复制代码

满足以上两个条件才会被认为是 "safe from protocol viewpoint" 的状况。启用 net.ipv4.tcp_tw_reuse 后,如果新的时间戳比之前存储的时间戳更大,那么 Linux 将会从 TIME-WAIT 状态的存活连接中选取一个,重新分配给新的连接出去的的 TCP 连接,这种情况下,TIME-WAIT 的连接相当于只需要 1 秒就可以被复用了。

重新回顾为什么要引入 TIME-WAIT:

第一个作用就是避免新连接接收到重复的数据包,由于使用了时间戳,重复的数据包会因为时间戳过期被丢弃。

第二个作用是确保远端不是处于 LAST-ACK 状态,如果 ACK 包丢失,远端没有成功获取到最后一个 ACK 包,则会重发 FIN 包。直到:

  1. 放弃 (连接断开)

  2. 收到 ACK 包

  3. 收到 RST 包

如果 FIN 包被及时接收到,并且本地端仍然是 TIME-WAIT 状态,那 ACK 包会被发送,此时就是正常的四次挥手流程。

如果 TIME-WAIT 的条目已经被新连接所复用,则新连接的 SYN 包会被忽略掉,并且会收到 FIN 包的重传,本地会回复一个 RST 包 (因为此时本地连接为 SYN-SENT 状态),这会让远程端跳出 LAST-ACK 状态,最初的 SYN 包也会在 1 秒后重新发送,然后完成连接的建立,整个过程不会中断,只是有轻微的延迟。流程如下:

需要注意,连接被复用后,TWrecycled 计数器会增加 (/proc/net/netstat 中 TWrecycled 值)

3.4.2 - net.ipv4.tcp_tw_recycle:更激进的设置

启用 TIME_WAIT 状态的 sockets 的快速回收,影响所有连入和连出的连接

[3]kernel sysctl 官方指南是这么写的:

Enable fast recycling TIME-WAIT sockets. Default value is 0. It should not be changed without advice/request of technical experts.

这次表述的更加模糊,继续翻看源码:

net/ipv4/tcp_input.c

int tcp_conn_request(struct request_sock_ops *rsk_ops,
            const struct tcp_request_sock_ops *af_ops,
            struct sock *sk, struct sk_buff *skb)
{
 /* ……省略…… */
 if (!want_cookie && !isn) {
     /* ……省略…… */
     if (net->ipv4.tcp_death_row.sysctl_tw_recycle) {
         bool strict;

dst = af_ops->route_req(sk, &fl, req, &strict);
 
if (dst && strict &&
              !tcp_peer_is_proven(req, dst, true,
                      tmp_opt.saw_tstamp)) {
              NET_INC_STATS(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
              goto drop_and_release;
       }
     }
     /* ……省略…… */
     isn = af_ops->init_seq(skb, &tcp_rsk(req)->ts_off);
   }
/* ……省略…… */

drop_and_release:
            dst_release(dst);
       drop_and_free:
            reqsk_free(req);
       drop:
            tcp_listendrop(sk);
            return 0;
}复制代码

简单来说就是,Linux 会丢弃所有来自远端的 timestramp 时间戳小于上次记录的时间戳 (由同一个远端发送的) 的任何数据包。也就是说要使用该选项,则必须保证数据包的时间戳是单调递增的。

问题在于,此处的时间戳并不是我们通常意义上面的绝对时间,而是一个相对时间。很多情况下,我们是没法保证时间戳单调递增的,比如使用了 nat,lvs 等情况。

而这也是很多优化文章中并没有提及的一点,大部分文章都是简单的推荐将 net.ipv4.tcp_tw_recycle 设置为 1,却忽略了该选项的局限性,最终造成严重的后果 (比如我们之前就遇到过部署在 nat 后端的业务网站有的用户访问没有问题,但有的用户就是打不开网页)。

3.5、被抛弃的 tcp_tw_recycle

如果说之前内核中 tcp_tw_recycle 仅仅不适用于 nat 和 lvs 环境,那么从 4.10 内核开始,官方修改了时间戳的生成机制。

参考:[4]tcp: randomize tcp timestamp offsets for each connection 95a22ca

在这种情况下,无论任何时候,tcp_tw_recycle 都不应该开启。故被抛弃也是理所应当的了。

4、总结

  • tcp_tw_recycle 选项在 4.10 内核之前还只是不适用于 NAT/LB 的情况 (其他情况下,我们也非常不推荐开启该选项),但 4.10 内核后彻底没有了用武之地,并且在 4.12 内核中被移除.

  • tcp_tw_reuse 选项仍然可用。在服务器上面,启用该选项对于连入的 TCP 连接来说不起作用,但是对于客户端 (比如服务器上面某个服务以客户端形式运行,比如 nginx 反向代理) 等是一个可以考虑的方案。

  • 修改 TCP_TIMEWAIT_LEN 是非常不建议的行为。

5、参考链接

[1]https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4396e46187ca5070219b81773c4e65088dac50cc

[2]https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/networking/ip-sysctl.txt?h=v4.11#n648

[3]https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/networking/ip-sysctl.txt?h=v4.11#n643

[4]https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=95a22caee396cef0bb2ca8fafdd82966a49367bb

[5]Coping with the TCP TIME-WAIT state on busy Linux servers:https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux

[6]net.ipv4.tcp_tw_recycle は廃止されました ― その危険性を理解する:https://qiita.com/tmshn/items/b49f1b51bfc472968b30

[7]tcp_tw_reuse、tcp_tw_recycle 使用场景及注意事项:https://www.cnblogs.com/lulu/p/4149312.html

本文首发于公众号 “小米运维”,点击查看原文

浏览 (881)
点赞
收藏
评论