如何保证应用实例的拓扑状态,在 Pod 删除和再创建的过程中保持稳定
Kubernetes架构master节点有一个很重要的组件-Service,Service 是 Kubernetes 项目中用来将一组 Pod 暴露给外界访问的一种机制。比如,一个 Deployment 有 3 个 Pod,那么我就可以定义一个 Service。然后,用户只要能访问到这个 Service,它就能访问到某个具体的 Pod。
那么,这个 Service 又是如何被访问的呢?
第一种方式,是以 Service 的 VIP(Virtual IP,即:虚拟 IP)方式。
比如:当我访问 10.0.23.1 这个 Service 的 IP 地址时,10.0.23.1 其实就是一个 VIP,它会把请求转发到该 Service 所代理的某一个 Pod 上。
第二种方式,就是以 Service 的 DNS 方式。
比如:这时候,只要我访问“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 地址。
那么,这样的设计又有什么作用呢?
想要回答这个问题,我们需要从 Headless Service 的定义方式看起。
下面是一个标准的 Headless Service 对应的 YAML 文件:
1 | apiVersion: v1 |
可以看到,所谓的 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 |
这个 DNS 记录,正是 Kubernetes 项目为 Pod 分配的唯一的“可解析身份”(Resolvable Identity)。
有了这个“可解析身份”,只要你知道了一个 Pod 的名字,以及它对应的 Service 的名字,你就可以非常确定地通过这条 DNS 记录访问到 Pod 的 IP 地址。
那么,StatefulSet 又是如何使用这个 DNS 记录来维持 Pod 的拓扑状态的呢?
为了回答这个问题,现在我们就来编写一个 StatefulSet 的 YAML 文件,如下所示:
1 | apiVersion: apps/v1 |
这个 YAML 文件,和我们在前面文章中用到的 nginx-deployment 的唯一区别,就是多了一个 serviceName=nginx 字段。
这个字段的作用,就是告诉 StatefulSet 控制器,在执行控制循环(Control Loop)的时候,请使用 nginx 这个 Headless Service 来保证 Pod 的“可解析身份”。
所以,当你通过 kubectl create 创建了上面这个 Service 和 StatefulSet 之后,就会看到如下两个对象:
1 | $ kubectl create -f svc.yaml |
这时候,如果你手比较快的话,还可以通过 kubectl 的 -w 参数,即:Watch 功能,实时查看 StatefulSet 创建两个有状态实例的过程:
备注:如果手不够快的话,Pod 很快就创建完了。不过,你依然可以通过这个 StatefulSet 的 Events 看到这些信息。
1 | $ kubectl get pods -w -l app=nginx |
通过上面这个 Pod 的创建过程,我们不难看到,StatefulSet 给它所管理的所有 Pod 的名字,进行了编号,编号规则是 [statefulset name]-[ordinal index]。
而且这些编号都是从 0 开始累加,与 StatefulSet 的每个 Pod 实例一一对应,绝不重复。
更重要的是,这些 Pod 的创建,也是严格按照编号顺序进行的。比如,在 web-0 进入到 Running 状态、并且细分状态(Conditions)成为 Ready 之前,web-1 会一直处于 Pending 状态。
备注:Ready 状态再一次提醒了我们,为 Pod 设置 livenessProbe 和 readinessProbe 的重要性。
当这两个 Pod 都进入了 Running 状态之后,你就可以查看到它们各自唯一的“网络身份”了。
我们使用 kubectl exec 命令进入到容器中查看它们的 hostname:
1 | $ kubectl exec web-0 -- sh -c 'hostname' |
可以看到,这两个 Pod 的 hostname 与 Pod 名字是一致的,都被分配了对应的编号。接下来,我们再试着以 DNS 的方式,访问一下这个 Headless Service:
1 | $ kubectl run -i --tty --image busybox:1.28.4 dns-test --restart=Never --rm /bin/sh |
通过这条命令,我们启动了一个一次性的 Pod,因为 –rm 意味着 Pod 退出后就会被删除掉。然后,在这个 Pod 的容器里面,我们尝试用 nslookup 命令,解析一下 Pod 对应的 Headless Service:
1 | $ kubectl run -i --tty --image busybox:1.28.4 dns-test --restart=Never --rm /bin/sh |
我们可以看到,在这个 StatefulSet 中,这两个新 Pod 的“网络标识”(比如:web-0.nginx 和 web-1.nginx),再次解析到了正确的 IP 地址(比如:web-0 Pod 的 IP 地址 10.44.0.5)。
通过这种方法,Kubernetes 就成功地将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录。
这些状态,在 StatefulSet 的整个生命周期里都会保持不变,绝不会因为对应 Pod 的删除或者重新创建而失效。
不过,相信你也已经注意到了,尽管 web-0.nginx 这条记录本身不会变,但它解析到的 Pod 的 IP 地址,并不是固定的。这就意味着,对于“有状态应用”实例的访问,你必须使用 DNS 记录或者 hostname 的方式,而绝不应该直接访问这些 Pod 的 IP 地址。