背景,为什么需要service
对于以pod提供服务的k8s集群中,如何有效的将pod服务对外提供访问也是一个问题,由于pod的ip是不断变化的,并且一个服务可能同时有多个pod提供服务,如何有效的负载均衡访问这组pod呢,为了解决这两个点便有了service这个概念
service
service我们可以将其理解为pod对外提供服务的入口,一般来说分为nodeport、loadblancer、externalName三种模式,而这三种模式有各自有自己的使用场景,当声明service时如果不指定type则默认会是clusterIP类型,本文将以clusterIP类型为例,讲解整个k8s网络访问的流程。
以下是一个描述service的yaml文件,其中selector是说明该service要选定有app=hostnames这组标签的pod,而端口部分则是访问service的80端口则转发到对应pod的9376端口,由于没有指定类型,该service为clusterIP类型。
apiVersion: v1
kind: Service
metadata:
name: hostnames
spec:
selector:
app: hostnames
ports:
- name: default
protocol: TCP
port: 80
targetPort: 9376
而这组被选定的pod被称为该service的endpoints,可以用kubectl get endpoints hostnames
命令查看(列表中的ep均为running状态且通过探针检查的pod):
$ kubectl get endpoints hostnames
NAME ENDPOINTS
hostnames 10.244.0.5:9376,10.244.0.6:9376,10.244.0.7:9376
那么到这儿,pod ip会变化的问题就解决了,因为不管pod ip如何变化,我们在实际进行访问的时候都是访问service的ip,而service ip在一般情况下不会改变,所以对外访问的入口也就固定了。 你可以用kubectl get svc hostnames
命令看到这个service的相关信息,并尝试访问这个ip的80端口,你的请求会轮询到后面的三个pod上。
$ kubectl get svc hostnames
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
hostnames ClusterIP 10.0.1.175 <none> 80/TCP 5s
$ curl 10.0.1.175:80
hostnames-0uton
$ curl 10.0.1.175:80
hostnames-yp2kp
$ curl 10.0.1.175:80
hostnames-bvc05
kube-proxy
上面说到了可以通过访问service的ip来取代直接访问pod ip,那么这个具体又是如何实现的呢,对于service的功能实现则要提到kube-proxy这个组件了,该组件是用于实现内部网络访问的功能,有iptables和ipvs两种模式。
iptables模式是早期的基础模式,通过监听informer中的service创建,来创建对应的iptables规则,而这些iptables规则则实现了从service ip:port到pod ip:port的转发。
用iptables save
命令可以看到已有的iptables规则,观察如下这条规则:
-A KUBE-SERVICES -d 10.0.1.175/32 -p tcp -m comment --comment "default/hostnames: cluster IP" -m tcp --dport 80 -j KUBE-SVC-NWV5X2332I4OT4T3
它将对10.0.1.175:80的访问转KUBE-SVC-NWV5X2332I4OT4T3这条规则链上,而我们继续看这条规则链的详细内容
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-WNBA2IHDGP2BOBGZ
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-X3P2623AGDH6CDF3
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -j KUBE-SEP-57KPRZ3JQVENLNBR
关注–mode random,–probability,-j 这三个参数,一个说明是随机转发模式,一个指定该规则的权重,一个指定下一步跳转。因为iptables规则是从上往下顺序匹配的,所以为保证负载均衡的概率相等,第一条的命中概率设置为1/3,第二条为1/2(第一条没选中则只剩两条了),第三条为1(只剩最后一条了),而这三个跳转的规则其实对应了三个pod的信息:
-A KUBE-SEP-57KPRZ3JQVENLNBR -s 10.244.3.6/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000 -A KUBE-SEP-57KPRZ3JQVENLNBR -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.3.6:9376
-A KUBE-SEP-WNBA2IHDGP2BOBGZ -s 10.244.1.7/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000 -A KUBE-SEP-WNBA2IHDGP2BOBGZ -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.1.7:9376
-A KUBE-SEP-X3P2623AGDH6CDF3 -s 10.244.2.3/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000 -A KUBE-SEP-X3P2623AGDH6CDF3 -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.2.3:9376
这里用dnat将数据包访问的目的地址修改为了后端pod ip,也就实现了由service ip:port访问到pod ip:port的的全流程。
上面介绍了iptables模式,可以看到逻辑非常简单,但是对于不断监听informer不断修正iptables规则,并且随着pod数量增加,这将是一个很庞大的工程,并且由于iptables是一个顺序匹配即O(n)的时间复杂度,对于不断扩张的业务集群来说,并不是一个很好的选择,此时便有了ipvs模式的出现。
IPVS是linux内部自己实现的一套映射规则,其实对于iptable和IPVS来说,IPVS使用了iptables的扩展ipset,ipset的数据结构是类似于map的实现,实际查找ip代理的规则时时O(1)的时间复杂度。
kube-proxy先会创建一个虚拟网卡,并以service的vip作为ip,然后用linux的IPVS模块,为这个ip设置三个IPVS虚拟主机,这三个主机其实就是三个pod的ip及端口
可以直接用ipvsadm -ln
命令查看ipvs的相关规则,他是一个map的实现,直接将service的ip:port应设有三个pod的ip:port,并且以轮询的模式进行访问。
其实ipvs模式并没有性能提升,仅仅是将规则的处理放到了内核态,减轻了规则维护的成本,但本身大量使用iptables就会增加cpu成本,所以这还是一个有效的方案。
dns
对于如何找到容器这点,不单是有直接ip层面的负载均衡,dns也是行之有效的一个思路,拿我们正常web服务的网络访问来做类比,更多的时候是直接访问域名,域名解析这里就能做很多文章,甚至可以直接解析多条记录实现负载均衡。
在k8s中,所有的service和pod都会分配对应A记录,在容器中内部分互相访问更多是使用域名的形式,通常pod直接分配的域名是..pod.cluster.local,而service则是..svc.cluster.local,举个例子,你可以在任意其他容器中访问hostnames.default.svc.cluster.local,这是k8s对于已有service分配的域名。
在这其中有一类比较特殊的模式,即headless-service,无头服务,通常对于service域名的解析,会拿到对应service的ip,如上例的clusterIP类型,会拿到clusterIP,但是如果你在声明service时,指定了clusterIP: None这个属性,则该service会变为无头服务,,这时候解析到的便是这个service所有的endpoints的ip列表了,这个东西在有状态的服务时会用到,后续会展开说明。
总的来说,k8s为了解决找到某一个容器这个问题,有了service、dns这些手段,而service可以通过label和pod做好绑定,而kube-proxy则是通过不同的手段实现了serivce到pod的访问路径,dns则是解决了访问service或者访问pod问题,实现集群内部的互相访问
特别说明:本文有参考张磊老师的《深入剖析 Kubernetes》
文档信息
- 本文作者:Mikatsuki
- 本文链接:https://akitozz.github.io/2022/06/24/k8s-net-1/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)