k8s 网络转发实现 - (sunznx) 振翅飞翔
29 June 2019

假标题

写这篇文章的原因是:

  1. 一段时间后,老忘记 k8s 的 IP,ClusterIP,NodePort,Port 等等是什么意思
  2. 周末没事做 -.-

环境说明

dnsDomain: cluster.local
podSubnet: 192.167.0.0/16
serviceSubnet: 10.96.0.0/12

k8s (v1.15.0) 部署在 3 台主机上,网络插件使用的是 flannel
centos0  192.168.20.70 [master]
centos1  192.168.20.71
centos2  192.168.20.72

k8s 例子使用的是 https://kubernetes.io/docs/tutorials/stateless-application/guestbook/

$ kubectl get service -o wide
NAME           TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE   SELECTOR
frontend       NodePort    10.99.173.89     <none>        80:30193/TCP   13h   app=guestbook,tier=frontend
kubernetes     ClusterIP   10.96.0.1        <none>        443/TCP        20h   <none>
redis-master   ClusterIP   10.106.223.234   <none>        6379/TCP       13h   app=redis,role=master,tier=backend
redis-slave    ClusterIP   10.106.94.73     <none>        6379/TCP       13h   app=redis,role=slave,tier=backend

$ kubectl describe service frontend
Name:                     frontend
...
Type:                     NodePort
IP:                       10.99.173.89
Port:                     <unset>  80/TCP
TargetPort:               80/TCP
NodePort:                 <unset>  30193/TCP
Endpoints:                192.167.1.2:80,192.167.1.3:80,192.167.1.4:80 + 2 more...
Session Affinity:         None
External Traffic Policy:  Cluster

k8s 模块

在开始下面的分析之前,先看看 k8s 的模块大概有哪些?

主要有 3 个模块, Node, Service, Pod

  1. Node 即我们本机,NodeIP 就是我们本机的 IP(对应我们上面部署的主机 centos0/centos1/centos2),NodePort 即 Service 提供的服务映射到我们本机的端口号
  2. 总所周知 Pod 是一组容器的集合(从网络的角度来看,Pod 相当于一个 docker,Pod 里面的数个 docker 的网络命名空间是共享的)
  3. Service 存在的意义:因为在 k8s 中,Pod 的 IP 是变化的(比如 增/减 pod 个数),Pod 的 IP 有多个。因此抽象出一个 Service,以一个 Service IP 对应多个 Pod IP(例如轮询),ServicePort 对应 Pod 的 Port。另外 Service 在一些场景下,也叫 Cluster,ClusterIP 即 ServiceIP。还有个另外,ServiceIP 是虚拟的,这在最后会解释

Service

Service 类型:

  1. ClusterIP
    不暴露端口,即 ServiceIP 相当于只能在 Service 内部访问
  2. NodePort
    暴露端口,实际上在任何一个 Node 上都可以根据该端口访问到

再看看 Service 的信息

$ kubectl get service -o wide
NAME           TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE   SELECTOR
frontend       NodePort    10.99.173.89     <none>        80:30193/TCP   13h   app=guestbook,tier=frontend
redis-master   ClusterIP   10.106.223.234   <none>        6379/TCP       13h   app=redis,role=master,tier=backend

frontend 的类型是 NodePort,ClusterIP(即 ServiceIP)为 10.99.173.89,ServicePort 为 80,NodePort 30193。可以通过两种方式访问 Frontend Service

  1. 直接访问 Service
$ curl http://10.99.173.89:80
  1. 通过 Node 访问,再转发到 Service
$ curl http://192.168.20.70:30193
$ curl http://192.168.20.71:30193
$ curl http://192.168.20.72:30193

redis-master 的类型是 ClusterIP,访问方式只能通过 Service 直接访问

$ redis-cli -h 10.106.223.234
$ kubectl describe service frontend
Type:       NodePort                                                  # 类型是 NodePort
IP:         10.99.173.89                                              # ServiceIP
Port:       <unset>  80/TCP                                           # ServicePort
TargetPort: 80/TCP                                                    # 对应的 Pod Port
NodePort:   <unset>  30193/TCP                                        # NodePort
Endpoints:  192.167.1.2:80,192.167.1.3:80,192.167.1.4:80 + 2 more...  # 对应 Pod 的 IP:Port

实现

k8s 默认是通过 iptables (也有 用户空间[不懂] 的 和 ipvs [需要设置才能开启])来实现转发的

上面,我们说过,ServiceIP 是虚拟的。虚拟的意思是我们找不到该地址对应的网络设备
当我们要访问 ServiceIP 的时候,iptables 匹配到该 ServiceIP 之后,就会转发到对应的 Pod。原理就这么简单,看得懂 iptables 规则的自然也就看懂了,看不懂的说了等于白说 - -

$ iptables-save -t nat      # 也可以使用 iptables -t nat -L 查看
...

01. -A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
02. -A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING

03. -A KUBE-SERVICES ! -s 192.167.0.0/16 -d 10.99.173.89/32 -p tcp -m comment --comment "default/frontend: cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
04. -A KUBE-SERVICES -d 10.99.173.89/32 -p tcp -m comment --comment "default/frontend: cluster IP" -m tcp --dport 80 -j KUBE-SVC-GYQQTB6TY565JPRW

05. -A KUBE-SVC-GYQQTB6TY565JPRW -m statistic --mode random --probability 0.20000000019 -j KUBE-SEP-5M6IMKFOUATBNJIE
06. -A KUBE-SVC-GYQQTB6TY565JPRW -m statistic --mode random --probability 0.25000000000 -j KUBE-SEP-TOJEKTNBMDCO7OBL
07. -A KUBE-SVC-GYQQTB6TY565JPRW -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-2TZ5ELMBBIM6AEEL
08. -A KUBE-SVC-GYQQTB6TY565JPRW -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-BXGUNY74Y2MJVOFH
09. -A KUBE-SVC-GYQQTB6TY565JPRW -j KUBE-SEP-U2DUTXQ5GM4LVCNQ

10. -A KUBE-SEP-TOJEKTNBMDCO7OBL -s 192.167.1.3/32 -j KUBE-MARK-MASQ
11. -A KUBE-SEP-TOJEKTNBMDCO7OBL -p tcp -m tcp -j DNAT --to-destination 192.167.1.3:80
12. -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
13. -A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE