• 记录一次syn后只收到ack的情况 timewait
  • client--  proxy---server;

    四次挥手走完, client 发出syn ,proxy 代理发出syn后,收到 server的ack后就发出了rst。

    分析报文特征如下:proxy的syn的seq ()大于 server回复ack的seq?

    为什么会出现这样的问题呢?

    ------------------说到这个问题,就需要了解TCP协议栈的timewait 这个东西-----------------

     首先要明白:

    • 只有主动关闭的一方才会进入 TIME_WAIT,这既可以发生在客户端,也可以发生在服务端。
    • TIME_WAIT 会持续 2*MSL 的时间,之后进入CLOSED 状态。{PS:MSL(Maximum Segment Lifetime),意为最大报文生存时间,TTL与MSL是有关系的但不是简单的相等的关系,MSL要大于等于TTL}

     TCP中所谓的同一个连接的不同实例是指一个连接关闭后,又以相同的四元组打开了一个新的连接。

    •  一个连接因为某种原因被关闭,紧接着又以相同的四元组被重新建立。如果这时旧连接的一个延时的报文又到达了,碰巧这个延时的报文段的序列号对新连接又是有效的,那么这个报文就会被视为有效的数据进入新连接的数据流中
    • 同时如果Timewait发出的ack 丢失, Last_ack状态下的server 会重发Fin,此时Timewait收到Fin就会回复ack,这样最后一个ack丢失,触发重传后因为存在Timewait socket也会正常走完四次挥手

    所以:TIME_WAIT 的目的

    • 可靠的实现 TCP 全双工连接的终止。
    • 让老的重复的报文段在网络中消失。

    TIME_WAIT 造成的影响

    • TIME_WAIT 会长时间占用(2*MSL)一个四元组连接,这可能导致后续相同元组的连接创建失败。
    • 2*MSL 期间内核需要维持该 SOCKET 的数据结构,因此数量过多的话会消耗内存、并且增加内核遍历有关 hash table 的时间(更消耗CPU),从而导致性能问题。

     

    目前各个操作系统提供了绕过TIME_WAIT的方法, linux 下最经典的就是 tcp_tw_reuse 以及 tcp_tw_recycle ,此参数都依赖tcp_timestamps

    RFC1323 引入了 timestamp 时间戳选项,主要为了精确计算 RTT(ROUND-TRIP TIME) 。时间戳的另一个功用是为了防止序列号回绕

    net.ipv4.tcp_tw_reuse 定义:

    tcp_tw_reuse - INTEGER
        Enable reuse of TIME-WAIT sockets for new connections when it is
        safe from protocol viewpoint.
        0 - disable
        1 - global enable
        2 - enable for loopback traffic only
        It should not be changed without advice/request of technical
        experts.
        Default: 2 

     tcp_tw_reuse的使用:

    /* called with local bh disabled */
    static int __inet_check_established(struct inet_timewait_death_row *death_row,
                        struct sock *sk, __u16 lport,
                        struct inet_timewait_sock **twp)
    {
        -----------------------------------
        /* Check TIME-WAIT sockets first. */
        sk_nulls_for_each(sk2, node, &head->twchain) {
            tw = inet_twsk(sk2);
    
            if (INET_TW_MATCH(sk2, net, hash, acookie,
                        saddr, daddr, ports, dif)) {
                if (twsk_unique(sk, sk2, twp))
                    goto unique;
                else
                    goto not_unique;
            }
        }
    ------------------------------
    }
    static inline int twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
    {
        if (sk->sk_prot->twsk_prot->twsk_unique != NULL)
            return sk->sk_prot->twsk_prot->twsk_unique(sk, sktw, twp);
        return 0;
    }
    int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
    {
        const struct tcp_timewait_sock *tcptw = tcp_twsk(sktw);
        struct tcp_sock *tp = tcp_sk(sk);
    
        /* With PAWS, it is safe from the viewpoint
           of data integrity. Even without PAWS it is safe provided sequence
           spaces do not overlap i.e. at data rates <= 80Mbit/sec.
    
           Actually, the idea is close to VJ's one, only timestamp cache is
           held not per host, but per port pair and TW bucket is used as state
           holder.
    
           If TW bucket has been already destroyed we fall back to VJ's scheme
           and use initial timestamp retrieved from peer table.
         */
        if (tcptw->tw_ts_recent_stamp &&
            (twp == NULL || (sysctl_tcp_tw_reuse &&
                     get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
            tp->write_seq = tcptw->tw_snd_nxt + 65535 + 2;
            if (tp->write_seq == 0)
                tp->write_seq = 1;
            tp->rx_opt.ts_recent       = tcptw->tw_ts_recent;
            tp->rx_opt.ts_recent_stamp = tcptw->tw_ts_recent_stamp;
            sock_hold(sktw);
            return 1;
        }
    
        return 0;
    }

     启用net.ipv4.tcp_tw_reuse后,如果新的时间戳比之前存储的时间戳更大, 才会被认为是safe from protocol viewpoint

     根据代码可知: tcp_tw_reuse 解决的是client 端connect remote ip 发出syn 报文时,loop up local port的问题;也就是对client 客户端生效

    tcp_tw_recycle

     这个参数在 Linux 内核 4.12 中已经被废弃,但对于老版本内核还是需要分析一下。

      An additional mechanism could be added to the TCP, a per-host cache of the last timestamp received from any connection. This value could then be used in the PAWS mechanism to reject old duplicate segments from earlier incarnations of the connection, if the timestamp clock can be guaranteed to have ticked at least once since the old connection was open. This would require that the TIME-WAIT delay plus the RTT together must be at least one tick of the sender's timestamp clock. Such an extension is not part of the proposal of this RFC.

       TCP 中用 cache 针对每个 host 记录任何连接最后的接收时间戳;是每个host 不是每个链接

       开启了这个配置后,内核会快速的回收处于 TIME_WAIT 状态的socket 连接。多快?不再是 2*MSL,而是一个RTO(retransmission timeout,数据包重传的timeout时间)的时间;对出站连接和入站连接均有影响。

    /*
     * Move a socket to time-wait or dead fin-wait-2 state.
     */
    void tcp_time_wait(struct sock *sk, int state, int timeo)
    {
        struct inet_timewait_sock *tw = NULL;
        const struct inet_connection_sock *icsk = inet_csk(sk);
        const struct tcp_sock *tp = tcp_sk(sk);
        int recycle_ok = 0;
    
        if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
            recycle_ok = tcp_remember_stamp(sk);
    
        ---------------------
            /* Linkage updates. */
            __inet_twsk_hashdance(tw, sk, &tcp_hashinfo);
    
            /* Get the TIME_WAIT timeout firing. */
            if (timeo < rto)
                timeo = rto;
    
            if (recycle_ok) {
                tw->tw_timeout = rto;
            } else {
                tw->tw_timeout = TCP_TIMEWAIT_LEN;
                if (state == TCP_TIME_WAIT)
                    timeo = TCP_TIMEWAIT_LEN;
            }
    
            inet_twsk_schedule(tw, &tcp_death_row, timeo,
                       TCP_TIMEWAIT_LEN);
            inet_twsk_put(tw);
        -----------------------------------
    }

    可以看到 只要开启timestamp 以及tcp_tw_recycle,其回收destroy Timewait socket state 大约就是 rto;

      同时开启 tcp_tw_recycle  linux3.x协议栈会丢弃所有来自远端的 timestramp 时间戳小于上次记录的时间戳(由同一个远端发送的)的任何数据包。也就是说要使用该选项,则必须保证数据包的时间戳是单调递增的,所以这对NAT 是一个致命的伤害, 所有终端 经过NAT后,终端的src ip 一样但是其 timestamp不一样,导致部分syn 被丢弃

    int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
    {//收到syn后 准备调用conn_request 发送syn+ack,但在发送前会进行校验, 校验不过则丢弃此syn
    ----------------------------------
            struct inet_peer *peer = NULL;
    
            /* VJ's idea. We save last timestamp seen
             * from the destination in peer table, when entering
             * state TIME-WAIT, and check against it before
             * accepting new connection request.
             *
             * If "isn" is not zero, this request hit alive
             * timewait bucket, so that all the necessary checks
             * are made in the function processing timewait state.
             */
            if (tmp_opt.saw_tstamp &&
                tcp_death_row.sysctl_tw_recycle &&
                (dst = inet_csk_route_req(sk, req)) != NULL &&
                (peer = rt_get_peer((struct rtable *)dst)) != NULL &&
                peer->daddr.addr.a4 == saddr) {
                inet_peer_refcheck(peer);
                if ((u32)get_seconds() - peer->tcp_ts_stamp < TCP_PAWS_MSL &&//该源ip的上次tcp通讯发生在60s
                    (s32)(peer->tcp_ts - req->ts_recent) >//该条件判断表示该源ip的上次tcp通讯的timestamp 大于 本次tcp。
                                TCP_PAWS_WINDOW) {
                    NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
                    goto drop_and_release;
                }
            }
        
    
        if (tcp_v4_send_synack(sk, dst, req,
                       (struct request_values *)&tmp_ext) ||
            want_cookie)
        ----------------------------------------
    }

       Linux 有个内核参数 tcp_max_tw_buckets控制并发的 TIME_WAIT 的数量,如果超限,那么,系统会把多的给 destory 掉,然后在日志里打一个警告(如:time wait bucket table overflow),这相当于跳过了 TIME_WAIT 直接将连接销毁。如果对端(被动关闭方)没有收到最后的 ACK,那么会重发 FIN 报文,因为主动端服务器已没有相关的连接信息,服务器收到 FIN 后会回一个 RST 报文将连接重置,到这里还没有什么大问题,只是 TCP 连接没有优雅的关闭而已

    So:如何去避免 TIME_WAIT ?

    • 永远不要主动断开连接,让客户端去断开
    • 如果服务端既要建立出站连接又要建立入站连接,法则是:如果 TIME_WAIT 注定要发生,那么就让它发生在对端。如果对端超时了,直接使用 RST 终止连接。但你服务端也要避免发起频繁的短时连接,尽量使用长连接或者连接池
    • 在应用层加入一个“关闭” ”确认再见“ 然后 服务端/客户端直接发送RST 终止链接

    https://juejin.cn/post/6844903730874171405

    https://juejin.cn/post/6844903730874171405

    http://liupzmin.com/2020/02/26/network/tcp-time-wait/

    https://club.perfma.com/article/1992143?type=parent&last=1994719

    https://club.perfma.com/article/1992143

    https://www.cnblogs.com/sunsky303/p/12818009.html

     https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux

  • 相关阅读:
    Spring中的@Transactional(rollbackFor = Exception.class)属性详解
    Kubernetes 升级记录:从 1.16.3 升级至 1.17.0
    k8s 安装 prometheus 过程记录
    覆盖索引、联合索引、索引下推
    如何保障 API 接口的安全性?
    UDP和TCP是网络通讯
    HTTPS
    Kubernetes Ingress API Ingress资源通过允许API网关样式的流量路由
    30条黄金法则
    工作流
  • 【推广】 阿里云小站-上云优惠聚集地(新老客户同享)更有每天限时秒杀!
    【推广】 云服务器低至0.95折 1核2G ECS云服务器8.1元/月
    【推广】 阿里云老用户升级四重礼遇享6.5折限时折扣!
  • 原文地址:https://www.cnblogs.com/codestack/p/14047996.html
走看看 - 开发者的网上家园