美文网首页
k8s ipvs 表项超时导致tcp长连接中断

k8s ipvs 表项超时导致tcp长连接中断

作者: 分享放大价值 | 来源:发表于2021-04-17 15:29 被阅读0次

    上一篇文章分析了kube-proxy使用iptables时,由于conntrack表项超时被删除而导致tcp连接出问题,那么kube-proxy使用ipvs时,有没有类似的问题呢?这篇文章就探究此事。

    首先根据这篇文章搭建个ipvs的环境,并且使用这篇文章中使用到的service.yaml创建出需要的pod和service。拓扑如下

    image.png
    三个pod和service信息如下
    root@master:~# kubectl get pod -o wide
    NAME                      READY   STATUS    RESTARTS   AGE   IP               NODE     NOMINATED NODE   READINESS GATES
    client-797b85996c-hkp7t   1/1     Running   1          27h   172.18.219.69    master   <none>           <none>
    server-65d547c44-rb9z9    1/1     Running   1          27h   172.18.166.135   node1    <none>           <none>
    server-65d547c44-v79jq    1/1     Running   1          27h   172.18.166.134   node1    <none>           <none>
    root@master:~# kubectl get svc
    NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
    ...
    myservice    ClusterIP   10.102.54.177   <none>        2222/TCP   27h
    

    复现

    ipvs中tcp表项超时时候默认为900秒,如下

    root@master:~# ipvsadm -l --timeout
    Timeout (tcp tcpfin udp): 900 120 300
    

    为了加快复现速度,可将超时时间调整到60s

    root@master:~# ipvsadm --set 60 120 300
    root@master:~# ipvsadm -l --timeout
    Timeout (tcp tcpfin udp): 60 120 300
    

    启动几个终端,开始复现

    //启动两个server
    //terminal 1
    root@server-65d547c44-rb9z9:/# ./server
    //terminal 2
    root@server-65d547c44-v79jq:/# ./server
    //terminal 3,启动client,去连接server
    root@client-797b85996c-hkp7t:/# ./client
    please input:  --->有这个提示,说明tcp三次握手成功
    
    //terminal 4
    //查看ipvs conn表项,可看到刚才client连接server的表项,如下所示,超时时间还有55s
    //FromIP AC12DB45(172.18.219.69) 是client的pod ip
    //FPrt C5C2(50626) 是client的源port
    //ToIP 0A6636B1(10.102.54.177) service ip
    //TPrt 08AE (2222) 目的port
    //DestIP AC12A687 (172.18.166.135) 经过dnat后,实际的server pod ip
    //DPrt 08AE(2222) 目的port
    root@master:~# cat /proc/net/ip_vs_conn
    Pro FromIP   FPrt ToIP     TPrt DestIP   DPrt State       Expires PEName PEData
    TCP AC12DB45 C5C2 0A6636B1 08AE AC12A687 08AE ESTABLISHED      55
    
    //等上面表项超时被删除后,在client终端上输入字符,发送给server
    root@client-797b85996c-hkp7t:/# ./client
    please input:1
    send result: Success  --->第一次发送时,会触发对端回复rst消息
    please input:2
    send result: Connection reset by peer --->再次发送数据,就会遇到此错误
    

    原因

    从上面复现能看到,kube-proxy使用ipvs实现service ip时,如果表项超时了,也会遇到问题。这里的表项指的是动态创建的表项,可通过文件 /proc/net/ip_vs_conn 查看,而不是ipvsadm -ln看到的静态配置。

    根据这篇文章,看一下ipvs的原理,client去访问service时,会在director的INPUT链上进行DNAT处理,对应的处理函数为 ip_vs_remote_request4。

    image.png

    ip_vs_remote_request4 代码流程如下

    ip_vs_remote_request4 -> ip_vs_in
        struct ip_vs_conn *cp;
        //首先查找是否存在此连接的表项,对于新连接肯定查找不到
        cp = pp->conn_in_get(af, skb, &iph, 0);
        //如果没有表项,就需要查找ipvs的配置做dnat
        if (unlikely(!cp) && !iph.fragoffs) {
            /* No (second) fragments need to enter here, as nf_defrag_ipv6
             * replayed fragment zero will already have created the cp
             */
            int v;
            //对于tcp协议来说,调用 tcp_conn_schedule
            /* Schedule and create new connection entry into &cp */
            if (!pp->conn_schedule(af, skb, pd, &v, &cp, &iph))
                return v;
        }
        //调度结束后,cp仍为空,说明此报文不需要经过ipvs的处理,返回NF_ACCEPT,继续协议栈的处理
        if (unlikely(!cp)) {
            /* sorry, all this trouble for a no-hit :) */
            IP_VS_DBG_PKT(12, af, pp, skb, 0,
                      "ip_vs_in: packet continues traversal as normal");
            ...
            }
            return NF_ACCEPT;
        }
    

    对于满足条件的报文,执行ipvs调度,找到一个真正的server。由先if判断可知条件如下:
    a. tcp报文携带syn标志,或者sysctl_sloppy_tcp为true
    b. tcp报文不能携带rst
    c. 通过目的ip和端口号能查找到service

    static int
    tcp_conn_schedule(int af, struct sk_buff *skb, struct ip_vs_proto_data *pd,
              int *verdict, struct ip_vs_conn **cpp,
              struct ip_vs_iphdr *iph)
        if ((th->syn || sysctl_sloppy_tcp(ipvs)) && !th->rst &&
            (svc = ip_vs_service_find(net, af, skb->mark, iph->protocol,
                          &iph->daddr, th->dest))) {
            *cpp = ip_vs_schedule(svc, skb, pd, &ignored, iph);
        }
        rcu_read_unlock();
        /* NF_ACCEPT */
        return 1;
    

    我们这里遇到的问题就是因为不能满足条件a,收到的报文是不带syn的,并且sysctl_sloppy_tcp默认为false,所以tcp_conn_schedule返回1,cp仍然为空,最后返回accept,报文继续协议栈的处理。
    由上面图片可知,报文如果在INPUT链返回accept,就会上送到本机的协议栈ip_local_deliver_finish,tcp报文会被tcp_v4_rcv处理,此函数根据目的端口号查找是否有监听socket,没有的话就会返回rst报文。

    int tcp_v4_rcv(struct sk_buff *skb)
        sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
        if (!sk)
            goto no_tcp_socket;
    
    no_tcp_socket:
        tcp_v4_send_reset(NULL, skb);
    discard_it:
        /* Discard frame. */
        kfree_skb(skb);
        return 0;
    

    sloppy_tcp

    上面条件a中,如果收到的报文不带syn标志,又想让ipvs继续处理,可以使你 sloppy_tcp 功能。默认是关闭的。

    root@master:~# sysctl -a | grep sloppy_tcp
    net.ipv4.vs.sloppy_tcp = 0
    

    sloppy_tcp 在下面的patch中被引入

    //http://lkml.iu.edu/hypermail/linux/kernel/1306.1/02391.html
    This adds support for sloppy TCP and SCTP modes to IPVS.
    
    When enabled (sysctls net.ipv4.vs.sloppy_tcp and
    net.ipv4.vs.sloppy_sctp), allows IPVS to create connection state on any
    packet, not just a TCP SYN (or SCTP INIT).
    

    使能sloppy_tcp后,仍然是存在上面的问题。可通过观察 conntrack 表项看到

    [NEW] tcp      6 120 SYN_SENT src=172.18.219.69 dst=10.102.54.177 sport=38510 dport=2222 [UNREPLIED] src=172.18.166.134 dst=172.18.219.69 sport=2222 dport=38510
    [UPDATE] tcp      6 60 SYN_RECV src=172.18.219.69 dst=10.102.54.177 sport=38510 dport=2222 src=172.18.166.134 dst=172.18.219.69 sport=2222 dport=38510
    [UPDATE] tcp      6 86400 ESTABLISHED src=172.18.219.69 dst=10.102.54.177 sport=38510 dport=2222 src=172.18.166.134 dst=172.18.219.69 sport=2222 dport=38510 [ASSURED]
    
    [DESTROY] tcp      6 src=172.18.219.69 dst=10.102.54.177 sport=38510 dport=2222 src=172.18.166.134 dst=172.18.219.69 sport=2222 dport=38510 [ASSURED]
    
    //表项超时后,再发送数据,虽然报文不携带syn,但是sloppy_tcp为true,仍然会查到ipvs配置进行DNAT操作,但是目的ip为新pod,
    //所以即使经过dnat后的报文可以到达pod,也会在pod的协议栈出丢包(监听socket收到ack报文会认为报文不合法,回复rst消息)
    [NEW] tcp      6 300 ESTABLISHED src=172.18.219.69 dst=10.102.54.177 sport=38510 dport=2222 [UNREPLIED] src=172.18.166.135 dst=172.18.219.69 sport=2222 dport=38510
    [DESTROY] tcp      6 src=172.18.219.69 dst=10.102.54.177 sport=38510 dport=2222 [UNREPLIED] src=172.18.166.135 dst=172.18.219.69 sport=2222 dport=38510
    

    这次的原因和使用iptables时的原因就一致的,ipvs的director可以正常转发到后端pod,但是在有多个后面的情况下,ipvs调度算法为round-robin时,每次都会选择不同的后端pod,如果选择的pod为第一次接收ack数据,它就会认为这个报文不合法,返回rst消息。

    conntrack

    ipvs的功能本身不依赖于conntrack,但是可以在做nat转换后,同时创建conntrack表项(这个表项是经过nat转换后的),方便查看,所以上面我们能使用 conntrack -E 观察。
    那不创建conntrack表项ipvs能正常工作吗?下面就把conntrack关闭试试

    //默认是打开的
    root@master:~# sysctl -a | grep conntrack
    net.ipv4.vs.conntrack = 1
    //关闭conntrack
    root@master:~# sysctl -w net.ipv4.vs.conntrack=0
    

    关闭后,client连接server都会失败,这又是另一个问题,可能tcp三次握手都没有成功。

    分析原因
    client连接server时,仍然可以查到 ipvs 配置,并且经过了DNAT处理

    root@master:~# cat /proc/net/ip_vs_conn
    Pro FromIP   FPrt ToIP     TPrt DestIP   DPrt State       Expires PEName PEData
    TCP 0A600001 C2BC 0A600001 01BB C0A87A14 192B ESTABLISHED     877
    TCP AC12DB46 937A 0A642684 08AE AC12A6B9 08AE SYN_RECV         57
    TCP AC12DB43 EDA8 0A600001 01BB C0A87A14 192B ESTABLISHED     898
    TCP AC12DB44 AD66 0A600001 01BB C0A87A14 192B ESTABLISHED     898
    TCP 0A600001 BDEA 0A600001 01BB C0A87A14 192B ESTABLISHED     875
    

    并且可以在tunl0上抓到转换后的报文进行收发,但是pod的cali31cac956f10口抓不到报文,应该是在报文到达cali31cac956f10口之前已经被drop

    root@master:~# tcpdump -vne -i tunl0
    tcpdump: listening on tunl0, link-type RAW (Raw IP), capture size 262144 bytes
    21:22:16.901315 ip: (tos 0x0, ttl 63, id 1171, offset 0, flags [DF], proto TCP (6), length 60)
        172.18.219.70.37754 > 172.18.166.185.2222: Flags [S], cksum 0xda53 (incorrect -> 0xc11e), seq 737083861, win 64400, options [mss 1400,sackOK,TS val 4076008583 ecr 0,nop,wscale 7], length 0
    21:22:16.906601 ip: (tos 0x0, ttl 63, id 0, offset 0, flags [DF], proto TCP (6), length 60)
        172.18.166.185.2222 > 172.18.219.70.37754: Flags [S.], cksum 0x9db8 (correct), seq 848674335, ack 737083862, win 65236, options [mss 1400,sackOK,TS val 1386863794 ecr 4076008583,nop,wscale 7], length 0
    

    观察conntrack计数,invalid字段有增加,所以从server返回的报文被conntrack认为invalid

    root@master:~# cat /proc/net/stat/nf_conntrack
    cat: /proc/net/stat/nf_conntrack: No such file or directory
    root@master:~# conntrack -S
    cpu=0           found=3 invalid=94 ignore=2591855 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=33
    cpu=1           found=2 invalid=77 ignore=2414222 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=25
    cpu=2           found=3 invalid=414 ignore=2407334 insert=0 insert_failed=1 drop=1 early_drop=0 error=0 search_restart=33
    cpu=3           found=6 invalid=118 ignore=2668757 insert=0 insert_failed=2 drop=2 early_drop=0 error=0 search_restart=34
    

    原因如下,worker收到报文后,解封装vxlan,查找路由,发现不是本地报文,需要forward到 cali31cac956f10。

    在 prerouting 链上,经过conntrack的处理,在 tcp_new 中判断报文不合法

    static unsigned int get_conntrack_index(const struct tcphdr *tcph)
    {
        if (tcph->rst) return TCP_RST_SET;
        else if (tcph->syn) return (tcph->ack ? TCP_SYNACK_SET : TCP_SYN_SET);
        else if (tcph->fin) return TCP_FIN_SET;
        else if (tcph->ack) return TCP_ACK_SET;
        else return TCP_NONE_SET;
    }
    
    /* What TCP flags are set from RST/SYN/FIN/ACK. */
    enum tcp_bit_set {
        TCP_SYN_SET,
        TCP_SYNACK_SET,
        TCP_FIN_SET,
        TCP_ACK_SET,
        TCP_RST_SET,
        TCP_NONE_SET,
    };
    
    #define sNO TCP_CONNTRACK_NONE
    #define sSS TCP_CONNTRACK_SYN_SENT
    #define sSR TCP_CONNTRACK_SYN_RECV
    #define sES TCP_CONNTRACK_ESTABLISHED
    #define sFW TCP_CONNTRACK_FIN_WAIT
    #define sCW TCP_CONNTRACK_CLOSE_WAIT
    #define sLA TCP_CONNTRACK_LAST_ACK
    #define sTW TCP_CONNTRACK_TIME_WAIT
    #define sCL TCP_CONNTRACK_CLOSE
    #define sS2 TCP_CONNTRACK_SYN_SENT2
    #define sIV TCP_CONNTRACK_MAX
    #define sIG TCP_CONNTRACK_IGNORE
    
    nf_conntrack_in -> resolve_normal_ct -> init_conntrack -> l4proto->new -> tcp_new
    
        /* Don't need lock here: this conntrack not in circulation yet */
        //因为是syn+ack报文,所以 get_conntrack_index 返回 TCP_SYNACK_SET
        //tcp_conntracks[0][1] = { sIV, sIV, sSR, sIV, sIV, sIV, sIV, sIV, sIV, sSR }
        //tcp_conntracks[0][1][0] = sIV, 新状态 sIV对应 TCP_CONNTRACK_MAX
        new_state = tcp_conntracks[0][get_conntrack_index(th)][TCP_CONNTRACK_NONE];
    
        //new_state 为 TCP_CONNTRACK_MAX,所以是无效报文
        /* Invalid: delete conntrack */
        if (new_state >= TCP_CONNTRACK_MAX) {
            pr_debug("nf_ct_tcp: invalid new deleting.\n");
            return false;
        }
    
    结果是:
        skb->nfct = NULL;
        skb->nfctinfo = 0
    

    在 forward 链上,经过filter的处理,此处有如下规则,判断 ctstate 状态如果为 INVALID,就会丢包

    -A cali-tw-cali31cac956f10 -m comment --comment "cali:LRch2B0Ufmk4Riia" -m conntrack --ctstate INVALID -j DROP
    

    判断报文 ctstate 的代码

    /* Return conntrack_info and tuple hash for given skb. */
    static inline struct nf_conn *
    nf_ct_get(const struct sk_buff *skb, enum ip_conntrack_info *ctinfo)
    {
        *ctinfo = skb->nfctinfo;
        return (struct nf_conn *)skb->nfct;
    }
    
    #define XT_STATE_BIT(ctinfo) (1 << ((ctinfo)%IP_CT_IS_REPLY+1))
    #define XT_STATE_INVALID (1 << 0)
    
    #define XT_STATE_UNTRACKED (1 << (IP_CT_NUMBER + 1))
    
    static bool
    state_mt(const struct sk_buff *skb, struct xt_action_param *par)
    {
        const struct xt_state_info *sinfo = par->matchinfo;
        enum ip_conntrack_info ctinfo;
        unsigned int statebit;
        //因为在 conntrack 模块处理时,认为这个报文不合法,所以没有分配 skb->nfct 为空,这里也返回空
        struct nf_conn *ct = nf_ct_get(skb, &ctinfo);
    
        //ct为空的话,设置状态为 invalid
        if (!ct)
            statebit = XT_STATE_INVALID;
        else {
            if (nf_ct_is_untracked(ct))
                statebit = XT_STATE_UNTRACKED;
            else
                statebit = XT_STATE_BIT(ctinfo);
        }
        //规则中匹配的就是invalid,所以可以匹配成功,执行action,最终报文被drop
        return (sinfo->statemask & statebit);
    }
    

    最后总结一下,为什么 ipvs 使能conntrack时,可以正常通行,关闭conntrack就出问题呢?
    因为使能conntrack时,从client发送的第一个syn报文在经过conntrack模块时,会建立conntrack表项,从server返回的syn+ack报文,在conntrack模块可以查找到表项,并且报文是valid,后面经过filter时,就不会被drop。
    如果不是calico的环境,可能就不会设置此规则,正常环境是可以关闭conntrack的。

    相关文章

      网友评论

          本文标题:k8s ipvs 表项超时导致tcp长连接中断

          本文链接:https://www.haomeiwen.com/subject/jobnkltx.html