作者简介
姚灿武,SUSE Rancher 研发工程师,拥有 6 年云计算领域经验,热衷开源技术,在云原生相关技术领域拥有丰富的开发和实践经验。
Harvester 通过 Multus 扩展了标准的 Kubernetes CNI 网络,可以让虚拟机拥有基于 Bridge Vlan 技术分配的虚拟网卡。本文源于一次问题排查实践,以解决复杂网络情况下产生的通信问题。
本文使用的 Harvester 版本为 v1.0.0
问题描述
Harvester 利用 Kubernetes service 为虚拟机中的服务提供负载均衡。在这个方案中,负载均衡后端地址是<虚拟机的 IP 地址:端口>,被记录在与 Kubernetes service 对应的 endpointslice 中。示意图表示如下:
下面是本文所使用的例子对应的 service 与 endpointslice。
harvester-host:/home/rancher # kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default-nginx-lb-db9bdca5 LoadBalancer 10.43.113.238 80:32586/TCP 4h32m
harvester-host:/home/rancher # kubectl get endpointslices
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
default-nginx-lb-db9bdca5 IPv4 80 172.16.178.178 4h33m
复制代码
但是,我们发现,当 VM 使用 Harvester VLAN 网络,并且发起请求的客户端(如 curl)与 VM 同在一个 Harvester 主机时,通过负载均衡(本例中是访问 service clusterIP)的请求失败了。结果如下:
harvester-host:/home/rancher # curl 10.43.113.238
curl: (7) Failed to connect to 10.53.202.161 port 80: Connection timed out
复制代码
整个网络拓扑表示如下:
分析过程
明确流量路径
一般来说,对于网络问题,我们首先需要明确流量的转发路径。在 Kubernetes 中,当请求 service clusterIP 时,Kube-proxy 会将请求目的地址转为后端服务的地址。在我们的案例中,后端地址是 172.16.178.178:80
。因为目的地址172.16.178.178
是在 VLAN 178 里,请求和响应都需要经过外部网关。因此,我们可以在网络拓扑中标记上流量转发路径。
抓包
不通过负载均衡,在各个 VLAN 网络中直接访问后端服务是通的,我们可以首先排除是外部交换机以及网关引发的问题。为了定位到是在哪个转发环节发生的问题,我们需要对 Harvester 主机流量路径上的各个网络接口进行抓包。我们将抓包结果整理后简化表示如下:
从抓包结果可以看出,VM 网卡正常接收到了 TCP SYN 网络包,并且响应发送了 SYN/ACK 报文。但是,当 SYN/ACK 报文被网桥从 veth2db2ad9c
转发到 eth1
后,目的端口发生了改变。因为该目的端口与 SYN 报文的源端口不匹配,SYN/ACK 报文被丢弃导致 TCP 三次握手失败。由 conntrack
表中的记录可以看出,修改后的目的端口是负载均衡之前原方向请求的源端口。
harvester-host:/home/rancher # conntrack -L | grep 172.16.178.178
tcp 6 56 SYN_RECV src=172.16.0.57 dst=10.43.113.238 sport=38944 dport=80 src=172.16.178.178 dst=172.16.0.57 sport=80 dport=10598 mark=0 use=1
conntrack v1.4.5 (conntrack-tools): 262 flow entries have been shown.
复制代码
因此,我们合理猜测在网桥转发 SYN/ACK 报文的过程中发生了一次网络地址转换(NAT)。
根据 Kubernetes 官方文档描述,Kubernetes 默认开启了net.bridge.bridge-nf-call-iptables
设置。
if the plugin connects containers to a Linux bridge, the plugin must set the net/bridge/bridge-nf-call-iptables sysctl to 1 to ensure that the iptables proxy functions correctly.
复制代码
该设置可以控制当报文经过网桥时,原来作用于三层网络的 iptables 规则是否在此二层转发过程中生效。在 Kubernetes 中,默认生效。也就是说,Kube-proxy 所设置的 iptables 规则包括一些 NAT 规则都会在网桥转发过程中起作用。
当我们设置 net.bridge.bridge-nf-call-iptables
为 0 时,我们发现请求成功了。所以,毫无疑问,kube-proxy 的 iptables 规则导致了这个问题。但是,到目前为止,我们仍然无法定位具体是哪一条规则,或者说我们还没有定位 netfilter 中哪个环节导致了该问题。我们需要深入研究分析 netfilter 才能找到问题背后的根本原因。
深入分析 netfilter NAT
在网络分析工具 pwru 的帮助下,通过观察目的端口的变化以及打印出内核函数调用栈,我们可以确定 NAT 发生在 pre-routing 链上。
0xffff9e90d1c55200 [<empty>] skb_ensure_writable 16542012456363 netns=4026531992 mark=0x0 ifindex=10 proto=8 mtu=1500 len=60 172.16.178.178:80->172.16.0.57:10598(tcp)
0xffff9e90d1c55200 [<empty>] inet_proto_csum_replace4 16542012502740 netns=4026531992 mark=0x0 ifindex=10 proto=8 mtu=1500 len=60 172.16.178.178:80->172.16.0.57:38944(tcp)
复制代码
# stack of the function skb_ensure_writable
0xffff9e90c12a8d00 [ksoftirqd/5] skb_ensure_writable 16558140061379 netns=4026531992 mark=0x0 ifindex=10 proto=8 mtu=1500 len=60 172.16.178.178:80->172.16.0.57:10598(tcp)
skb_ensure_writable
l4proto_manip_pkt [nf_nat]
nf_nat_ipv4_manip_pkt [nf_nat]
nf_nat_manip_pkt [nf_nat]
nf_nat_ipv4_pre_routing [nf_nat]
nf_hook_slow
br_nf_pre_routing [br_netfilter]
br_handle_frame [bridge]
__netif_receive_skb_core
__netif_receive_skb_one_core
process_backlog
__napi_poll
net_rx_action
__softirqentry_text_start
run_ksoftirqd
smpboot_thread_fn
kthread
ret_from_fork
# stack of the function inet_proto_csum_replace4
0xffff9e90c12a8d00 [ksoftirqd/5] inet_proto_csum_replace4 16558140095491 netns=4026531992 mark=0x0 ifindex=10 proto=8 mtu=1500 len=60 172.16.178.178:80->172.16.0.57:38944(tcp)
inet_proto_csum_replace4
l4proto_manip_pkt [nf_nat]
nf_nat_ipv4_manip_pkt [nf_nat]
nf_nat_manip_pkt [nf_nat]
nf_nat_ipv4_pre_routing [nf_nat]
nf_hook_slow
br_nf_pre_routing [br_netfilter]
br_handle_frame [bridge]
__netif_receive_skb_core
__netif_receive_skb_one_core
process_backlog
__napi_poll
net_rx_action
__softirqentry_text_start
run_ksoftirqd
smpboot_thread_fn
kthread
ret_from_fork
复制代码
通过 Linux 内核中的 netfilter 源码以及 netfilter 在三层协议上的结构图,我们可以看到,NF_IP_PRI_NAT_DST
参数表明,pre-routing 链上的 NAT 钩子只会改变报文的目的地址,即在 pre-routing 链上只会发生 DNAT 或者 de-SNAT。
static const struct nf_hook_ops nf_nat_ipv4_ops[] = {
{
.hook = ipt_do_table,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_NAT_DST,
},
...
}
复制代码
在我们的案例中,SYN/ACK 是回包,所以应当是发生了 de-SNAT。Kube-proxy 在 post-routing 链上添加了 SNAT iptables 规则。请求 service 的报文经过时会在 output 链上打上标记,打上标记的报文在 post-chain 上会发生 SNAT。回包时,报文会在 pre-routing 链上发生 de-SNAT。
-A KUBE-SVC-2CMXP7HKUVJN7L6M ! -s 10.42.0.0/16 -d 10.43.220.155/32 -p tcp -m comment --comment "default/nginx cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
复制代码
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
复制代码
在 Kubernetes 中,通常客户端请求与后端服务实例在同一个主机节点上,请求不会经过 KUBE-MART-MASQ 链,也就不会发生 SNAT 和 de-SNAT。但是不知道为什么在这个案例中发生了。通过查阅 Kubernetes 官方文档,确定 kube-proxy 的配置项 cluster-cidr
是罪魁祸首。
--cluster-cidr string
The CIDR range of pods in the cluster. When configured, traffic sent to a Service cluster IP from outside this range will be masqueraded and traffic sent from pods to an external LoadBalancer IP will be directed to the respective cluster IP instead
复制代码
将 cluster-cidr
设置为空,请求成功。
解决方案
从上面的分析中可以知道,解决问题的关键是避免发生不必要的 SNAT。有两个可选的解决方案。
因为 Harvester 所使用的 canal CNI 不依赖 bridge netfilter,我们可以直接关闭 net.bridge.bridge-nf-call-iptables
。
将 kube-proxy cluster-cidr
配置为空。
参考文献
https://kubernetes.io/docs/reference/command-line-tools-reference/kube-proxy/
https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#network-plugin-requirements
https://wiki.libvirt.org/page/Net.bridge.bridge-nf-call_and_sysctl.conf
https://segmentfault.com/a/1190000041259845
https://serenafeng.github.io/2020/03/26/kube-proxy-in-iptables-mode/
https://github.com/projectcalico/calico/issues/2999
评论