之前的文章<<Kubernetes网络和CNI>>和<<Kubernetes flannel网络分析>>介绍了Kubernetes集群的pod
网络的通信过程。
pod
本质上非固定的,经常发生变化,而pod IP
在pod
销毁和创建的时候会发生变更,因而不能直接对外提供服务。Kubernetes
通过service
资源来对外提供服务,service
的IP
是固定的,它自动绑定一组pod
并根据不同实现将流量转发到这些pod
中, 并在流量转发的过程中实现负载均衡(load balance)。
在Kubernetes
的node
节点上的主要组件有kube-proxy
和kubelet
, kubelet
会调用相关的CNI
实现完成POD
网络的通信。而kube-proxy
则负责上述的service
与POD
之间的流量转发。实际上,在之前文章的实验环境里,即使把node
节点上的kube-proxy
组件都停止,也不会影响pod
网络通信。
service
本质就是将一组pod
通过固定IP
暴露给使用者,可以由ip:port:protocol
来标识。
service
主要以下几种类型:
ClusterIP
: 用Kubernetes
集群内部IP
暴露服务,也就是说只有在Kubernetes
集群内才可以访问这个service
。这是默认的service
类型。ClusterIP
的范围是在kube-apiserver
启动时通过-service-cluster-ip-range
参数指定的。这些IP
只能在kubernetes
集群内进行访问。service
的相关信息是在yaml
文件中定义的,最终暴露的信息可表示为:1
spec.clusterIp:spec.ports[*].port:spec.ports[*].protocol
NodePort
: 在Kubernetes
集群的所有node
节点上使用相同的固定port
来暴露服务。这种类型会自动创建ClusterIP
类型的服务,NodePort
的service
会将流量转发到ClusterIP
类型的服务。服务的使用者可以使用NodeIP:NodePort
来访问该服务。这种类型服务暴露的信息可以表示为:1
2<NodeIP>:spec.ports[*].nodePort:spec.ports[*].protocol
spec.clusterIp:spec.ports[*].port:spec.ports[*].protocolLoadBalancer
: 是通过kubernetes
集群外部设施所提供的IP
来暴露服务。NodePort
和ClusterIP
类型的服务会被自动创建。不同的LoadBalancer
负责实现外部IP:port
与NodePort
服务的映射。这种类型暴露的信息可以表示为:1
2
3spec.loadBalancerIp:spec.ports[*].port:spec.ports[*].protocol
<NodeIP>:spec.ports[*].nodePort:spec.ports[*].protocol
spec.clusterIp:spec.ports[*].port:spec.ports[*].protocol
Kubernetes
集群默认会存在一个ClusterIP
类型的服务kubernetes
,它可以让集群内部的pod
去访问kube-apiserver
,如:
1 | [root@master1 ~]# kubectl get svc |
接下来我们基于containous/whoami
镜像创建一个ClusterIP
类型的服务whoami
:
1 | apiVersion: v1 |
创建service
:
1 | [root@master1 ~]# kubectl apply -f whoami.yaml |
查看service
资源:
1 | [root@master1 ~]# kubectl get svc -o wide |
查看service
对应的endpoint
:
1 | [root@master1 ~]# kubectl get ep |
创建一个客户端curl
的pod
:
1 | [root@master1 ~]# kubectl run curl1 --image=radial/busyboxplus:curl --command -- sleep 3600 |
此时pod
如下, 一个curl
客户端,3个whoami
的WEB服务器:
1 | [root@master1 ~]# kubectl get pods -o wide |
从客户端pod
访问service
, 可以看到请求被转发到不同的后端pod
:
1 | [root@master1 ~]# kubectl exec -it curl1-54dbd6b8cb-vm5wc -- curl http://10.32.0.235 |
kube-proxy
服务会监听kube-apiserver
中的service
和endpoint
的变化来配置service
和endpoint
的对应关系。当前具体的转发实现方案有userspace
, iptables
, ipvs
几种,默认实现为iptables
。本文来分析iptables
实现方案下,POD
访问ClusterIP
的数据包路径。数据包整体通过iptables
链的过程,可以参考之前的文章<<IPTABLES机制分析>>。
要注意的是,kube-proxy
的iptables
规则都是添加在init_net
的,所以数据包从pod
一端的veth peer
发出,然后从宿主机这端veth peer
进入init_net
时,才会由nat
表的PREROUTING
链处理, 跳转至KUBE-SERVICES
链:
1 | -A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES |
KUBE-SERVICES
链规则如下:
1 | -A KUBE-SERVICES ! -s 10.230.0.0/16 -d 10.32.0.235/32 -p tcp -m comment --comment "default/whoami:web cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ |
kubernetes
和whoami
两个service
, 每个service
有两条iptables
规则:
第一条表示,! -s 10.230.0.0/16
表示数据包来自kubernetes
集群外IP
。如果是kubernetes
集群外访问service
,跳转至KUBE-MARK-MASQ
。
第二条规则表示,集群内IP
访问该service
则跳转至KUBE-SVC-225DYIB7Z2N6SCOU
处理。
最后一条KUBE-NODEPORTS
用于匹配NodePort
类型的service
。
KUBE-SVC-225DYIB7Z2N6SCOU
的规则如下:
1 | -A KUBE-SVC-225DYIB7Z2N6SCOU -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-6ALQJ7ITJYIQGLX3 |
KUBE-SVC-225DYIB7Z2N6SCOU
的规则,使用statistic
模块的random
模式来实现负载均衡,跳转到不同的endpoint
。每个endpoint
有一条自定义链,如KUBE-SEP-6ALQJ7ITJYIQGLX3
:
1 | -A KUBE-SEP-6ALQJ7ITJYIQGLX3 -s 10.230.74.7/32 -j KUBE-MARK-MASQ |
第一条规则表示数据包是由service
的endpoint
自身去访问ClusterIP
而访问到自身, 这种情况跳转至KUBE-MARK-MASQ
处理。
第二条规则表示源不是自身pod IP
的数据包,这种情况下将数据包目标地址的ClusterIP:Port
修改为对应pod
的地址和端口。
之后数据包进行路由决策,判断是由flannel.1
发送到其他节点,还是由cni0
进行二层转发。
进入filter
表的FORWARD
链进行处理如下:
1 | -A FORWARD -m comment --comment "kubernetes forwarding rules" -j KUBE-FORWARD |
而KUBE-FORWARD
的规则如下:
1 | -A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP |
FORWARD
阶段主要是由conntrack
模块实现连接跟踪。连接状态为NEW
的数据包跳转到KUBE-SERVICE
链进行处理。需要注意的是,iptables
中不同表中的同名链是不相同的链。这里filter
表的KUBE-SERVICES
链为空。
数据包路由决策结束之后进入nat
的POSTROUTING
处理:
1 | -A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING |
KUBE-POSTROUTING
的规则如下:
1 | -A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE |
客户端pod
发出的数据包匹配到nat:POSTROUTING
的第4条规则,跳到转CNI-3f43e40b0646cf4ef37761a7
处理:
1 | -A CNI-3f43e40b0646cf4ef37761a7 -d 10.230.74.0/24 -m comment --comment "name: \"cbr0\" id: \"f75b50453a13f79d2fe56f288b04b248a3194f76ceb56f3a0e55b083cdedc006\"" -j ACCEPT |
匹配到第一条规则,数据包被接受,转发到目标pod
。
整个匹配过程如图:
pod
的响应数据包进入init_net
之后同样首先匹配nat:PREROUTING
,匹配不到任何规则,进入filter
表的FORWARD
链处理, 匹配到conntrack
规则,此时根据conntrack
的信息,自动完成SNAT
过程,将响应数据包的源IP
修改为ClusterIP: 10.230.0.235
。
接着进入nat
的POSTROUTING
, 匹配不到任何规则,转发回客户端pod
,完成一来一回的通信过程。
需要注意的是,这种情况下,node
节点的参数net.bridge.bridge-nf-call-iptables
需要设置为1
。否则,当选择到同一台主机的后端pod
时,在KUBE-SEP-*
链中完成DNAT
之后,目的地址和源地址在同一个二层网络内,直接由网桥转发到相应的网卡上,并不会调用到iptables
的FORWARD
链。这样,响应数据包回到网桥时,因为不会调用iptables
的FORWARD
链,不会完成对应的SNAT
操作。因而客户端pod
会收到源IP
为后端pod IP
的数据包。对于TCP
连接,因为收到的SYN+ACK
包不匹配,因而会发送一个RESET
包,因而TCP
连接无法建立。
这种场景下,在客户端pod
上抓包,可以看到:
1 | [root@node1 ~]# tcpdump -i vethb4eaf1e5 -nn |
收到的SYN+ACK
包的源IP
并不是10.32.0.235
, 而是后端pod
的10.230.74.8
,因而返回一个RESET
数据包。
如果KUBE-SVC-*
链里匹配到的endpoint
不在该node
, 路由决策完会从flannel.1
发送。数据包通过VXLAN
到达node2
之后,数据包地址已经过DNAT
,目标地址为pod IP
,就是正常的POD
网络通信过程,经由nat:PREROUTING
,filter:FORWARD
和nat:POSTROUTING
处理,匹配不到任何规则,转发到目标POD
。
响应包在node2
上是正常的POD
网络通信过程,通过VXLAN
回到node1
。然后进入nat:PREROUTING
链处理, 匹配不到任何规则,进入filter:FORWARD
链处理, 匹配到conntrack
规则,根据conntrack
的信息,自动完成SNAT
过程,数据包的源IP
修改回10.230.0.235
。接着进入nat:POSTROUTING
处理, 匹配不到任何规则,转发回客户端pod
,完成一来一回的数据包过程。
POD
访问ClusterIP
的基础过程就是这样。
我们把上边的service
修改为NodePort
类型。
1 | [root@master1 ~]# kubectl get svc -o wide |
查看service
信息,分配的port
为31554
,
访问NodeIP:NodePort
的数据包进入node1
之后,先由nat:PREROUTING
链处理, 匹配到KUBE-SERVICES
中的规则,进而由KUBE-NODEPORTS
进行处理:
1 | -A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS |
配置NodePort
类型service
后,iptables
增加了两条规则:
1 | -A KUBE-NODEPORTS -p tcp -m comment --comment "default/whoami:web" -m tcp --dport 31554 -j KUBE-MARK-MASQ |
在KUBE-MARK-MASQ
链中设置0x4000/0x4000
的mark
值:
1 | -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000 |
然后跳转到KUBE-SVC-225DYIB7Z2N6SCOU
进行处理,之后的过程和ClusterIP
的通信过程一致,直到nat:POSTROUTING
链的处理:
1 | -A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE |
因为数据包在PREROUTING
阶段已经设置mark
, 匹配到这条规则完成SNAT
过程,因而在这种情况下,后端pod
看到的源IP
为所在node
上MASQUERADE
选择的IP
。如果选定的后端pod
位于本node
上,则出口设备为网桥cni0
,因而后端pod
上看到的源IP
为cni0
的地址。如果选定的后端位于其他node
, 需要通过VXLAN
转发到其他节点,则出口设备为flannel.1
, 因而后端pod
上看到的源IP
为该node
上flannel.1
的IP
, 比如:
1 | [root@master1 ~]# kubectl exec -it curl1-54dbd6b8cb-vm5wc -- curl http://10.240.0.101:31554 |
总结来说,kube-proxy
的iptables
模式主要用到这些自定义链:
nat:KUBE-SERVICES
: 匹配数据包目标地址,跳转到相应的KUBE-SVC-*
nat:KUBE-SVC-*
: 作为负载均衡设备,分发流量到不同的KUBE-SEP-*
nat:KUBE-SEP-*
: 代表一个后端pod
, 也叫service end-point
,完成DNAT
过程nat:KUBE-MARK-MASQ
: 给来自集群外的数据包设置mark
, 这些数据包在POSTROUTING
阶段需要完成SNAT
操作nat:KUBE-NODEPORTS
: 当配置NodePort
或者LoadBalancer
类型的service
时,跳转到KUBE-MARK-MASQ
设置mark
和跳转到相应的KUBE-SVC-*
参考:
- https://arthurchiao.art/blog/cracking-k8s-node-proxy/#1-background-knowledge
- https://www.cloudsavvyit.com/11261/kubernetes-clusterip-nodeport-or-ingress-when-to-use-each/
- https://kubernetes.io/docs/concepts/services-networking/service/
- https://stackoverflow.com/questions/41509439/whats-the-difference-between-clusterip-nodeport-and-loadbalancer-service-types
- https://cizixs.com/2017/03/30/kubernetes-introduction-service-and-kube-proxy/
- https://faun.pub/kubernetes-without-kube-proxy-1c5d25786e18
- https://bugzilla.redhat.com/show_bug.cgi?id=512206
- https://blog.csdn.net/flxzlxb/article/details/112849468
- https://www.codeleading.com/article/46422763270/
- https://serenafeng.github.io/2020/03/26/kube-proxy-in-iptables-mode/