本文通过下面的例子,分析访问service ip的流程及iptables规则如何生效。

创建service

通过此yaml文件创建三个pod,一个client,两个nginx(监听在80端口),和一个service(将9999映射到nginx的80端口),实现到nginx后端的负载均衡。

[master-1 ~]# cat pod.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx1
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      nodeSelector:
        kubernetes.io/hostname: pccc-203-10-worker-1
      containers:
        - name: hello
          image: nginx
          ports:
            - name: http
              containerPort: 80

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx2
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      nodeSelector:
        kubernetes.io/hostname: pccc-203-10-worker-1
      containers:
        - name: hello
          image: nginx
          ports:
            - name: http
              containerPort: 80

---
apiVersion: v1
kind: Service
metadata:
  name: test
spec:
  selector:
    app: nginx
  ports:
  - protocol: TCP
    port: 9999
    targetPort: 80

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
spec:
  selector:
    matchLabels:
      app: test
  replicas: 1
  template:
    metadata:
      labels:
        app: test
    spec:
      nodeSelector:
        kubernetes.io/hostname: pccc-203-10-worker-2
      containers:
        - name: hello
          image: dgarros/tcpreplay:latest

[master-1 ~]# kubectl apply -f pod.yaml
deployment.apps/nginx1 created
deployment.apps/nginx2 created
service/test created
deployment.apps/client created

查看创建的三个pod,两个nginx pod部署在worker1上,client部署在worker2上。

[master-1 ~]# kubectl get pod -o wide
NAME                         READY   STATUS      RESTARTS   AGE   IP             NODE                   NOMINATED NODE   READINESS GATES
client-6688779b7f-dzkwv      1/1     Running     0          93s   10.1.236.141   worker-2   <none>           <none>
nginx1-6fbbb6bf5c-5trp7      1/1     Running     0          45s   10.1.139.84    worker-1   <none>           <none>
nginx2-6fbbb6bf5c-t2b5b      1/1     Running     0          45s   10.1.139.93    worker-1   <none>           <none>

查看创建的service,可看到对应的两个endpoint。

[master-1 ~]# kubectl describe svc test
Name:              test
Namespace:         default
Labels:            <none>
Annotations:       Selector:  app=nginx
Type:              ClusterIP
IP:                10.99.64.233
Port:              <unset>  9999/TCP
TargetPort:        80/TCP
Endpoints:         10.1.139.84:80,10.1.139.93:80
Session Affinity:  None
Events:            <none>

service创建成功后,会在每个worker node上添加如下iptable规则

#下面8条规则是公共部分
:KUBE-SERVICES - [0:0]
:KUBE-MARK-MASQ - [0:0]
:KUBE-POSTROUTING - [0:0]

-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES

-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE

-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000

#下面几条规则是创建一个service添加的
#如果在worker node上访问service ip,需要给数据包加标记  0x4000/0x4000,以便在POSTROUTING做snat。
-A KUBE-SERVICES ! -s 10.1.0.0/16 -d 10.99.64.233/32 -p tcp -m comment --comment "default/test: cluster IP" -m tcp --dport 9999 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.99.64.233/32 -p tcp -m comment --comment "default/test: cluster IP" -m tcp --dport 9999 -j KUBE-SVC-IOIC7CRUMQYLZ32S
#通过这两条规则做负载均衡
-A KUBE-SVC-IOIC7CRUMQYLZ32S -m comment --comment "default/test:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-LOGU6L2JYVKGEHXE
-A KUBE-SVC-IOIC7CRUMQYLZ32S -m comment --comment "default/test:" -j KUBE-SEP-ZSSX2RT66T6MAGNI
#如果访问service的pod正好是提供服务的pod,也需要给数据包加标记0x4000/0x4000,以便在POSTROUTING做snat。
-A KUBE-SEP-LOGU6L2JYVKGEHXE -s 10.1.139.84/32 -m comment --comment "default/test:" -j KUBE-MARK-MASQ
-A KUBE-SEP-LOGU6L2JYVKGEHXE -p tcp -m comment --comment "default/test:" -m tcp -j DNAT --to-destination 10.1.139.84:80
#如果访问service的pod正好是提供服务的pod,也需要给数据包加标记0x4000/0x4000,以便在POSTROUTING做snat。
-A KUBE-SEP-ZSSX2RT66T6MAGNI -s 10.1.139.93/32 -m comment --comment "default/test:" -j KUBE-MARK-MASQ
-A KUBE-SEP-ZSSX2RT66T6MAGNI -p tcp -m comment --comment "default/test:" -m tcp -j DNAT --to-destination 10.1.139.93:80

访问service ip

有如下三种访问service ip的场景,下面分别验证并分析iptables规则
a. 在client pod内部访问
b. 在worker node上访问
c. 在监听80端口的nginx pod中访问

a. 在client pod内部访问

image.png

  1. 第一步数据包从pod内部发出去,eth0类型为veth,调用 veth_xmit 发送。
  2. eth0和host上的calie8f03783e20成对存在,即从eth0发送的数据包会达到calie8f03783e20。在 calie8f03783e20 调用 netif_rx_internal 进入worker上的软中断处理函数,开始执行worker上的内核协议栈流程。
  3. 在iptables的PREROUTING处,首先经过conntrack处理,创建链接跟踪表项,记录数据包的状态。可通过conntrack命令查看链接跟踪表项

[worker-2 ~]# conntrack -L | grep 10.1.236.141
tcp      6 102 TIME_WAIT src=10.1.236.141 dst=10.99.64.233 sport=44462 dport=9999 src=10.1.139.84 dst=10.1.236.141 sport=80 dport=44462 [ASSURED] mark=0 use=1

然后进入nat表的处理,在PREROUTING 链上依次查找如下的规则

#跳转到链KUBE-SERVICES
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
#数据包从pod内部发出,源ip为10.1.0.0/16网段,不满足此条规则
-A KUBE-SERVICES ! -s 10.1.0.0/16 -d 10.99.64.233/32 -p tcp -m comment --comment "default/test: cluster IP" -m tcp --dport 9999 -j KUBE-MARK-MASQ
#数据包目的ip为10.99.64.233,四层协议为tcp,目的端口为9999,满足此条规则,跳转到 KUBE-SVC-IOIC7CRUMQYLZ32S
-A KUBE-SERVICES -d 10.99.64.233/32 -p tcp -m comment --comment "default/test: cluster IP" -m tcp --dport 9999 -j KUBE-SVC-IOIC7CRUMQYLZ32S
#下面两条为负载均衡机制,因为有两个后端pod,所以只有两条,
#如果有多个pod,就会有多条规则。--probability 0.50000000000表
#示命令概率为50%。注意这里的负载均衡完全是随机的,不会考虑通五元组流发给同一个pod
-A KUBE-SVC-IOIC7CRUMQYLZ32S -m comment --comment "default/test:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-LOGU6L2JYVKGEHXE
-A KUBE-SVC-IOIC7CRUMQYLZ32S -m comment --comment "default/test:" -j KUBE-SEP-ZSSX2RT66T6MAGNI
#假如命中前一个规则,则执行下面两条规则。
#源ip不为10.1.139.84/32,不满足此规则
-A KUBE-SEP-LOGU6L2JYVKGEHXE -s 10.1.139.84/32 -m comment --comment "default/test:" -j KUBE-MARK-MASQ
#满足最后一条规则,target为dnat到 10.1.139.84:80
-A KUBE-SEP-LOGU6L2JYVKGEHXE -p tcp -m comment --comment "default/test:" -m tcp -j DNAT --to-destination 10.1.139.84:80

-A KUBE-SEP-ZSSX2RT66T6MAGNI -s 10.1.139.93/32 -m comment --comment "default/test:" -j KUBE-MARK-MASQ
-A KUBE-SEP-ZSSX2RT66T6MAGNI -p tcp -m comment --comment "default/test:" -m tcp -j DNAT --to-destination 10.1.139.93:80

虽然POSTROUTING链上也有规则,但是都不匹配。
所以查找nat表的结果就是做了dnat。

  1. 将目的ip转换成 10.1.139.84后,查找到如下路由表项,

10.1.139.64/26 via 192.168.2.2 dev tunl0
  1. 在tunl0处,封装成ipip报文,外层目的ip为192.168.2.2,再次查找路由表,最终从em1发送出去

192.168.2.0/24 dev em1
  1. 在worker1上,em1收到报文后,交给ipip模块处理,在tunl0处去掉外层ip,查找路由表可知,将报文发送给calic7bfae5c264

10.1.139.93 dev calic7bfae5c264
  1. calic7bfae5c264调用veth_xmit发送到nginx pod的eth0,报文开始执行pod内部协议栈流程,最终发给nginx服务。

b. 在worker node上访问

image.png

  1. 在worker node上访问service ip时,首先查找路由表,匹配到默认路由

default via 192.168.2.254 dev em1
  1. 在iptables的OUTPUT处,首先经过conntrack处理,创建链接跟踪表项,记录数据包的状态。可通过conntrack命令查看链接跟踪表项

[worker-2 ~]# curl 10.99.64.233:9999
[worker-2 ~]# conntrack -L | grep 10.99.64.233
tcp      6 118 TIME_WAIT src=192.168.2.3 dst=10.99.64.233 sport=36064 dport=9999 src=10.1.139.93 dst=10.1.236.128 sport=80 dport=36064 [ASSURED] mark=0 use=1

然后进入nat表的处理,依次查找如下的规则

#跳转到链KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
#数据包从worker node发出,源ip不是10.1.0.0/16网段,满足此条规则,跳转到链KUBE-MARK-MASQ
-A KUBE-SERVICES ! -s 10.1.0.0/16 -d 10.99.64.233/32 -p tcp -m comment --comment "default/test: cluster IP" -m tcp --dport 9999 -j KUBE-MARK-MASQ
#在此链上,给数据包做标记
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
#继续匹配,满足此规则
-A KUBE-SERVICES -d 10.99.64.233/32 -p tcp -m comment --comment "default/test: cluster IP" -m tcp --dport 9999 -j KUBE-SVC-IOIC7CRUMQYLZ32S
#下面两条为负载均衡机制,因为有两个后端pod,所以只有两条,
#如果有多个pod,就会有多条规则。--probability 0.50000000000表
#示命令概率为50%。注意这里的负载均衡完全是随机的,不会考虑通五元组流发给同一个pod
-A KUBE-SVC-IOIC7CRUMQYLZ32S -m comment --comment "default/test:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-LOGU6L2JYVKGEHXE
-A KUBE-SVC-IOIC7CRUMQYLZ32S -m comment --comment "default/test:" -j KUBE-SEP-ZSSX2RT66T6MAGNI
#假如命中前一个规则,则执行下面两条规则。
#源ip不为10.1.139.84/32,不满足此规则
-A KUBE-SEP-LOGU6L2JYVKGEHXE -s 10.1.139.84/32 -m comment --comment "default/test:" -j KUBE-MARK-MASQ
#满足最后一条规则,target为dnat到 10.1.139.84:80
-A KUBE-SEP-LOGU6L2JYVKGEHXE -p tcp -m comment --comment "default/test:" -m tcp -j DNAT --to-destination 10.1.139.84:80

-A KUBE-SEP-ZSSX2RT66T6MAGNI -s 10.1.139.93/32 -m comment --comment "default/test:" -j KUBE-MARK-MASQ
-A KUBE-SEP-ZSSX2RT66T6MAGNI -p tcp -m comment --comment "default/test:" -m tcp -j DNAT --to-destination 10.1.139.93:80

在OUTPUT链上,匹配到dnat规则,将数据包的目的ip/port换成了10.1.139.84:80或者10.1.139.93:80,并且给数据包做了标记0x4000/0x4000。

  1. 因为目的ip被修改了,所以重新查找路由表,如下,下一跳为192.168.2.2,经过经过tunl0发送出去

10.1.139.64/26 via 192.168.2.2 dev tunl0
  1. 在POSTROUTING链查找如下规则,因为之前已经给数据包加了标记,此处可以匹配成功。

-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
#目标为MASQUERADE,意味着需要做snat
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE

根据MASQUERADE做snat时,源地址选择可以通过下面命令获取,在此例中,源ip为10.1.236.128

[root@pccc-203-10-worker-2 ~]# ip route get 10.1.139.84
10.1.139.84 via 192.168.2.2 dev tunl0 src 10.1.236.128
    cache
  1. 接下来需要发给tunl0,在此处给报文封装外层ip,外层目的ip为192.168.2.2,再次查找路由表,需要通过em1发送出去

192.168.2.0/24 dev em1
  1. 数据包到底worker1后的处理和场景a一样。

c. 在nginx pod访问
这里还要再分两种场景,负载均衡后的ip是发起访问的pod和不是发起访问的pod。比如 在nginx1 pod内部访问nginx的service服务,负载均衡后的ip为nginx1 pod的ip,或者为nginx2 pod的ip。

不同pod
在nginx1 pod内部访问nginx的service服务,负载均衡后的ip为nginx2 pod的ip。

image.png


假设从nginx1 pod内部访问service,前面部分和场景a是一样的,在PREROUTING的nat表处做dnat,

#跳转到链KUBE-SERVICES
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
#数据包从pod内部发出,源ip为10.1.0.0/16网段,不满足此条规则
-A KUBE-SERVICES ! -s 10.1.0.0/16 -d 10.99.64.233/32 -p tcp -m comment --comment "default/test: cluster IP" -m tcp --dport 9999 -j KUBE-MARK-MASQ
#数据包目的ip为10.99.64.233,四层协议为tcp,目的端口为9999,满足此条规则,跳转到 KUBE-SVC-IOIC7CRUMQYLZ32S
-A KUBE-SERVICES -d 10.99.64.233/32 -p tcp -m comment --comment "default/test: cluster IP" -m tcp --dport 9999 -j KUBE-SVC-IOIC7CRUMQYLZ32S
#下面两条为负载均衡机制,因为有两个后端pod,所以只有两条,
#如果有多个pod,就会有多条规则。--probability 0.50000000000表
#示命令概率为50%。注意这里的负载均衡完全是随机的,不会考虑通五元组流发给同一个pod
-A KUBE-SVC-IOIC7CRUMQYLZ32S -m comment --comment "default/test:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-LOGU6L2JYVKGEHXE
-A KUBE-SVC-IOIC7CRUMQYLZ32S -m comment --comment "default/test:" -j KUBE-SEP-ZSSX2RT66T6MAGNI
#假如命中后一个规则,则不会执行下面两条规则。
-A KUBE-SEP-LOGU6L2JYVKGEHXE -s 10.1.139.84/32 -m comment --comment "default/test:" -j KUBE-MARK-MASQ
-A KUBE-SEP-LOGU6L2JYVKGEHXE -p tcp -m comment --comment "default/test:" -m tcp -j DNAT --to-destination 10.1.139.84:80
#负载均衡选择下面两条规则
#源ip为10.1.139.84,不满足此规则
-A KUBE-SEP-ZSSX2RT66T6MAGNI -s 10.1.139.93/32 -m comment --comment "default/test:" -j KUBE-MARK-MASQ
#满足此规则,做dnat
-A KUBE-SEP-ZSSX2RT66T6MAGNI -p tcp -m comment --comment "default/test:" -m tcp -j DNAT --to-destination 10.1.139.93:80

将目的ip修改为10.1.139.93后,查找路由表时,发现只需要发给本worker上的calic6244c9748e即可。

10.1.139.93 dev calic7bfae5c264

此场景下的链接跟踪表项

[worker-1 ~]# conntrack -L | grep 10.99.64.233
tcp      6 17 TIME_WAIT src=10.1.139.84 dst=10.99.64.233 sport=43328 dport=9999 src=10.1.139.93 dst=10.1.139.84 sport=80 dport=43328 [ASSURED] mark=0 use=1

同一个pod
在nginx1 pod内部访问nginx的service服务,负载均衡后的ip为nginx1 pod的ip。和上面的场景的区别是,不只做dnat,还要做snat。

image.png


假设从nginx1 pod内部访问service,前面部分和场景a是一样的,在PREROUTING的nat表处做dnat,

#跳转到链KUBE-SERVICES
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
#数据包从pod内部发出,源ip为10.1.0.0/16网段,不满足此条规则
-A KUBE-SERVICES ! -s 10.1.0.0/16 -d 10.99.64.233/32 -p tcp -m comment --comment "default/test: cluster IP" -m tcp --dport 9999 -j KUBE-MARK-MASQ
#数据包目的ip为10.99.64.233,四层协议为tcp,目的端口为9999,满足此条规则,跳转到 KUBE-SVC-IOIC7CRUMQYLZ32S
-A KUBE-SERVICES -d 10.99.64.233/32 -p tcp -m comment --comment "default/test: cluster IP" -m tcp --dport 9999 -j KUBE-SVC-IOIC7CRUMQYLZ32S
#下面两条为负载均衡机制,因为有两个后端pod,所以只有两条,
#如果有多个pod,就会有多条规则。--probability 0.50000000000表
#示命令概率为50%。注意这里的负载均衡完全是随机的,不会考虑通五元组流发给同一个pod
-A KUBE-SVC-IOIC7CRUMQYLZ32S -m comment --comment "default/test:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-LOGU6L2JYVKGEHXE
-A KUBE-SVC-IOIC7CRUMQYLZ32S -m comment --comment "default/test:" -j KUBE-SEP-ZSSX2RT66T6MAGNI
#假如命中前一个规则,则执行下面两条规则。
#源ip为10.1.139.84,满足此规则,跳转到KUBE-MARK-MASQ
#给数据包做标记 0x4000/0x4000
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-SEP-LOGU6L2JYVKGEHXE -s 10.1.139.84/32 -m comment --comment "default/test:" -j KUBE-MARK-MASQ
#满足此规则,做dnat
-A KUBE-SEP-LOGU6L2JYVKGEHXE -p tcp -m comment --comment "default/test:" -m tcp -j DNAT --to-destination 10.1.139.84:80
#不执行下面两条规则
-A KUBE-SEP-ZSSX2RT66T6MAGNI -s 10.1.139.93/32 -m comment --comment "default/test:" -j KUBE-MARK-MASQ
-A KUBE-SEP-ZSSX2RT66T6MAGNI -p tcp -m comment --comment "default/test:" -m tcp -j DNAT --to-destination 10.1.139.93:80

将目的ip修改为10.1.139.84后,查找路由表时,发现只需要发给本worker上的calic6244c9748e。

10.1.139.84 dev calif67c1668c34

但是在POSTROUTING处,还需要执行如下两条规则,因为数据包已经被打上标记0x4000/0x4000,所以在这里还要执行MASQUERADE,即snat

-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE

snat后的源ip可以通过如下命令获取

[root@pccc-203-10-worker-1 ~]# ip route get 10.1.139.84
10.1.139.84 dev calif67c1668c34 src 192.168.2.2

最后数据包经过dnat和snat后发给给本worker node上的calif67c1668c34。

此场景下的链接跟踪表项

[worker-1 ~]# conntrack -L | grep 10.99.64.233
tcp      6 117 TIME_WAIT src=10.1.139.84 dst=10.99.64.233 sport=48544 dport=9999 src=10.1.139.84 dst=192.168.2.2 sport=80 dport=16495 [ASSURED] mark=0 use=1

也可参考:k8s 之 service ip - 简书 (jianshu.com) 

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐