使用kubernetes遇到最多的70%问题都可以归于网络问题,最近发现如果内核参数: bridge-nf-call-iptables
设置不当的话会影响kubernetes中Node节点上的Pod通过ClusterIP去访问同Node上的其它pod时会有超时现象,复盘记录一下排查的前因后因.
问题现象
集群环境为K8s v1.15.9,cni指定了flannel-vxlan跟portmap, kube-proxy使用mode为ipvs
问题现象是,某个Node节点上的pod通过service访问其它服务时,有些能通,有些不通,不通的都提示timeout
异常的请求:
1 | curl -v http://panorama-v2-frontend-service.spring-prod.svc.cluster.local:8080 |
这个节点上的很多pod都存在这个问题, 其它节点未发现异常.
正常请求:
1 | curl -v http://panorama-v2-frontend-service.spring-prod.svc.cluster.local:8080 |
排查过程
先对异常Node进行排查,因为其它节点是正常的,通过对异常Node的网络、kube-proxy、 iptables、ipvs规则、kubelet等排查后未发现有可疑的地方,就暂时排除嫌疑了.
第二个可疑的是DNS, coreDNS负责对service解析成ClusterIP, 在出问题的Node上经过多次测试均能正确解析,coreDNS排除嫌疑
可疑的地方都筛了一遍,无果, 那就只能再从现象来发现看看有没有相同点.
首先,访问路径为: Service – > ClusterIP – > PodIP
对service访问异常,coreDNS已经被排除了,那先绕过service,直接使用ClusterIP访问呢? 测试后现象依旧
那再绕过ClusterIP,直接使用PodIP呢? Bingo,之前会出问题的访问都是正常的了.
那么问题就出在CluterIP – > PodIP上, 那么又有以下可能:
- ClusterIP没有正确转发到PodIP上可能导致超时
- 如果正确转发,响应没有返回也可能导致超时
第一种可能性很容易排查,之前已经确认了ipvs规则、iptables规则都是没有问题的,且通过ClusterIP发起的请求可以到达PodIP上, 基本就排除了可能性一
另外,对比正常跟异常请求会发现,异常的请求原Pod跟目标pod都是在同一个Node上,而正常的请求则处于不同的Node,会是这个影响吗?
上面的可能性二,只能祭出抓包神器了tcpdump, 通过抓包发现(抓包过程见文未)会发现请求中出现了Reset
那么问题转换一下: 为什么相同Node上podA通过service/ClusterIP访问PodB响应会不返回呢,而通过PodIP访问就没问题?
补充一句就是,相同Node上的pod相互访问是不需要经过Flannel的,因此Flannel可以排除嫌疑
so, 问题在哪?
回到tcpdump的抓包数据, 可以发现,响应的数据没有按照请求的路径返回,嗯,Interesting
罪魁祸首
不管是 iptables 还是 ipvs 模式,Kubernetes 中访问 service都会进行 DNAT,将原本访问 ClusterIP:Port 的数据包 DNAT 成 service 的某个 Endpoint (PodIP:Port),然后内核将连接信息插入 conntrack 表以记录连接,目的端回包的时候内核从 conntrack 表匹配连接并SNAT,这样原路返回形成一个完整的连接链路.
从tcpdump看到请求被reset了, 没错, bridge-nf-call-iptables(如果是ipv6的话则是net.bridge.bridge-nf-call-ip6tables)参数
但是不对,这个参数linux默认开启的呢?难道是有人修改了么?
使用命令查看该参数是否开启:
1 | cat /proc/sys/net/bridge/bridge-nf-call-iptables |
返回0,说明确实没有开启(后来被证实是被同事修改了),那这个参数是如何影响的返回路径的呢?
那就不得不说linux bridge了
虽然CNI使用的是flannel, 但flannel封装的也是linux bridge,linux bridge是虚拟的二层转发设备,而 iptables conntrack 是在三层上,所以如果直接访问同一网桥内的地址(ip同一网段),就会直接走二层转发,不经过 conntrack:
结合上面的图来看,同Node通过service访问pod的访问路径如下:
- PodA 访问 service, 经过coreDNS解析成Cluster IP,不是网桥内的地址(ClusterIP一般跟PodIP不在一个网段),走Conntrack,进行DNAT,将ClusterIP转换成PodIP:Port
- DNAT 后发现是要转发到了同节点上的 PodB,PodB 回包时发现目的 IP(此时是PodA的IP) 在同一网桥上(PodA与PodB的IP段一致),就直接走二层转发了,不会去调 conntrack,这样就导致回包时没有原路返回
没有返回包就导致请求方一直等直到超时退出.
这样也解释了为何访问在其它节点的应用的ClusterIP没有问题,因为目标PodIP与源PodIP不在同一个网段上,肯定要走conntrack.
问题解决
总述,开启参数后问题解决
1 | echo "net.bridge.bridge-nf-call-iptables=1" >> /etc/sysctl.conf |
linux conntrack
关于conntrack其实也是个值得好好研究一番的知识点, 各个发行版都有工具可以看到conntrack里的记录,格式如下:
1 | # conntrack -L |
那个著名的DNS 5s timeout的问题就跟conntrack机制有关,由于篇幅有限,就不在这里展开.
tcpdump
在容器中的抓包命令
1 | tcpdump -vvv host 10.224.1.34 or 10.233.53.172 or 10.224.1.56 |
其中的三个ip分别对应podA IP, podB的ClusterIP, podB的PodIP
这里由于篇幅的关系,只保存有关键信息,同时使用注释是作者加入的,方便理解.
对于异常请求的tcpdump,如下:
1 | # podA 请求 PodB |
最后会发现PodA给PodB发送了个R Flags, 也就是reset, 就是因为当PodB返回握手确认给到PodA,PodA根本不认识这个请求,所以直接给reset掉了, 三手握手都没有建立,this is why!
而对于net.bridge.bridge-nf-call-iptables=1
的正常请求的tcpdump如下:
1 | # 能看到正常的三次握手, 这里省略 |
相信这个请求路径还是很清晰的,就不再啰嗦.
结语
禁用net.bridge.bridge-nf-call-ip6tables这个参数当然也有好外,那就是考虑同网段的IP访问没必要走conntrack,一定程度有助于性能.
kubernetes的官方文档中明确提及Node节点上需要开启这个参数,不然碰到各种诡异的现象也只是时间问题,所以还是不要随意调整。
以防后患的话可以对该参数是否开启进行监控,防止被人误修改.
参考文章:
- https://izsk.me/2020/06/10/Kubernetes-coredns-5s-timeout
- https://wiki.libvirt.org/page/Net.bridge.bridge-nf-call_and_sysctl.conf
- https://cloud.tencent.com/developer/article/1761367
- https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/
- https://imroc.cc/post/202105/why-enable-bridge-nf-call-iptables/
- https://github.com/kubernetes/kubernetes/issues/85422#issuecomment-714629138
- https://www.stackrox.com/post/2020/01/kubernetes-networking-demystified/
- https://blog.csdn.net/u010278923/article/details/81735970