最近遇到一点k8s的网络问题,顺便仔细看了一下k8s的iptables规则,在此记录一下相关思路。
查看service
我司使用的是ucloud云,使用的是ucloud提供的LoadBalancer Service。
下面是service的详细信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
[root@10-42-74-90 ~]# kubectl describe services testapi-smzdm-com -n zhongce-v2-0 Name: testapi-smzdm-com Namespace: zhongce-v2-0 Labels: <none> Annotations: kubectl.kubernetes.io/last-applied-configuration: {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{"service.beta.kubernetes.io/ucloud-load-balancer-type":"Inner","service.bet... prometheus.io/scrape: true service.beta.kubernetes.io/ucloud-load-balancer-type: Inner service.beta.kubernetes.io/ucloud-load-balancer-vserver-client-timeout: 60 service.beta.kubernetes.io/ucloud-load-balancer-vserver-method: Roundrobin service.beta.kubernetes.io/ucloud-load-balancer-vserver-protocol: TCP service.beta.kubernetes.io/ucloud-load-balancer-vserver-session-persistence-info: string service.beta.kubernetes.io/ucloud-load-balancer-vserver-session-persistence-type: None Selector: zdm-app-owner=testapi-smzdm-com Type: LoadBalancer IP: 172.17.185.22 LoadBalancer Ingress: 10.42.162.216 Port: <unset> 809/TCP TargetPort: 809/TCP NodePort: <unset> 39746/TCP Endpoints: 10.42.147.255:809,10.42.38.222:809 Session Affinity: None External Traffic Policy: Cluster Events: <none> |
该Service背后对应了2个POD,也就是Endpoints所示。
我们首先需要搞清楚LoadBalancer Service是如何工作的,才能进一步分析iptables规则:
首先,该service仍旧会分配一个K8S CIDR网段的Cluster iP,这里就是IP:172.17.185.22。
其次,该service仍旧会分配一个nodeport,也就是在所有node上开放一个唯一端口对K8S集群外服务,这里端口就是NodePort:39746。
最后,会在k8s集群外部署一个proxy服务,其反向代理到K8S集群各个node上的nodeport端口,这个proxy的服务地址是:LoadBalancer Ingress 17.42.162.216。
集群内调用service
集群内POD调用service,通常是使用Cluster IP,也就是172.17.185.22。
但因为该Service是LoadBalancer类型,所以其实也可以调用集群外LB(IP:10.42.162.216)反向代理到nodeport,同样可以访问到集群内的Service。
所以,我们在观察k8s service iptables规则的时候,可以分析2种访问路径:
- 集群内发起调用,通过cluster ip访问到service。
- 集群外发起调用,通过nodeport访问到service。
现在我们登录到任意node,因为k8s会把完整的iptables规则下发到每一个node。
cluster ip工作原理
调用方是某个node上面的某个POD,它向cluster ip 172.17.185.22发起调用,所以走的是OUTPUT链。
iptables要做的事情,就是识别dst(目标IP)是172.17.185.22的包,把dst改写成某一个Endpoints的IP地址(这个service背后对应2个endpoints)。
我们从output链开始看:
1 2 3 4 |
[root@10-42-8-102 ~]# iptables -t nat -nvL OUTPUT Chain OUTPUT (policy ACCEPT 4 packets, 240 bytes) pkts bytes target prot opt in out source destination 3424K 209M KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */ |
直接交给KUBE-SERVICES链了,继续看:
1 2 3 4 5 6 |
[root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-SERVICES Chain KUBE-SERVICES (2 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ tcp -- * * !172.17.0.0/16 172.17.185.22 /* zhongce-v2-0/testapi-smzdm-com: cluster IP */ tcp dpt:809 0 0 KUBE-SVC-G3OM5DSD2HHDMN6U tcp -- * * 0.0.0.0/0 172.17.185.22 /* zhongce-v2-0/testapi-smzdm-com: cluster IP */ tcp dpt:809 10 520 KUBE-FW-G3OM5DSD2HHDMN6U tcp -- * * 0.0.0.0/0 10.42.162.216 /* zhongce-v2-0/testapi-smzdm-com: loadbalancer IP */ tcp dpt:809 |
我对结果进行了截取,只保留了属于该Service的规则,你会发现K8S生成的规则都有清晰的注释,还是很好阅读的。
上述3条规则是顺序执行的:
- 第1条规则匹配发往Cluster IP 172.17.185.22的流量,跳转到了KUBE-MARK-MASQ链进一步处理,其作用就是打了一个MARK,稍后展开说明。
- 第2条规则匹配发往Cluster IP 172.17.185.22的流量,跳转到了KUBE-SVC-G3OM5DSD2HHDMN6U链进一步处理,稍后展开说明。
- 第3条规则匹配发往集群外LB IP的10.42.162.216的流量,跳转到了
KUBE-FW-G3OM5DSD2HHDMN6U链进一步处理,稍后展开说明。
打MARK这一步还不是很重要,所以我们先说第2条规则,显然第2条规则对发往Cluster IP的流量要做dst IP改写到具体的一个Endpoints上面,我们看一下:
1 2 3 4 5 |
[root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-SVC-G3OM5DSD2HHDMN6U Chain KUBE-SVC-G3OM5DSD2HHDMN6U (3 references) pkts bytes target prot opt in out source destination 18 936 KUBE-SEP-JT2KW6YUTVPLLGV6 all -- * * 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.50000000000 21 1092 KUBE-SEP-VETLC6CJY2HOK3EL all -- * * 0.0.0.0/0 0.0.0.0/0 |
一共2条规则,每条规则的应用概率是0.5,其实就是负载均衡流量到这2条链的意思,而这2条链分别对应各自endpoints的DNAT规则:
1 2 3 4 5 6 7 8 9 10 11 |
[root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-SEP-JT2KW6YUTVPLLGV6 Chain KUBE-SEP-JT2KW6YUTVPLLGV6 (1 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ all -- * * 10.42.147.255 0.0.0.0/0 26 1352 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp to:10.42.147.255:809 [root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-SEP-VETLC6CJY2HOK3EL Chain KUBE-SEP-VETLC6CJY2HOK3EL (1 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ all -- * * 10.42.38.222 0.0.0.0/0 2 104 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp to:10.42.38.222:809 |
这样就实现了在调用方完成负载均衡逻辑,也就是Cluster IP的工作原理了。
流量经过本机路由表决定从eth0 LAN出去(ucloud是underlay扁平网络):
1 2 3 4 5 |
[root@10-42-8-102 ~]# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.42.0.1 0.0.0.0 UG 0 0 0 eth0 10.42.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0 |
在流量离开本机之前会经过POSTROUTING链:
1 2 3 4 5 6 7 8 9 |
[root@10-42-8-102 ~]# iptables -t nat -nvL POSTROUTING Chain POSTROUTING (policy ACCEPT 274 packets, 17340 bytes) pkts bytes target prot opt in out source destination 632M 36G KUBE-POSTROUTING all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */ [root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-POSTROUTING Chain KUBE-POSTROUTING (1 references) pkts bytes target prot opt in out source destination 526 27352 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000 |
其实直接就跳转到了KUBE-POSTROUTING,然后匹配打过0x4000 MARK的流量,将其做SNAT转换,而这个MARK其实就是之前没说的KUBE-MARK-MASQ做的事情:
1 2 3 4 |
[root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-MARK-MASQ Chain KUBE-MARK-MASQ (183 references) pkts bytes target prot opt in out source destination 492 25604 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000 |
也就是说,当流量离开本机时,src IP会被修改为node的IP,而不是发出流量的POD IP了。
最后还有一个KUBE-FW-G3OM5DSD2HHDMN6U链没有讲,从本机发往LB IP的流量要做啥事情呢?
其实也是让流量直接发往具体某个Endpoints,就别真的发往LB了,这样才能获得最佳的延迟:
1 2 3 4 5 6 |
[root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-FW-G3OM5DSD2HHDMN6U Chain KUBE-FW-G3OM5DSD2HHDMN6U (1 references) pkts bytes target prot opt in out source destination 2 104 KUBE-MARK-MASQ all -- * * 0.0.0.0/0 0.0.0.0/0 /* zhongce-v2-0/testapi-smzdm-com: loadbalancer IP */ 2 104 KUBE-SVC-G3OM5DSD2HHDMN6U all -- * * 0.0.0.0/0 0.0.0.0/0 /* zhongce-v2-0/testapi-smzdm-com: loadbalancer IP */ 0 0 KUBE-MARK-DROP all -- * * 0.0.0.0/0 0.0.0.0/0 /* zhongce-v2-0/testapi-smzdm-com: loadbalancer IP */ |
其中第2条规则就是复用了之前我们看到的负载均衡链。
nodeport工作原理
nodeport就是在每个node上开放同一个端口,只要集群外访问node上的该端口就可以访问到对应的service。
因此,iptables此时的目标是dst IP是node自己,并且dst port是nodeport端口的流量,将其直接改写到service的某个endpoints。
其实该匹配规则也是写在PREROUTING链上的,具体在KUBE-SERVICES链上,在之前的结果里面我没有贴出来而已:
1 2 3 4 5 6 7 |
[root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-SERVICES Chain KUBE-SERVICES (2 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ tcp -- * * !172.17.0.0/16 172.17.185.22 /* zhongce-v2-0/testapi-smzdm-com: cluster IP */ tcp dpt:809 0 0 KUBE-SVC-G3OM5DSD2HHDMN6U tcp -- * * 0.0.0.0/0 172.17.185.22 /* zhongce-v2-0/testapi-smzdm-com: cluster IP */ tcp dpt:809 10 520 KUBE-FW-G3OM5DSD2HHDMN6U tcp -- * * 0.0.0.0/0 10.42.162.216 /* zhongce-v2-0/testapi-smzdm-com: loadbalancer IP */ tcp dpt:809 0 0 KUBE-NODEPORTS all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL |
处理nodeport的规则在末尾,只要dst IP是node本机IP的话(–dst-type LOCAL),就跳转到KUBE-NODEPORTS做进一步判定:
1 2 3 4 5 |
[root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-NODEPORTS Chain KUBE-NODEPORTS (1 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* zhongce-v2-0/testapi-smzdm-com: */ tcp dpt:39746 0 0 KUBE-SVC-G3OM5DSD2HHDMN6U tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* zhongce-v2-0/testapi-smzdm-com: */ tcp dpt:39746 |
我只保留了与该service相关的2条规则:
- 第1条匹配dst port如果是39746,那么就打mark。
- 第2条匹配dst port如果是39746,那么就跳到负载均衡链做DNAT改写。
- 否则就不做任何处理,流量继续交给后续链条处理。
路由表
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 |
[root@10-42-8-102 ~]# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.42.0.1 0.0.0.0 UG 0 0 0 eth0 10.42.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0 10.42.2.81 0.0.0.0 255.255.255.255 UH 0 0 0 vethc739b9e3 10.42.8.62 0.0.0.0 255.255.255.255 UH 0 0 0 veth1e24e60d 10.42.24.222 0.0.0.0 255.255.255.255 UH 0 0 0 veth95162021 10.42.27.43 0.0.0.0 255.255.255.255 UH 0 0 0 veth24de41d5 10.42.34.46 0.0.0.0 255.255.255.255 UH 0 0 0 vetha2f363a4 10.42.40.53 0.0.0.0 255.255.255.255 UH 0 0 0 veth0b41aa27 10.42.40.234 0.0.0.0 255.255.255.255 UH 0 0 0 veth3efbdb1d 10.42.43.215 0.0.0.0 255.255.255.255 UH 0 0 0 vethe3d4da66 10.42.44.118 0.0.0.0 255.255.255.255 UH 0 0 0 vethe2ece1a2 10.42.45.252 0.0.0.0 255.255.255.255 UH 0 0 0 vethec59f595 10.42.79.48 0.0.0.0 255.255.255.255 UH 0 0 0 veth601bcc30 10.42.80.237 0.0.0.0 255.255.255.255 UH 0 0 0 veth5ceb25f3 10.42.94.18 0.0.0.0 255.255.255.255 UH 0 0 0 veth4ead654a 10.42.98.165 0.0.0.0 255.255.255.255 UH 0 0 0 veth351de976 10.42.107.57 0.0.0.0 255.255.255.255 UH 0 0 0 vethcb1b7717 10.42.117.183 0.0.0.0 255.255.255.255 UH 0 0 0 vethec5432d8 10.42.123.142 0.0.0.0 255.255.255.255 UH 0 0 0 veth05b8db79 10.42.128.25 0.0.0.0 255.255.255.255 UH 0 0 0 vethb7404f7d 10.42.152.205 0.0.0.0 255.255.255.255 UH 0 0 0 vethe1c8bcef 10.42.184.41 0.0.0.0 255.255.255.255 UH 0 0 0 vethed9c2764 |
因为使用ucloud的CNI插件,所以网络是一个underlay模式,Node和POD的IP段是直通的。
当被调用的node收到流量的时候,经过PREROUTING链什么也不做,经过路由表后可以判定转发给哪个虚拟网卡,从而将流量FORWARD到具体的docker容器内。
最后
关于K8S Service的分析就到这里,我觉得比较重要的一点认识是:
K8S集群内调用LB IP并不会真的发给LB,仍旧是直接走了集群内的负载均衡。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

新手,今晚观察到SVC下的每个POD的DNAT都由一个单独的chain维护,还在疑惑数据是以怎样的逻辑准确命中这些chain的,看完博主的文章恍然大悟,感谢!