k8s网络之服务发现

2022/06/24 k8s 网络 共 4248 字,约 13 分钟
Mikatsuki

背景,为什么需要service

对于以pod提供服务的k8s集群中,如何有效的将pod服务对外提供访问也是一个问题,由于pod的ip是不断变化的,并且一个服务可能同时有多个pod提供服务,如何有效的负载均衡访问这组pod呢,为了解决这两个点便有了service这个概念


service

    service我们可以将其理解为pod对外提供服务的入口,一般来说分为nodeportloadblancerexternalName三种模式,而这三种模式有各自有自己的使用场景,当声明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》

文档信息

Search

    Table of Contents