美文网首页
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