0%

K8S问题排查-UDP频繁发包导致Pod重启后无法接收数据

问题背景

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表有关:

netfilter

查看指定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

解决方案

  1. 升级K8S到v1.21及以上版本;
  2. 在无法升级K8S版本的前提下,将社区修改patch到老版本;