kubernetes service如何通过iptables转发
# kubernetes service如何通过iptables转发
# 前言
本文主要是介绍kubernetes的service是如何利用iptables来进行流量的转发达到流量的负载均衡的,并会通过实践操作来更好的理解与验证其原理。
# 环境搭建
搭建环境如下图所示:
- 创建一个deploy,设置其副本数为3
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp-deployment
labels:
app: demoapp
spec:
replicas: 3
selector:
matchLabels:
app: demoapp
template:
metadata:
labels:
app: demoapp
spec:
containers:
- name: ubuntu
image: ikubernetes/demoapp:v1.0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 创建service,为上面deployment的3个pod进行负载均衡访问
apiVersion: v1
kind: Service
metadata:
name: demoapp-service
spec:
selector:
app: demoapp
clusterIP: 192.44.140.73
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
2
3
4
5
6
7
8
9
10
11
12
13
- 验证
创建完后,环境如下:
# kubectl get pods -n zwf -o wide
NAME READY STATUS RESTARTS AGE IP
demoapp-deployment-64bdcb8666-4prbk 1/1 Running 0 7d17h 192.33.229.12 dmoc-fa163eee1e30
demoapp-deployment-64bdcb8666-cxmmz 1/1 Running 0 7d17h 192.33.73.139 dmoc-fa163e7188d6
demoapp-deployment-64bdcb8666-kkzsg 1/1 Running 0 7d17h 192.33.206.93 dmoc-fa163ee6bbe1
demoapp-pod 1/1 Running 0 4d17h 192.33.73.172 dmoc-fa163e7188d6
# kubectl get svc -n zwf
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
demoapp-service ClusterIP 192.44.140.73 <none> 80/TCP 7s
2
3
4
5
6
7
8
9
10
可以直接在环境中请求service的clusterip地址,会随机访问到三个pod,输出的信息包含了请求IP和目的IP。
# curl 192.44.140.73
iKubernetes demoapp v1.0 !! ClientIP: 10.10.10.16, ServerName: demoapp-deployment-64bdcb8666-kkzsg, ServerIP: 192.33.206.93!
# curl 192.44.140.73
iKubernetes demoapp v1.0 !! ClientIP: 10.65.196.41, ServerName: demoapp-deployment-64bdcb8666-cxmmz, ServerIP: 192.33.73.139!
# curl 192.44.140.73
iKubernetes demoapp v1.0 !! ClientIP: 10.10.10.16, ServerName: demoapp-deployment-64bdcb8666-4prbk, ServerIP: 192.33.229.12!
2
3
4
5
6
7
8
# KUBE-SERVICES
在PREROUTING
和OUTPUT
链的nat表中,都包含了KUBE-SERVICES
子链,该子链中就包含了serivce的iptables规则的配置。
# iptables -L PREROUTING -t nat
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
KUBE-SERVICES all -- anywhere anywhere /* kubernetes service portals */
cali-PREROUTING all -- anywhere anywhere /* cali:6gwbT8clXdHdC1b1 */
# iptables -L OUTPUT -t nat
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
KUBE-SERVICES all -- anywhere anywhere /* kubernetes service portals */
cali-OUTPUT all -- anywhere anywhere /* cali:tVnHkvAo15HuiPy0 */
2
3
4
5
6
7
8
9
10
11
为什么在PREROUTING和OUTPUT链中添加service规则?
service的作用是负载均衡,也就是需要将IP包进行DNAT操作,而这个操作需要在最靠近发送的位置。网卡收到的包进入到网络协议栈时,最先进入的就是PREROUTING链,也就是节点外部的流量进入的入口。OUTPUT是本地进程发出的包最先进入的链。所以需要在这两条链中添加KUBE-SERVICES
,详情可参考快速了解iptables (opens new window)
为什么nat表?
因为要做DNAT操作,所以在nat表。
KUBE-SERVICES下面有许多KUBE-SVC-XXX的子链,每一条子链都是一个集群中一个service的配置,筛选一下
通过筛选demoapp-service
得到下面的子链,只有访问目的地址为192.44.140.73才能进入到该子链中。
# iptables -L KUBE-SERVICES -t nat | grep demoapp-service
KUBE-SVC-FIHIHDNRZEG5VQEQ tcp -- anywhere 192.44.140.73 /* zwf/demoapp-service:http cluster IP */ tcp dpt:http
2
查看UBE-SVC-FIHIHDNRZEG5VQEQ
链中内容,其中包含了四条子链。
# iptables -L KUBE-SVC-FIHIHDNRZEG5VQEQ -t nat -n
Chain KUBE-SVC-FIHIHDNRZEG5VQEQ (1 references)
target prot opt source destination
KUBE-MARK-MASQ tcp -- !192.33.0.0/16 192.44.140.73 /* zwf/demoapp-service:http cluster IP */ tcp dpt:80
KUBE-SEP-FO7YK2K5DAKTCVSE all -- 0.0.0.0/0 0.0.0.0/0 /* zwf/demoapp-service:http */ statistic mode random probability 0.33333333349
KUBE-SEP-IPKL7NSNTVGKI4T5 all -- 0.0.0.0/0 0.0.0.0/0 /* zwf/demoapp-service:http */ statistic mode random probability 0.50000000000
KUBE-SEP-BPER562ISF3UFYOQ all -- 0.0.0.0/0 0.0.0.0/0 /* zwf/demoapp-service:http */
2
3
4
5
6
7
第一条链是,源IP不在192.33.0.0/16段、目的IP为192.44.140.73并且目的端口是80的数据包会匹配到该条子链进行跳转。其中192.33.0.0/16是pod的网段,如果不是集群内的流量,则会匹配上该条子链,会打上0x4000的标记。
# iptables -L KUBE-MARK-MASQ -t nat -n
Chain KUBE-MARK-MASQ (548 references)
target prot opt source destination
MARK all -- 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000
2
3
4
后面的三条链是为了将流量随机的分配到任意一个pod中。第二条链代表着有1/3的概率会匹配到该链,而第三条链代表着1/2,最后一条链代表着百分百。
看第二条链中的规则,第一条规则是跳转到子链KUBE-MARK-MASQ
,源IP为192.33.206.93的数据包打上了0x4000标记,再将目的IP DNAT成192.33.206.93,说明如果自己访问自己,也会被打上标记。
这里的192.33.206.93是本身service所匹配上的pod IP地址,如果调用自己的service则会匹配上该链,打上0x4000标记。
而第二条规则是一个DNAT操作,会将目标地址改为192.33.206.93:80,也就是pod的地址。
# iptables -L KUBE-SEP-FO7YK2K5DAKTCVSE -t nat
Chain KUBE-SEP-FO7YK2K5DAKTCVSE (1 references)
target prot opt source destination
KUBE-MARK-MASQ all -- 192.33.206.93 anywhere /* zwf/demoapp-service:http */
DNAT tcp -- anywhere anywhere /* zwf/demoapp-service:http */ tcp to:192.33.206.93:80
2
3
4
5
剩下的第3、4条规则也是一样的操作,会将流量DNAT到对应的pod中。
# POSTROUTING
POSTROUTING中包含了一条KUBE-POSTROUTING子链,子链也是由kube-porxy来写入的。
# iptables -L POSTROUTING -t nat
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
KUBE-POSTROUTING all -- anywhere anywhere /* kubernetes postrouting rules */
cali-POSTROUTING all -- anywhere anywhere /* cali:O3lYWMrLQYEMJtB5 */
2
3
4
5
查看子链KUBE-POSTROUTING
内容:
- 第一条规则是没有标记为0x400的数据包会return,不执行下面的子链。还记得在
KUBE-PREROUTING
中有两次打上0x4000标记,代表着访问是集群外部访问该service或者是源和目的一样,都会往下走。 - 第二条规则是对数据包的标记值进行异或,而这里是0x4000与0x4000进行异或,也就是0
- 第三条规则时对该数据包进行SNAT,random-fully是对源端口进行随机获取。
# iptables -L KUBE-POSTROUTING -t nat
Chain KUBE-POSTROUTING (1 references)
target prot opt source destination
RETURN all -- anywhere anywhere mark match ! 0x4000/0x4000
MARK all -- anywhere anywhere MARK xor 0x4000
MASQUERADE all -- anywhere anywhere /* kubernetes service traffic requiring SNAT */ random-fully
2
3
4
5
6
这里有两种情况会被SNAT,说说为什么。
# SNAT
# 为什么集群外部访问该service需要被SNAT?
数据包需要回包,如果没有SNAT,那么回包的目的IP是客户端的IP,而源IP是Pod的IP,而客户端并不认该数据包,则会被丢弃。
如果有发生SNAT,那么回包的时候可以回到Node1的节点,再回到client。
但问题又来了,是怎么从Node1再回包到client的呢?
# conntrack
conntrack是一个追踪表,在每一个数据包进入到netfilter的时候,都会记录一条记录,当我们进行SNAT和DNAT的时候也都会更新该表中的记录。
所以在Node1回包给client时候,可以通过查询这个表中的记录,然后再次SNAT和DNAT恢复回去就可以进行正常的回包了。
可以看下conntrack里面的记录长什么样子。
先在某一台节点上curl service的ip,上图的client其实也是在Node1上的,因为Node1上有两张网卡,Node1和Node2的通信使用的网卡eth2 ip为10.10.10.16,因为转发到其他节点,所以会被SNAT。
# curl 192.44.140.73
iKubernetes demoapp v1.0 !! ClientIP: 10.10.10.16, ServerName: demoapp-deployment-64bdcb8666-4prbk, ServerIP: 192.33.229.12!
2
这里主要是看src和dst分别是client请求service的目的IP,然后后面又有一个src和dst它是由Node2回包给Node1的源和目的IP,也就是源是pod的IP,dst是Node1节点IP。
前面是发送包的方向,后面是回包的方向。通过查询该表,即可以从Node1回包给client
# conntrack -L | grep 192.33.229.12
tcp 6 107 TIME_WAIT src=10.65.196.41 dst=192.44.140.73 sport=33468 dport=80 src=192.33.229.12 dst=10.10.10.16 sport=80 dport=18623 [ASSURED] mark=0 use=1
2
# 源和目的IP相同
当pod访问service时,正好DNAT成自己的IP,那么就存在源和目的IP一样的情况。
这种数据包是很特殊的存在,网络设备可能会将这种数据包视为无效或者异常的包被丢弃,最好不要一样,所以会将源IP SNAT为主机的IP,解决该问题。
实验
当前节点的IP为:10.65.196.41,然后进入在当前节点的Pod内,当前pod的ip为:192.33.73.139
kubectl exec -it -n zwf demoapp-deployment-64bdcb8666-cxmmz -- sh
2
进行多次curl service ip,当某一次请求到本POD的IP时,查看其ClientIP,发现其源IP是:10.65.196.41,这个当前节点的IP
# curl 192.44.140.73
iKubernetes demoapp v1.0 !! ClientIP: 10.65.196.41, ServerName: demoapp-deployment-64bdcb8666-cxmmz, ServerIP: 192.33.73.139!
2
# NodePort
上面介绍的是serivi type=ClusterIP方式的,那么NodePort有什么区别呢?
# 搭建环境
先将clusterIP的service删了,防止干扰。
kubectl delete svc -n zwf demoapp-service
创建NodePort的service文件demo_service_nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: demoapp-nodeport-service
spec:
selector:
app: demoapp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
type: NodePort
2
3
4
5
6
7
8
9
10
11
12
13
创建Service
# kubectl apply -n zwf -f demo_service_nodeport.yaml
# kubectl get svc -n zwf
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
demoapp-nodeport-service NodePort 192.44.152.223 <none> 80:30337/TCP 64s
2
3
4
5
# 请求clusterip
通过clusterip+端口的方式请求service,原理和上面将是一样的。
在POSTROUTING和OUTPUT中添加了KUBE-SERVICES链,其中包含了新增的service对应的KUBE-SVC-XXX子链,当访问的是其clusterip时,则匹配上该链。在其中包含了从多个pod中随机选择一个进行DNAT操作,打上标记,在POSTROUTING中再SNAT。
# iptables -L PREROUTING -t nat
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
KUBE-SERVICES all -- anywhere anywhere /* kubernetes service portals */
cali-PREROUTING all -- anywhere anywhere /* cali:6gwbT8clXdHdC1b1 */
# iptables -L KUBE-SERVICES -t nat | grep zwf
KUBE-SVC-YSWRJGOK5VEB6HOG tcp -- anywhere 192.44.152.223 /* zwf/demoapp-nodeport-service:http cluster IP */ tcp dpt:http
# iptables -L KUBE-SVC-YSWRJGOK5VEB6HOG -t nat |grep zwf
KUBE-MARK-MASQ tcp -- !192.33.0.0/16 192.44.152.223 /* zwf/demoapp-nodeport-service:http cluster IP */ tcp dpt:http
KUBE-MARK-MASQ tcp -- anywhere anywhere /* zwf/demoapp-nodeport-service:http */ tcp dpt:30337
KUBE-SEP-NMMBRSZXI4OIWDPB all -- anywhere anywhere /* zwf/demoapp-nodeport-service:http */ statistic mode random probability 0.33333333349
KUBE-SEP-PXDORKZI3GD2RUMC all -- anywhere anywhere /* zwf/demoapp-nodeport-service:http */ statistic mode random probability 0.50000000000
KUBE-SEP-YLPOR67MTHPHIZAB all -- anywhere anywhere /* zwf/demoapp-nodeport-service:http */
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 请求节点IP
但是如果不是通过ClusterIP而是通过节点IP请求service呢,它是怎么做的?
因为是节点IP,所以无法匹配上该service的KUBE-SVC-XXX子链,但是有一条KUBE-NODEPORTS
# iptables -L KUBE-SERVICES -t nat | grep KUBE-NODEPORTS
KUBE-NODEPORTS all -- anywhere anywhere /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL
2
在其中包含了一条子链KUBE-SVC-YSWRJGOK5VEB6HOG
,只要目的端口为30337就能匹配上,而这个端口正好就是demoapp-nodeport-service
的NodePort端口。
这条子链和上面匹配clusterIP时是同一个链,都是进行一个DNAT操作。
# # iptables -L KUBE-NODEPORTS -t nat | grep zwf
KUBE-SVC-YSWRJGOK5VEB6HOG tcp -- anywhere anywhere /* zwf/demoapp-nodeport-service:http */ tcp dpt:30337
2