上一篇文章分析了kube-proxy使用iptables时,由于conntrack表项超时被删除而导致tcp连接出问题,那么kube-proxy使用ipvs时,有没有类似的问题呢?这篇文章就探究此事。
首先根据这篇文章搭建个ipvs的环境,并且使用这篇文章中使用到的service.yaml创建出需要的pod和service。拓扑如下
三个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。
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的。
网友评论