Keep learning, keep living...

0%

Kubernetes Service网络通信分析

之前的文章<<Kubernetes网络和CNI>><<Kubernetes flannel网络分析>>介绍了Kubernetes集群的pod网络的通信过程。

pod本质上非固定的,经常发生变化,而pod IPpod销毁和创建的时候会发生变更,因而不能直接对外提供服务。Kubernetes通过service资源来对外提供服务,serviceIP是固定的,它自动绑定一组pod并根据不同实现将流量转发到这些pod中, 并在流量转发的过程中实现负载均衡(load balance)。

Kubernetesnode节点上的主要组件有kube-proxykubelet, kubelet会调用相关的CNI实现完成POD网络的通信。而kube-proxy则负责上述的servicePOD之间的流量转发。实际上,在之前文章的实验环境里,即使把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类型的服务,NodePortservice会将流量转发到ClusterIP类型的服务。服务的使用者可以使用NodeIP:NodePort来访问该服务。这种类型服务暴露的信息可以表示为:

    1
    2
    <NodeIP>:spec.ports[*].nodePort:spec.ports[*].protocol
    spec.clusterIp:spec.ports[*].port:spec.ports[*].protocol
  • LoadBalancer: 是通过kubernetes集群外部设施所提供的IP来暴露服务。NodePortClusterIP类型的服务会被自动创建。不同的LoadBalancer负责实现外部IP:portNodePort服务的映射。这种类型暴露的信息可以表示为:

    1
    2
    3
    spec.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
2
3
[root@master1 ~]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.32.0.1 <none> 443/TCP 15d

接下来我们基于containous/whoami镜像创建一个ClusterIP类型的服务whoami:

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: v1
kind: Service
metadata:
labels:
name: whoami
name: whoami
spec:
ports:
- port: 80
name: web
protocol: TCP
selector:
app: whoami
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
labels:
app: whoami
spec:
replicas: 3
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: containous/whoami
ports:
- containerPort: 80
name: web

创建service:

1
2
3
[root@master1 ~]# kubectl apply -f whoami.yaml
service/whoami created
deployment.apps/whoami created

查看service资源:

1
2
3
4
[root@master1 ~]# kubectl get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.32.0.1 <none> 443/TCP 16d <none>
whoami ClusterIP 10.32.0.235 <none> 80/TCP 3m7s app=whoami

查看service对应的endpoint:

1
2
3
4
[root@master1 ~]# kubectl get ep
NAME ENDPOINTS AGE
kubernetes 10.240.0.10:6443,10.240.0.11:6443,10.240.0.12:6443 16d
whoami 10.230.74.7:80,10.230.74.8:80,10.230.95.7:80 3h23m

创建一个客户端curlpod:

1
2
3
[root@master1 ~]# kubectl run curl1 --image=radial/busyboxplus:curl --command -- sleep 3600
kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
deployment.apps/curl1 created

此时pod如下, 一个curl客户端,3个whoami的WEB服务器:

1
2
3
4
5
6
[root@master1 ~]# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
curl1-54dbd6b8cb-vm5wc 1/1 Running 0 8m1s 10.230.74.19 node1 <none> <none>
whoami-5df4df6ff5-2frx6 1/1 Running 0 3h 10.230.74.7 node1 <none> <none>
whoami-5df4df6ff5-84tpp 1/1 Running 0 3h 10.230.95.7 node2 <none> <none>
whoami-5df4df6ff5-rh4zq 1/1 Running 0 3h 10.230.74.8 node1 <none> <none>

从客户端pod访问service, 可以看到请求被转发到不同的后端pod:

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
[root@master1 ~]# kubectl exec -it curl1-54dbd6b8cb-vm5wc -- curl http://10.32.0.235
Hostname: whoami-5df4df6ff5-rh4zq
IP: 127.0.0.1
IP: ::1
IP: 10.230.74.8
IP: fe80::589f:13ff:fef7:bc05
RemoteAddr: 10.230.74.19:36694
GET / HTTP/1.1
Host: 10.32.0.235
User-Agent: curl/7.35.0
Accept: */*

[root@master1 ~]# kubectl exec -it curl1-54dbd6b8cb-vm5wc -- curl http://10.32.0.235
Hostname: whoami-5df4df6ff5-84tpp
IP: 127.0.0.1
IP: ::1
IP: 10.230.95.7
IP: fe80::5433:63ff:fe98:6d1e
RemoteAddr: 10.230.74.0:36698
GET / HTTP/1.1
Host: 10.32.0.235
User-Agent: curl/7.35.0
Accept: */*

[root@master1 ~]# kubectl exec -it curl1-54dbd6b8cb-vm5wc -- curl http://10.32.0.235
Hostname: whoami-5df4df6ff5-2frx6
IP: 127.0.0.1
IP: ::1
IP: 10.230.74.7
IP: fe80::20c8:faff:fe77:7a40
RemoteAddr: 10.230.74.19:36706
GET / HTTP/1.1
Host: 10.32.0.235
User-Agent: curl/7.35.0
Accept: */*

kube-proxy服务会监听kube-apiserver中的serviceendpoint的变化来配置serviceendpoint的对应关系。当前具体的转发实现方案有userspace, iptables, ipvs几种,默认实现为iptables。本文来分析iptables实现方案下,POD访问ClusterIP的数据包路径。数据包整体通过iptables链的过程,可以参考之前的文章<<IPTABLES机制分析>>

要注意的是,kube-proxyiptables规则都是添加在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
2
3
4
5
-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
-A KUBE-SERVICES -d 10.32.0.235/32 -p tcp -m comment --comment "default/whoami:web cluster IP" -m tcp --dport 80 -j KUBE-SVC-225DYIB7Z2N6SCOU
-A KUBE-SERVICES ! -s 10.230.0.0/16 -d 10.32.0.1/32 -p tcp -m comment --comment "default/kubernetes:https cluster IP" -m tcp --dport 443 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.32.0.1/32 -p tcp -m comment --comment "default/kubernetes:https cluster IP" -m tcp --dport 443 -j KUBE-SVC-NPX46M4PTMTKRN6Y
-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

kuberneteswhoami两个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
2
3
-A KUBE-SVC-225DYIB7Z2N6SCOU -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-6ALQJ7ITJYIQGLX3
-A KUBE-SVC-225DYIB7Z2N6SCOU -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-GEJ3N4FTSFGJDJNF
-A KUBE-SVC-225DYIB7Z2N6SCOU -j KUBE-SEP-VVIWAZ76Z4TWVPK7

KUBE-SVC-225DYIB7Z2N6SCOU的规则,使用statistic模块的random模式来实现负载均衡,跳转到不同的endpoint。每个endpoint有一条自定义链,如KUBE-SEP-6ALQJ7ITJYIQGLX3:

1
2
-A KUBE-SEP-6ALQJ7ITJYIQGLX3 -s 10.230.74.7/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-6ALQJ7ITJYIQGLX3 -p tcp -m tcp -j DNAT --to-destination 10.230.74.7:80

第一条规则表示数据包是由serviceendpoint自身去访问ClusterIP而访问到自身, 这种情况跳转至KUBE-MARK-MASQ处理。
第二条规则表示源不是自身pod IP的数据包,这种情况下将数据包目标地址的ClusterIP:Port修改为对应pod的地址和端口。

之后数据包进行路由决策,判断是由flannel.1发送到其他节点,还是由cni0进行二层转发。

进入filter表的FORWARD链进行处理如下:

1
2
-A FORWARD -m comment --comment "kubernetes forwarding rules" -j KUBE-FORWARD
-A FORWARD -m conntrack --ctstate NEW -m comment --comment "kubernetes service portals" -j KUBE-SERVICES

KUBE-FORWARD的规则如下:

1
2
3
4
-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
-A KUBE-FORWARD -s 10.230.0.0/16 -m comment --comment "kubernetes forwarding conntrack pod source rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A KUBE-FORWARD -d 10.230.0.0/16 -m comment --comment "kubernetes forwarding conntrack pod destination rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

FORWARD阶段主要是由conntrack模块实现连接跟踪。连接状态为NEW的数据包跳转到KUBE-SERVICE链进行处理。需要注意的是,iptables中不同表中的同名链是不相同的链。这里filter表的KUBE-SERVICES链为空。

数据包路由决策结束之后进入natPOSTROUTING处理:

1
2
3
4
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A POSTROUTING -s 10.230.74.7/32 -m comment --comment "name: \"cbr0\" id: \"d2ec32c253b68ec5b3c5327c51c62637e8a54f0f96046f4c7820fd671c0c99c8\"" -j CNI-b227064a304757ab8c96d7e0
-A POSTROUTING -s 10.230.74.8/32 -m comment --comment "name: \"cbr0\" id: \"6ddf1a0d25326e975fb4a36f441ff86cb52977de18fbc10d5e153e1ab89ddd6f\"" -j CNI-e4c4a4418638b487abe587d5
-A POSTROUTING -s 10.230.74.19/32 -m comment --comment "name: \"cbr0\" id: \"f75b50453a13f79d2fe56f288b04b248a3194f76ceb56f3a0e55b083cdedc006\"" -j CNI-3f43e40b0646cf4ef37761a7

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
2
-A CNI-3f43e40b0646cf4ef37761a7 -d 10.230.74.0/24 -m comment --comment "name: \"cbr0\" id: \"f75b50453a13f79d2fe56f288b04b248a3194f76ceb56f3a0e55b083cdedc006\"" -j ACCEPT
-A CNI-3f43e40b0646cf4ef37761a7 ! -d 224.0.0.0/4 -m comment --comment "name: \"cbr0\" id: \"f75b50453a13f79d2fe56f288b04b248a3194f76ceb56f3a0e55b083cdedc006\"" -j MASQUERADE

匹配到第一条规则,数据包被接受,转发到目标pod

整个匹配过程如图:

pod的响应数据包进入init_net之后同样首先匹配nat:PREROUTING,匹配不到任何规则,进入filter表的FORWARD链处理, 匹配到conntrack规则,此时根据conntrack的信息,自动完成SNAT过程,将响应数据包的源IP修改为ClusterIP: 10.230.0.235

接着进入natPOSTROUTING, 匹配不到任何规则,转发回客户端pod,完成一来一回的通信过程。

需要注意的是,这种情况下,node节点的参数net.bridge.bridge-nf-call-iptables需要设置为1。否则,当选择到同一台主机的后端pod时,在KUBE-SEP-*链中完成DNAT之后,目的地址和源地址在同一个二层网络内,直接由网桥转发到相应的网卡上,并不会调用到iptablesFORWARD链。这样,响应数据包回到网桥时,因为不会调用iptablesFORWARD链,不会完成对应的SNAT操作。因而客户端pod会收到源IP为后端pod IP的数据包。对于TCP连接,因为收到的SYN+ACK包不匹配,因而会发送一个RESET包,因而TCP连接无法建立。

这种场景下,在客户端pod上抓包,可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@node1 ~]# tcpdump -i vethb4eaf1e5 -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on vethb4eaf1e5, link-type EN10MB (Ethernet), capture size 262144 bytes


13:20:38.386567 IP 10.230.74.19.36450 > 10.32.0.235.80: Flags [S], seq 1431048683, win 28200, options [mss 1410,sackOK,TS val 52241475 ecr 0,nop,wscale 7], length 0
13:20:38.386609 IP 10.230.74.19.36450 > 10.230.74.8.80: Flags [S], seq 1431048683, win 28200, options [mss 1410,sackOK,TS val 52241475 ecr 0,nop,wscale 7], length 0
13:20:38.386631 IP 10.230.74.8.80 > 10.230.74.19.36450: Flags [S.], seq 1948187370, ack 1431048684, win 27960, options [mss 1410,sackOK,TS val 52241475 ecr 52241475,nop,wscale 7], length 0
13:20:38.386639 IP 10.230.74.19.36450 > 10.230.74.8.80: Flags [R], seq 1431048684, win 0, length 0
^C
4 packets captured
4 packets received by filter
0 packets dropped by kernel

收到的SYN+ACK包的源IP并不是10.32.0.235, 而是后端pod10.230.74.8,因而返回一个RESET数据包。

如果KUBE-SVC-*链里匹配到的endpoint不在该node, 路由决策完会从flannel.1发送。数据包通过VXLAN到达node2之后,数据包地址已经过DNAT,目标地址为pod IP,就是正常的POD网络通信过程,经由nat:PREROUTINGfilter:FORWARDnat: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
2
3
4
[root@master1 ~]# kubectl get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.32.0.1 <none> 443/TCP 18d <none>
whoami NodePort 10.32.0.235 <none> 80:31554/TCP 33h app=whoami

查看service信息,分配的port31554,

访问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
2
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/whoami:web" -m tcp --dport 31554 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/whoami:web" -m tcp --dport 31554 -j KUBE-SVC-225DYIB7Z2N6SCOU

KUBE-MARK-MASQ链中设置0x4000/0x4000mark值:

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为所在nodeMASQUERADE选择的IP。如果选定的后端pod位于本node上,则出口设备为网桥cni0,因而后端pod上看到的源IPcni0的地址。如果选定的后端位于其他node, 需要通过VXLAN转发到其他节点,则出口设备为flannel.1, 因而后端pod上看到的源IP为该nodeflannel.1IP, 比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@master1 ~]# kubectl exec -it curl1-54dbd6b8cb-vm5wc -- curl http://10.240.0.101:31554
Hostname: whoami-5df4df6ff5-2frx6
IP: 127.0.0.1
IP: ::1
IP: 10.230.74.7
IP: fe80::20c8:faff:fe77:7a40
RemoteAddr: 10.230.74.1:37722
GET / HTTP/1.1
Host: 10.240.0.101:31554
User-Agent: curl/7.35.0
Accept: */*

[root@master1 ~]# kubectl exec -it curl1-54dbd6b8cb-vm5wc -- curl http://10.240.0.101:31554
Hostname: whoami-5df4df6ff5-84tpp
IP: 127.0.0.1
IP: ::1
IP: 10.230.95.7
IP: fe80::5433:63ff:fe98:6d1e
RemoteAddr: 10.230.74.0:37726
GET / HTTP/1.1
Host: 10.240.0.101:31554
User-Agent: curl/7.35.0
Accept: */*

总结来说,kube-proxyiptables模式主要用到这些自定义链:

  • 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-*

参考: