为什么我在还没有开始讲解Service之前就要拿出来headless Service说一说呢?
因为我自己在回顾知识的时候发现自己并没有想象中的那么懂 Headless Service这个机制。 今天自己再温故学习的同时 输出文档开源供大家公共学习

Headless Service在service中的定位

在Service服务中 ,有三种服务类型, 分别是 ClusterIP、NodePort、LoadBalancer
前者是供集群内访问的 ,不对外暴露。 后两者是提供给公网用户访问 ,用户可以通过 <NodeIP>:<NodePort> 访问服务 。 当然, LoadBalancer需要提供负载均衡器来实现, 负载均衡器自动将流量分配到 NodePort 上的服务 。
说到这里 ,我们都知道 ,他们三个都应该是需要提供ip的。 不管是 对集群内还是集群外。但是, 对于Headless service来说 ,虽然它使用的是ClusterIP, 但它不对外提供ip(他的ClusterIP: None) 没想到吧。 无头服务就是这么来的。

怎么使用?

既然他是无头服务, 那我怎么使用呢? 没有ip就无法访问了吗? 当然不是, kubernetes中的statefulset就为我们提供了一种使用说明书。
既然无头Service不会获取集群ip, kube-proxy也就不会处理这类 Service。 而且kubernentes集群也不会提供给他们负载均衡或路由支持。
无头 Service 不使用虚拟 IP 地址和代理 配置路由和数据包转发相反,无头 Service 通过内部 DNS 记录报告各个 Pod 的端点 IP 地址,这些 DNS 记录是由集群的 DNS 服务所提供的。 这些 DNS 记录是由集群内部 DNS 服务所提供的 要定义无头 Service,你需要将 .spec.type 设置为 ClusterIP(这也是 type 的默认值),**并进一步将 **.spec.clusterIP** 设置为 ****None**
DNS 如何自动配置取决于 Service 是否定义了选择器。
其实换句话说,Headless Service 不会为其后端 Pods 分配一个集群内的虚拟 IP。相反,它会直接返回后端 Pods 的 IP 地址列表。

带选择算符的服务

对定义了选择算符的无头 Service,Kubernetes 控制平面在 Kubernetes API 中创建 EndpointSlice 对象,并且修改 DNS 配置返回 A 或 AAAA 记录(IPv4 或 IPv6 地址), 这些记录直接指向 Service 的后端 Pod 集合。
也就是以 Service 的 VIP(Virtual IP,即:虚拟 IP)方式。比如:当我访问 10.0.23.1 这个 Service 的 IP 地址时,10.0.23.1 其实就是一个 VIP,它会把请求转发到该 Service 所代理的某一个 Pod 上。(就是上面说到的)

无选择算符的服务

对没有定义选择算符的无头 Service,控制平面不会创建 EndpointSlice 对象。 然而 DNS 系统会执行以下操作之一:

  • 对于 type: ExternalName Service,查找和配置其 CNAME 记录;
  • 对所有其他类型的 Service,针对 Service 的就绪端点的所有 IP 地址,查找和配置 DNS A / AAAA 记录:
    • 对于 IPv4 端点,DNS 系统创建 A 记录。
    • 对于 IPv6 端点,DNS 系统创建 AAAA 记录。

当你定义无选择算符的无头 Service 时,port 必须与 targetPort 匹配。

详细点说就是, 比如:只要我访问“my-svc.my-namespace.svc.cluster.local”这条 DNS 记录,就可以访问到名叫 my-svc 的 Service 所代理的某一个 Pod。
而在第二种 Service DNS 的方式下,具体还可以分为两种处理方法:

  • 第一种处理方法,是 Normal Service。这种情况下,你访问“my-svc.my-namespace.svc.cluster.local”解析到的,正是 my-svc 这个 Service 的 VIP,后面的流程就跟 VIP 方式一致了。
  • 第二种处理方法,正是 Headless Service。这种情况下,你访问“my-svc.my-namespace.svc.cluster.local”解析到的,直接就是 my-svc 代理的某一个 Pod 的 IP 地址。可以看到,这里的区别在于,Headless Service 不需要分配一个 VIP,而是可以直接以 DNS 记录的方式解析出被代理 Pod 的 IP 地址。

实例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx

可以看到, 所谓的 Headless Service,其实仍是一个标准 Service 的 YAML 文件。只不过,它的 clusterIP 字段的值是:None,即:这个 Service,没有一个 VIP 作为“头”。这也就是 Headless 的含义。所以,这个 Service 被创建后并不会被分配一个 VIP,而是会以 DNS 记录的方式暴露出它所代理的 Pod。(上面也说到了, 对应)

而它所代理的 Pod,依然是采用 Label Selector 机制选择出来的,即:所有携带了 app=nginx 标签的 Pod,都会被这个 Service 代理起来。
然后关键。当你按照这样的方式创建了一个 Headless Service 之后,它所代理的所有 Pod 的 IP 地址,都会被绑定一个这样格式的 DNS 记录,如下所示:

1
<pod-name>.<svc-name>.<namespace>.svc.cluster.local

image.png

通过 工具包dns-test, 输入nslookup 查询一个特定的 StatefulSet Pod 的 DNS 记录。
image.png
这个 DNS 记录,正是 Kubernetes 项目为 Pod 分配的唯一的“可解析身份”(Resolvable Identity)。
有了这个“可解析身份”,只要你知道了一个 Pod 的名字,以及它对应的 Service 的名字,你就可以非常确定地通过这条 DNS 记录访问到 Pod 的 IP 地址
那么,StatefulSet 又是如何使用这个 DNS 记录来维持 Pod 的拓扑状态的呢?
下面我们来通过一段sts的yaml来看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.9.1
ports:
- containerPort: 80
name: web

看这里serviceName: "nginx" 这个字段。这个 YAML 文件,和我们在前面文章中用到的 nginx-deployment 的唯一区别,就是多了一个 serviceName=nginx 字段。
这个字段的作用,就是告诉 StatefulSet 控制器,在执行控制循环(Control Loop)的时候,请使用 nginx 这个 Headless Service 来保证 Pod 的“可解析身份”。
所以,当你通过 kubectl create 创建了上面这个 Service 和 StatefulSet 之后,就会看到如下两个对象:
image.png
通过上面这个 Pod 的创建过程,我们不难看到,StatefulSet 给它所管理的所有 Pod 的名字,进行了编号,编号规则是:**<statefulset name>-<ordinal index>**
而且这些编号都是从 0 开始累加,与 StatefulSet 的每个 Pod 实例一一对应,绝不重复。
更重要的是,这些 Pod 的创建,也是严格按照编号顺序进行的。比如,在 web-0 进入到 Running 状态、并且细分状态(Conditions)成为 Ready 之前,web-1 会一直处于 Pending 状态。

这个可以通过另一个node来监听, 然后master来做创建动作发现

当这两个 Pod 都进入了 Running 状态之后,你就可以查看到它们各自唯一的“网络身份”了。
我们使用 kubectl exec 命令进入到容器中查看它们的 hostname:

1
2
3
4
$ kubectl exec web-0 -- sh -c 'hostname'
web-0
$ kubectl exec web-1 -- sh -c 'hostname'
web-1

用 nslookup 命令,解析一下 Pod 对应的 Headless Service 就是上面我提供的那个表中的数据。
通过这种严格的对应规则,StatefulSet 就保证了 Pod 网络标识的稳定性
比如,如果 web-0 是一个需要先启动的主节点,web-1 是一个后启动的从节点,那么只要这个 StatefulSet 不被删除,你访问 web-0.nginx 时始终都会落在主节点上,访问 web-1.nginx 时,则始终都会落在从节点上,这个关系绝对不会发生任何变化。
所以,如果我们再用 nslookup 命令,查看一下这个新 Pod 对应的 Headless Service 的话还是可以看到,在这个 StatefulSet 中,这两个新 Pod 的“网络标识”(比如:web-0.nginx 和 web-1.nginx),再次解析到了正确的 IP 地址(比如:web-0 Pod 的 IP 地址 10.244.1.8)。

通过这种方法,Kubernetes 就成功地将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录。

sts的灾备考虑

疑问

前面我们说到

比如,如果 web-0 是一个需要先启动的主节点,web-1 是一个后启动的从节点,那么只要这个 StatefulSet 不被删除,你访问 web-0.nginx 时始终都会落在主节点上,访问 web-1.nginx 时,则始终都会落在从节点上,这个关系绝对不会发生任何变化。

但是, sts管理的pod可能是有启动顺序考虑的, 例如: web-1是依赖于web-0的 ,在启动之初, web-1在web-0正式runing之前是不会启动的, 在running之后, 他才会启动。也就是说他们之间存在着强依赖关系, 但是如果此时删除了web-0, 恰好有用户访问了web-1的某个服务 , 这个服务是依赖着web-0, 但是web-0被删除了 它在sts的控制下重新创建也是需要时间的,也就是说这个服务在这段时间是不可用的。 这显示是一个灾备考虑不周出现的故障了。

如何解决有状态服务的强依赖关系

在高可用性和容灾设计中,单个 Pod 或节点的不可用性不应导致整个服务的中断。所以说这个问题的重视程度应该再涨一个等级。 (经典的单点故障问题)

需要采用多种策略来提高 StatefulSet 的高可用性和容灾能力

  1. 多主架构

如果应用程序支持,使用多主架构而不是主从架构。例如,某些数据库(如 CockroachDB、Cassandra)支持多主节点架构,允许任意节点处理请求,从而避免单点故障。

  1. Pod 反亲和性

通过配置 Pod 反亲和性策略,确保 StatefulSet 的 Pod 分布在不同的节点上,从而提高容灾能力。如果一个节点故障,其他节点上的 Pod 仍然可以提供服务

  1. 服务的多副本

为关键服务部署多个副本,并使用负载均衡器(如 Kubernetes 的 Service 或 Ingress)来分发流量。如果一个副本不可用,流量会自动转发到其他可用副本。

  1. 多区域和多集群部署

在多个地理区域或多个 Kubernetes 集群中部署应用,以确保即使一个区域或集群发生故障,其他区域或集群仍能继续提供服务。这通常涉及跨区域的负载均衡和数据同步策略。

  1. 服务健康检查: 你可以配置Kubernetes的liveness和readiness探针,以检查Pod的健康状况。如果一个Pod出现问题,Kubernetes可以自动重启它。