问题背景 K8S环境下,集群外的设备通过NodePort方式频繁发送UDP请求到集群内的某个Pod,当Pod因为升级或异常重启时,出现流量中断的现象。
分析过程 构造K8s集群:
1 2 3 [root@node]# kubectl get node -owide NAME STATUS ROLES VERSION INTERNAL-IP node Ready master v1.15.12 10.10.212.164
\部署一个通过NodePort暴露的UDP服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 apiVersion: apps/v1 kind: Deployment metadata: name: dao spec: replicas: 1 selector: matchLabels: app: dao template: metadata: labels: app: dao spec: containers: - image: samwelkey24/dao-2048:1.0 name: dao --- apiVersion: v1 kind: Service metadata: name: dao labels: app: dao spec: type: NodePort ports: - port: 80 targetPort: 80 name: tcp - port: 8080 targetPort: 8080 nodePort: 30030 name: udp protocol: UDP selector: app: dao
使用nc命令模拟客户端频繁向集群外发送udp包:
1 [root@node]# while true; do echo "test" | nc -4u 10.10.212.164 30030 -p 9999;done
在Pod网卡和主机网卡上抓包,请求都正常:
1 2 3 4 5 6 7 [root@node]# tcpdump -n -i cali1bd5e5bd67b port 8080 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 17:39:50.543529 IP 10.10.212.164.7156 > 177.177.241.159.webcache: UDP, length 5 17:39:50.553849 IP 10.10.212.164.7156 > 177.177.241.159.webcache: UDP, length 5 17:39:50.565139 IP 10.10.212.164.7156 > 177.177.241.159.webcache: UDP, length 5 17:39:50.576749 IP 10.10.212.164.7156 > 177.177.241.159.webcache: UDP, length 5 17:39:50.587671 IP 10.10.212.164.7156 > 177.177.241.159.webcache: UDP, length 5
1 2 3 4 5 6 7 [root@node]# tcpdump -n -i eth0 port 30030 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes 17:43:10.470136 IP 10.10.212.167.distinct > 10.10.212.164.30030: UDP, length 5 17:43:10.481007 IP 10.10.212.167.distinct > 10.10.212.164.30030: UDP, length 5 17:43:10.491607 IP 10.10.212.167.distinct > 10.10.212.164.30030: UDP, length 5 17:43:10.502879 IP 10.10.212.167.distinct > 10.10.212.164.30030: UDP, length 5
通过删除Pod构造重启:
1 2 3 4 5 [root@node]# kubectl get pod -n allkinds -owide NAME READY STATUS RESTARTS AGE IP NODE dao-5f7669bc69-kkfk5 1/1 Running 0 18m 177.177.241.159 node [root@node]# kubectl delete pod dao-5f7669bc69-kkfk5
Pod重启后,抓包发现Pod无法再接收UDP包:
1 2 3 [root@node]# tcpdump -n -i cali1bd5e5bd67b port 8080 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode ^
在Pod所在节点网卡上可以抓到包,说明请求已到达节点上:
1 2 3 4 5 6 7 [root@node]# tcpdump -n -i eth0 port 30030 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes 17:55:08.173773 IP 10.10.212.167.distinct > 10.10.212.164.30030: UDP, length 5 17:55:08.187789 IP 10.10.212.167.distinct > 10.10.212.164.30030: UDP, length 5 17:55:08.201551 IP 10.10.212.167.distinct > 10.10.212.164.30030: UDP, length 5 17:55:08.212789 IP 10.10.212.167.distinct > 10.10.212.164.30030: UDP, length 5
继续通过trace iptables跟踪请求的走向,观察到流量没有经过PREROUTING表的nat链,之后也没有按预期的方向走到FORWARD链,而是走到了INPUT链,继续往上层协议栈,从这个现象可以推测是DNAT出了问题;
根据netfilter原理图可以知道,DNAT跟conntrack表有关:
查看指定NodePort端口的conntrack条目,确认是表项问题:
1 2 3 4 5 6 7 正常表项: [root@node]# cat /pro/net/nf_contrack |grep 30030 ipv4 2 udp 17 29 src=10.10.212.167 dst=10.10.212.164 sport=9999 dport=30030 [UNREPLIED] src=177.177.241.159 dst=10.10.212.164 sport=8080 dport=9999 mark=0 zone=0 use=2 异常表项: [root@node]# cat /pro/net/nf_contrack |grep 30030 ipv4 2 udp 17 29 src=10.10.212.167 dst=10.10.212.164 sport=9999 dport=30030 [UNREPLIED] src=10.10.212.164 dst=10.10.212.167 sport=8080 dport=9999 mark=0 zone=0 use=2
从conntrack表项可以看出,业务Pod重启时,conntrack表项记录了到节点IP而不是到Pod的IP,因为UDP的conntrack表项默认老化时间为30s,当设备请求频繁时,conntrack表项也就无法老化,后续所有请求都会转给节点IP而不是Pod的IP;
那么Pod重启场景下,UDP的表项中反向src为什么变成了节点IP呢?怀疑是Pod重启过程中,Podd的IP发送变化,相应的iptables规则也会删除重新添加,这段时间如果设备继续通过NodePort发送请求给该Pod,会存在短暂的时间请求无法发送到Pod内,而是节点IP收到后直接记录到conntrack表项里。
为了验证这个想法,再次构造nc命令频繁发送UDP请求到节点IP:
1 [root@node]# while true; do echo "test" | nc -4u 10.10.212.164 30031 -p 9999;done
查看30031端口的conntrack条目,确认正常情况下发送节点IP的UDP请求的反向src是节点IP,由此推测重启Pod过程中可能会出现这个问题:
1 2 [root@node]# cat /pro/net/nf_contrack |grep 30031 ipv4 2 udp 17 29 src=10.10.212.167 dst=10.10.212.164 sport=9999 dport=30031 [UNREPLIED] src=10.10.212.164 dst=10.10.212.167 sport=30031 dport=9999 mark=0 zone=0 use=2
一般来说,一个Pod的重启会经历先Kill再Create的操作,那么conntrack的异常表项的创建是在哪个阶段发生的呢?通过构造Pod的删除,实时记录conntrack的异常表项创建时间,可以分析出老的表项在Pod Kill阶段会被被动删除,而异常的表项是在Create Pod阶段创建的;
通过查看kube-proxy代码,也可以看出相关iptables规则的清除动作:
1 代码位置:https://github.com/kubernetes/kubernetes/blob/v1.15.12/pkg/proxy/iptables/proxier.go
而创建Pod阶段,为什么会偶现这个问题呢?查看proxier.go
的实现并验证发现,Pod从删除后到新创建之前,会在KUBE-EXTERNAL-SERVICES
链中临时设置如下规则(位于DNAT链之后),用于REJECT请求到异常Pod的流量:
1 -A KUBE-EXTERNAL-SERVICES -p udp -m comment --comment "allkinds/allkinds-deployment:udp has no endpoints" -m addrtype --dst-type LOCAL -m udp --dport 30030 -j REJECT --reject-with icmp-port-unreachable
上面的规则是在Pod异常时临时设置的,那么在Pod创建阶段,必然有个时机去清除,并且会下发相应的DNAT规则,而这两个操作的顺序就至关重要了。如果先下DNAT规则,请求从被拒绝转为走DNAT,这样conntrack表项的记录应该没有问题;如果先清理REJECT规则,则请求在DNAT规则下发之前有个临时状态——既没有了REJECT规则,又没有DNAT规则,这种情况下也就会出现我们见到的这个现象 ;
为了验证上面的猜想,继续查看proxier.go
的实现,可以发现实际下发规则的动作发生在如下几行代码,并且是先下发filter链,再下发nat链,而上面说的REJECT规则正是在filter链内,DNAT规则在nat链内,基本确认是下发顺序可能导致的异常;
1 2 3 4 5 6 7 8 代码位置:https://github.com/kubernetes/kubernetes/blob/v1.15.12/pkg/proxy/iptables/proxier.go#L667-L1446 // Sync rules. // NOTE: NoFlushTables is used so we don't flush non-kubernetes chains in the table proxier.iptablesData.Reset() proxier.iptablesData.Write(proxier.filterChains.Bytes()) proxier.iptablesData.Write(proxier.filterRules.Bytes()) proxier.iptablesData.Write(proxier.natChains.Bytes()) proxier.iptablesData.Write(proxier.natRules.Bytes())
最后是修改验证,通过调整filter链和nat链下发的顺序,重新制作kube-proxy镜像并替换到环境中,验证问题不再出现;
但是,这个修改方案只是为了定位出原因而做的临时修改,毕竟改变两个链的下发顺序的影响还是很大的,不能这么轻易调整,所以给社区提了相关issue(https://github.com/kubernetes/kubernetes/issues/102618 ),社区很快给出答复,说是https://github.com/kubernetes/kubernetes/pull/98305这个PR已经解决,社区的做法是将清理conntrack表项的时机移到了下发filter链和nat链之后,通过分析验证,该问题解决(唯一的小瑕疵是还会偶现几条异常conntrack表项,然后被清除,再恢复正常,不过也不影响什么);
1 2 3 4 ipv4 2 udp 17 29 src=10.10.212.167 dst=10.10.212.164 sport=9999 dport=30030 [UNREPLIED] src=10.10.212.164 dst=10.10.212.167 sport=8080 dport=9999 mark=0 zone=0 use=2 ipv4 2 udp 17 29 src=10.10.212.167 dst=10.10.212.164 sport=9999 dport=30030 [UNREPLIED] src=177.177.241.159dst=10.10.212.164 sport=8080 dport=9999 mark=0 zone=0 use=2 ipv4 2 udp 17 29 src=10.10.212.167 dst=10.10.212.164 sport=9999 dport=30030 [UNREPLIED] src=177.177.241.159dst=10.10.212.164 sport=8080 dport=9999 mark=0 zone=0 use=2 ipv4 2 udp 17 29 src=10.10.212.167 dst=10.10.212.164 sport=9999 dport=30030 [UNREPLIED] src=177.177.241.159dst=10.10.212.164 sport=8080 dport=9999 mark=0 zone=0 use=2
解决方案
升级K8S到v1.21及以上版本;
在无法升级K8S版本的前提下,将社区修改patch到老版本;