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即用户空间。为什么这么叫呢?看下面的图。
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
聪明如你,一定已经看明白了吧。
不过kube-proxy的iptables有个缺陷,即当pod故障时无法自动重试,需要依赖readiness probes,主要思想就是创建一个探测容器,当检测到后端pod挂了的时候,更新iptables。