k8s的pod可以有多个副本,但是在访问pod时,会有几个问题:

  • 客户端需要知道各个pod的地址
  • 某一node上的pod如果故障,客户端需要感知

为了解决这个问题,k8s引入了service的概念,用以指导客户端的流量。

Service

以下面的my-nginx为例。

pod和service的定义文件如下:

[root@localhost k8s]# cat run-my-nginx.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80
[root@localhost k8s]# cat run-my-nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    run: my-nginx

pod my-nginx定义的replicas为2即2个副本,端口号为80; service my-nginx定义的selector为run: my-nginx,即该service选中所有label为run: my-nginx的pod;定义的port为80。

使用kubectl create -f xx.yml创建后,可以在集群上看到2个pod,地址分别为10.244.1.10/10.244.2.10;可以看到1个service,IP/Port为10.11.97.177/80,其对接的Endpoints为10.244.1.10:80,10.244.2.10:80,即2个pod的服务地址,这三个URL在集群内任一节点都可以使用curl访问。

[root@localhost k8s]# kubectl get pods -n default -o wide
NAME                       READY     STATUS    RESTARTS   AGE       IP            NODE
my-nginx-379829228-3n755   1/1       Running   0          21h       10.244.1.10   note2
my-nginx-379829228-xh214   1/1       Running   0          21h       10.244.2.10   node1
[root@localhost ~]#
[root@localhost ~]# kubectl describe svc my-nginx
Name:                   my-nginx
Namespace:              default
Labels:                 run=my-nginx
Selector:               run=my-nginx
Type:                   ClusterIP
IP:                     10.11.97.177
Port:                   <unset> 80/TCP
Endpoints:              10.244.1.10:80,10.244.2.10:80
Session Affinity:       None

但是,如果你去查看集群各节点的IP信息,是找不到10.11.97.177这个IP的,那么curl是如何通过这个(Virtual)IP地址访问到后端的Endpoints呢?

答案在这里

kube-proxy

k8s支持2种proxy模式,userspace和iptables。从v1.2版本开始,默认采用iptables proxy。那么这两种模式有什么不同吗?

1、userspace

顾名思义,userspace即用户空间。为什么这么叫呢?看下面的图。

userspace

kube-proxy会为每个service随机监听一个端口(proxy port ),并增加一条iptables规则:所以到clusterIP:Port 的报文都redirect到proxy port;kube-proxy从它监听的proxy port收到报文后,走round robin(默认)或者session affinity(会话亲和力,即同一client IP都走同一链路给同一pod服务),分发给对应的pod。

显然userspace会造成所有报文都走一遍用户态,性能不高,现在k8s已经不再使用了。

2、iptables

我们回过头来看看userspace,既然用户态会增加性能损耗,那么有没有办法不走呢?实际上用户态也只是一个报文LB,通过iptables完全可以搞定。k8s下面这张图很清晰的说明了iptables方式与userspace方式的不同:kube-proxy只是作为controller,而不是server,真正服务的是内核的netfilter,体现在用户态则是iptables。

kube-proxy的iptables方式也支持round robin(默认)和session affinity。

那么iptables是怎么做到LB,而且还能round-robin呢?我们通过iptables-save来看my-nginx这个服务在某一个node上的iptables规则。

-A KUBE-SERVICES -d 10.11.97.177/32 -p tcp -m comment --comment "default/my-nginx: cluster IP" -m tcp --dport 80 -j KUBE-SVC-BEPXDJBUHFCSYIC3

-A KUBE-SVC-BEPXDJBUHFCSYIC3 -m comment --comment "default/my-nginx:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-U4UWLP4OR3LOJBXU
-A KUBE-SVC-BEPXDJBUHFCSYIC3 -m comment --comment "default/my-nginx:" -j KUBE-SEP-QHRWSLKOO5YUPI7O

-A KUBE-SEP-U4UWLP4OR3LOJBXU -s 10.244.1.10/32 -m comment --comment "default/my-nginx:" -j KUBE-MARK-MASQ
-A KUBE-SEP-U4UWLP4OR3LOJBXU -p tcp -m comment --comment "default/my-nginx:" -m tcp -j DNAT --to-destination 10.244.1.10:80

-A KUBE-SEP-QHRWSLKOO5YUPI7O -s 10.244.2.10/32 -m comment --comment "default/my-nginx:" -j KUBE-MARK-MASQ
-A KUBE-SEP-QHRWSLKOO5YUPI7O -p tcp -m comment --comment "default/my-nginx:" -m tcp -j DNAT --to-destination 10.244.2.10:80

第1条规则,终于看到这个virtual IP了。node上不需要有这个ip地址,iptables在看到目的地址为virutal ip的符合规则tcp报文,会走KUBE-SVC-BEPXDJBUHFCSYIC3规则。

第2/3条规则,KUBE-SVC-BEPXDJBUHFCSYIC3链实现了将报文按50%的统计概率随机匹配到2条规则(round-robin)。

第4/5和5/6为成对的2组规则,将报文转给了真正的服务pod。

至此,从物理node收到目的地址为10.11.97.177、端口号为80的报文开始,到pod my-nginx收到报文并响应,描述了一个完整的链路。可以看到,整个报文链路上没有经过任何用户态进程,效率和稳定性都比较高。

NodePort

上面的例子里,由于10.11.97.177其实还是在集群内有效地址,由于实际上并不存在这个地址,当从集群外访问时会访问失败,这时需要将service暴漏出去。k8s给出的一个方案是NodePort,客户端根据NodePort+集群内任一物理节点的IP,就可以访问k8s的service了。这又是怎么做到的呢?

答案还是iptables。我们来看下面这个sock-shop的例子,其创建方法见k8s.io,不再赘述。

[root@localhost k8s]# kubectl describe svc front-end -n sock-shop
Name:                   front-end
Namespace:              sock-shop
Labels:                 name=front-end
Selector:               name=front-end
Type:                   NodePort
IP:                     10.15.9.0
Port:                   <unset> 80/TCP
NodePort:               <unset> 30001/TCP
Endpoints:              10.244.2.5:8079
Session Affinity:       None

在任一node上查看iptables-save:

-A KUBE-NODEPORTS -p tcp -m comment --comment "sock-shop/front-end:" -m tcp --dport 30001 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "sock-shop/front-end:" -m tcp --dport 30001 -j KUBE-SVC-LFMD53S3EZEAOUSJ

-A KUBE-SERVICES -d 10.15.9.0/32 -p tcp -m comment --comment "sock-shop/front-end: cluster IP" -m tcp --dport 80 -j KUBE-SVC-LFMD53S3EZEAOUSJ

-A KUBE-SVC-LFMD53S3EZEAOUSJ -m comment --comment "sock-shop/front-end:" -j KUBE-SEP-SM6TGF2R62ADFGQA

-A KUBE-SEP-SM6TGF2R62ADFGQA -s 10.244.2.5/32 -m comment --comment "sock-shop/front-end:" -j KUBE-MARK-MASQ
-A KUBE-SEP-SM6TGF2R62ADFGQA -p tcp -m comment --comment "sock-shop/front-end:" -m tcp -j DNAT --to-destination 10.244.2.5:8079

聪明如你,一定已经看明白了吧。

要是还不明白,看看这篇文章:源地址审计:追踪 kubernetees 服务的SNAT

不过kube-proxy的iptables有个缺陷,即当pod故障时无法自动重试,需要依赖readiness probes,主要思想就是创建一个探测容器,当检测到后端pod挂了的时候,更新iptables。

在用NodePort的时候,经常会有人问一个问题,NodePort指定的端口(30000+),而client建立tcp连接时,本地端口是操作系统随机选定的(30000+),如何防止产生冲突呢?

解决办法是kube-proxy进程会去起一个tcp listen socket,监听端口号就是NodePort。可以把这个socket理解为“占位符”,目的是为了让操作系统跳开该端口。