之前的文章<<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/