Kubernetes Headless服务

1、概述

Headless Services是一种特殊的service,其spec:clusterIP表示为None,这样在实际运行时就不会被分配ClusterIP,也被称为**无头服务,**通过DNS解析提供服务发现。与普通服务不同的是Headless Services不提供负载均衡功能,每个Pod都有唯一的DNS记录,直接映射到其IP地址,适用于有状态应用的场景,如与StatefulSet一起部署数据库。这种服务使得直接访问单个Pod成为可能,而不经过负载均衡器。

因为 Headless Service 属于 Service ClusterIp 类型,所以在讲解Headless Service前,先简单说下 Service 和服务发现。

2、Service与服务发现

2.1 Service概述

Service主要用于实现对一组Pod的访问,Service 通过标签选择器来关联 Pod 资源,Service 根据访问的端口将对应的请求转发至后端Pod的端口上。Service对象的IP地址(ClusterIP)是虚拟IP地址,仅在 Kubernetes集群内可访问,外部无法访问,可以通过配置 NodePort 或 LoadBalancer 类型的 Service 将集群内的服务暴露给 Kuberenetes 集群外的客户端访问。

备注:Kubernetes部署的服务实例(Pod)不仅可以通过 Service nodePort 和 loadbalancer 的方式暴露给集群外客户端,一般还有以下几种方式暴露给外部访问:

  • 通过hostPort方式在单一节点上做端口映射;
  • 通过Pod的hostNetwork配置让Pod资源使用工作节点上的网络;
  • 使用Ingress 资源。

2.2 Service服务访问原理

本质上来讲,一个Service 对象对应于工作节点内核之中的一组路由规则,这些规则能够将到达Service对象的ClusterIP的流量转发至相应Pod对象的IP地址和端口。

每个工作节点的kube-proxy组件通过API Server持续监听各个Service及其关联的Pod对象,并将Service对象的创建或变动,实时写入到当前工作节点的路由规则上。客户端、Service及Pod对象的关系如下图所示:

2.3 Service类型

Service 一般分为3种类型:ClusterIP、NodePort、LoadBalancer。

(1)ClusterIP

通过集群内部IP 地址暴露服务,CusterIP地址仅在集群内部可以访问,无法被集群外部的客户端访问。

(2)NodePort

NodePort类型,将Service的端口号映射到每个Node的一个端口号上,然后分发给后端的Pod处理。这种类型的Service 既可以被集群内部客户端通过 CIusterIP 直接访问,也可以在集群外部客户端通过nodeIP:nodePort进行访问。

(3)LoadBalancer

LoadBalancer类型建立在 NodePort基础上,将Service映射到一个负载均衡器的IP 地址上,通常在公有云环境中使用。

客户端通过负载均衡器的IP和Service的端号就可以访问到具体的服务,无须再通过 kube-proxy提供的负载均衡机制进行流量转发,可以直接将流量转发到后端 Pod上。

如果是本地搭建LoadBalancer,一般采用metallb方案,官网地址:https://metallb.universe.tf/,有兴趣的朋友自行搭建。

3、Headless Service

简单讲完 Service 和服务发现后,现在回归本文主题,接下来详细讲解下 Headless Service。

3.1 观察Headless Service

由于现有 Kubernetes 集群里面有现成的 headless 服务,所以本文不再创建新的 headless 服务,下面观察下集群已创建的一个名为 openldap 的 headless 服务,以下是 openldap 服务规格定义文件。

复制代码
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/instance: cb-openldap
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: openldap-ha
  name: openldap
spec:
  clusterIP: None  #这使得服务成为headless
  ports:
  - name: ldap
    port: 389
    protocol: TCP
    targetPort: 389
  selector:
    app.kubernetes.io/instance: cb-openldap
    app.kubernetes.io/name: openldap-ha
  sessionAffinity: None
  type: ClusterIP

通过 kubectl get 和 kubectl describe 来查看服务,可以发现他没有集群 IP。

复制代码
[root@cloud ~]# kubectl get svc  openldap 
NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
openldap   ClusterIP   None         <none>        389/TCP   185d

并且它的后端包含与pod选择器匹配的(部分)pod。"部分"是因为pod包含就绪探针,所以只有准备就绪的pod会被列出作为服务转发的上游服务,通过 kubectl describe 命令可以看出openldap 服务有两个就绪Pod(10.233.0.214:389,10.233.9.73:389)。

复制代码
[root@cloud ~]# kubectl describe svc  openldap 
Name:              openldap
Namespace:         default
Labels:            app.kubernetes.io/instance=cb-openldap
                   app.kubernetes.io/managed-by=Helm
                   app.kubernetes.io/name=openldap-ha
Annotations:       meta.helm.sh/release-name: cb-openldap
                   meta.helm.sh/release-namespace: default
Selector:          app.kubernetes.io/instance=cb-openldap,app.kubernetes.io/name=openldap-ha
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                None
IPs:               None
Port:              ldap  389/TCP
TargetPort:        389/TCP
Endpoints:         10.233.0.214:389,10.233.9.73:389
Session Affinity:  None
Events:            <none>

3.2 通过DNS发现Pod

Kubernetes允许客户通过DNS查找发现 Pod IP。对于普通 Kubernetes Service,当执行服务的DNS查找时,DNS服务器会返回单个IP------ClusterIP;但是对于 Headless Service,进行DNS 查找时,DNS服务器将返回的则是 Pod IP 而不是单个服务IP。

DNS服务器不会返回单个DNS A记录,而是会为该服务返回多个A记录,每个记录指向当时支持该服务的单个pod的IP。客户端因此可以做一个简单的DNS A 记录查找并获取属于该服务一部分或者所有pod的IP。客户端可以使用该信息连接到其中的一个、多个或全部。

3.2.1 DNS发现Pod示例

准备一个具有nslooup命令的 Pod(此步骤本文不再赘余,最简单的话可以运行一个 busybox pod),运行此 Pod 后,通过进入 Pod 内部通过执行DNS查找以查看是否获得了实际的pod IP。

(1)对于普通 Kubernetes Service

复制代码
[root@108 ~]# kubectl get svc -n=istio-system |grep jaeger-query
jaeger-query                ClusterIP      10.233.49.189   <none>        16686/TCP,16685/TCP                          661d

进入具有nslooup命令Pod内部。

复制代码
[root@108 ~]# kubectl exec -it  busybox-848d7987f9-wbqq8 /bin/sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
/ # cat /etc/resolv.conf 
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 169.254.25.10
options ndots:5

执行服务DNS查找。

注意:经测试使用nslooup命令解析域名时候,不走search域,所以需要拼全服务名。

(2)对于普通 Headless Service

复制代码
[root@108 ~]# kubectl get svc -n=xxx-middleware |grep kafka-zookeeper-headless
kafka-zookeeper-headless   ClusterIP   None            <none>        2181/TCP,3888/TCP,2888/TCP   646d
[root@108 ~]# kubectl describe svc -n=xxx-middleware kafka-zookeeper-headless 
Name:              kafka-zookeeper-headless
Namespace:         xxx-middleware
Labels:            app=zookeeper
                   app.kubernetes.io/managed-by=Helm
                   chart=zookeeper-2.1.0
                   heritage=Helm
                   release=kafka
Annotations:       meta.helm.sh/release-name: kafka
                   meta.helm.sh/release-namespace: xxx-middleware
Selector:          app=zookeeper,release=kafka
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                None
IPs:               None
Port:              client  2181/TCP
TargetPort:        client/TCP
Endpoints:         10.233.66.179:2181,10.233.66.181:2181,10.233.69.223:2181
Port:              election  3888/TCP
TargetPort:        election/TCP
Endpoints:         10.233.66.179:3888,10.233.66.181:3888,10.233.69.223:3888
Port:              server  2888/TCP
TargetPort:        server/TCP
Endpoints:         10.233.66.179:2888,10.233.66.181:2888,10.233.69.223:2888
Session Affinity:  None
Events:            <none>

进入具有nslooup命令Pod内部。

复制代码
[root@108 ~]# kubectl exec -it  busybox-848d7987f9-wbqq8 /bin/sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
/ # cat /etc/resolv.conf 
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 169.254.25.10
options ndots:5

执行服务DNS查找,解析地址正是服务标签关联Pod Ip(Endpoints: 10.233.66.179,10.233.66.181,10.233.69.223)。

Headless Services还有一个用处,即Headless Service的对应的每一个Endpoints,都会有对应的DNS域名;这样Pod之间就可以互相访问。我们还是看上面的这个例子,通过statefulSet管理,共三个 Pod 实例。

复制代码
[root@108 ~]# kubectl get pods -n=xxx-middleware |grep zoo
kafka-zookeeper-0 1/1 Running 0 85d
kafka-zookeeper-1 1/1 Running 1 96d
kafka-zookeeper-2 1/1 Running 1 96d

现在直接解析指定 Pod 的DNS域名,对应的pod的域名为kafka-zookeeper-0、kafka-zookeeper-1、kafka-zookeeper-2,它们之间可以互相访问,这样对于一些集群类型的应用就可以解决互相之间身份识别的问题了。

注意:尽管headless服务看起来可能与常规服务不同,但在客户端的视角上它们并无不同。即使使用headless服务,集群内客户端也可以通过连接到服务的DNS名称来连接到pod上,就像使用常规服务一样。但是对于headless服务,由于DNS返回了pod的IP, 客户端直接连接到该pod,而不是通过服务代理。headless服务仍然提供跨pod的负载平衡,但是是通过DNS轮询机制,不是通过kube-proxy在工作节点提供的iptables/ipvs路由规则。

3.3 发现所有的Pod--包括未就绪的Pod

只有准备就绪的pod能够作为服务的后端。但有时希望即使pod没有准备就绪,服务发现机制也能够发现所有匹配服务标签选择器的pod。

幸运的是,不必通过查询KubernetesAPI服务器,可以使用DNS查找机制来查找那些未准备好的pod。要告诉Kubernetes无论pod的准备状态如何,希望将所有pod添加到服务中。必须将以下注解添加到服务中:

复制代码
kind: Service 
metadata:
     annotations:
         service.alpha.kubernetes.io/tolerate-unready-endpoints: "true"

示例:

复制代码
[root@cloud ~]# kubectl describe svc -n=xxx-system redis-ha-announce-0 
Name:              redis-ha-announce-0
Namespace:         xxx-system
Labels:            app=redis-ha
                   app.kubernetes.io/managed-by=Helm
                   chart=redis-ha-3.9.0
                   heritage=Helm
                   release=cb-redis
Annotations:       meta.helm.sh/release-name: cb-redis
                   meta.helm.sh/release-namespace: xxx-system
                   service.alpha.kubernetes.io/tolerate-unready-endpoints: true
Selector:          app=redis-ha,release=cb-redis,statefulset.kubernetes.io/pod-name=redis-ha-server-0
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.234.235.210
IPs:               10.234.235.210
Port:              server  6379/TCP
TargetPort:        redis/TCP
Endpoints:         10.233.0.213:6379
Port:              sentinel  26379/TCP
TargetPort:        sentinel/TCP
Endpoints:         10.233.0.213:26379
Session Affinity:  None
Events:            <none>

3.4 其他

Headless服务就是一组Pod组成的只供集群内访问(没有ClusterIP)的Service,一般结合StatefulSet用于部署有状态应用的场景,如果想让部署的有状态应用暴露给集群外部客户端访问的话,可以新建个普通(有ClusterIP)的服务,通过标签选择关联有状态服务实例。

示例:

4、总结

在某些场景中,无需对外提供访问能力,只需要在内部找到自己想找到的Pod资源时,可以通过Headless Service来实现。这种不具有ClusterIP的Service资源就是Headless Service,该 Service 的请求流量不需要 kube-proxy 处理,也不会有负载均衡和路由规则,而是由ClusterDNS的域名解析机制直接去访问固定的Pod资源。

既然是Headless Service,那首先它是Service,一般的Service能被内部和外部访问。之所以叫Headless Service,是因为只对内提供访问,既然只对内访问,那肯定就需要提供稳定的访问能力了,否则就没什么作用了。比如说拥有固定的Pod名称和存储,所以一般会结合StatefulSet一起使用,用来部署有状态的应用。

如果想让部署的有状态应用暴露给集群外部客户端访问的话,可以新建个普通(有ClusterIP)的服务,通过标签选择关联有状态服务实例。

参考:https://www.cnblogs.com/lizexiong/p/14778359.html

参考:http://www.mangod.top/articles/2023/09/04/1693799594643.html

参考:https://www.jianshu.com/p/a6d8b28c88a2