深入篇(2):StatefulSet:拓扑结构

156 阅读7分钟

前提

Deployment 对应用做了一个简单化假设。它认为一个应用的所有 Pod 是一样的。它们之间没有顺序,也无所谓运行在哪台宿主机上。需要时通过 Pod 模板创建新的 Pod;不需要的则“杀掉”任意一个 Pod。

但在实际的场景中,并不是所有的应用都可以满足这样的要求。比如:

  • 分布式应用,它的多个实例之间,往往有依赖关系,比如:主从关系、主备关系。
  • 数据存储类应用,它的多个实例都会在本地磁盘上保存一份数据。而这些实例一旦被杀掉,即便重建出来,实例与数据间的对应关系也已经丢失,从而导致应用失败。

这种实例之间有不对等关系,以及实例对外部数据有依赖关系的应用,就被称为“有状态应用”(Stateful Application)。

StatefulSet

得益于“控制器模式”的设计思想,K8s 在 Deployment 的基础上,扩展出了对“有状态应用”的初步支持:StatefulSet。

StatefulSet 把真实世界里的应用状态,抽象为了两种情况:

  1. 拓扑状态:应用的多个实例间不是完全对等的关系,必须按照某些顺序启动,比如节点 A 要先于节点 B 启动。删除这两个 Pod 后再次创建也必须按照这个顺序。并且新创建的 Pod 必须和原 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。
  2. 存储状态:应用的多个实例分别绑定了不同的存储数据。Pod A 第一次读到的数据和十分钟后再次读到的数据应该是同一份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。

StatefulSet 的核心功能就是通过某种方式记录这些状态,在 Pod 被重新创建时,为新 Pod 恢复这些状态。

在讲 StatefulSet 工作原理前,先回顾一下 Headless Service。

Service 是 K8s 中用来将一组 Pod 暴露给外界访问的一种机制。那 Service 又是如何被访问的呢?

1. 以 Service 的 VIP(Virtual IP,虚拟 IP)方式。比如:当我访问 10.0.23.1 这个 Service 的 IP 地址时,10.0.23.1 就是一个 VIP,它会把请求转发到该 Service 所代理的某一个 Pod 上。

2. 以 Service 的 DNS 方式。比如:我访问“my-svc.my-namespace.svc.cluster.local”这条 DNS 记录,就可以访问到名叫 my-svc 的 Service 所代理的某一个 Pod。

而 Service DNS 方式还可以分为两种处理方法:

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

这样的设计有什么作用呢?回答这个问题,需要从 Headless Service 的定义方式看起。

下面是一个 Headless Service 对应的 YAML 文件:

image.png

它和标准 Service 的区别是它的 clusterIP 字段的值是:None,即:这个 Service,没有一个 VIP 作为“头”,这也就是 Headless 的含义。所以,这个 Service 被创建后并不会被分配一个 VIP,而是会以 DNS 记录的方式暴露出它所代理的 Pod。

它所代理的 Pod,是 Label Selector 选择的,即:所有携带了 app=nginx 标签的 Pod,都会被这个 Service 代理。

当你按照这样的方式创建了一个 Headless Service 后,它所代理的所有 Pod 的 IP 地址,都会被绑定一个这样格式的 DNS 记录,如下所示:

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

这个 DNS 记录,正是 K8s 为 Pod 分配的唯一的“可解析身份”。

有了这个“可解析身份”,只要知道了 Pod 名字,以及它对应的 Service 的名字,就可以通过 DNS 记录访问到 Pod 的 IP 地址。

那么,StatefulSet 又是如何使用这个 DNS 记录来维持 Pod 的拓扑状态的呢?

为了回答这个问题,现在我们就来编写一个 StatefulSet 的 YAML 文件,如下所示:

image.png

和 deployment 的唯一区别,就是多了一个 serviceName=nginx 字段。这个字段告诉 StatefulSet 控制器,在执行控制循环时,使用 nginx 这个 Headless Service 来保证 Pod 的“可解析身份”。

通过 kubectl create 创建了上面这个 Service 和 StatefulSet 之后,就会看到如下两个对象:

image.png

这时如果你手比较快的话,还可以通过 kubectl 的 -w 参数,即:Watch 功能,实时查看 StatefulSet 创建两个有状态实例的过程: image.png

备注:手不够快的可以通过这个 StatefulSet 的 Events 看到这些信息。

通过上面这个 Pod 的创建过程,我们不难看到,StatefulSet 给它所管理的所有 Pod 的名字,进行了编号。这些编号都是从 0 开始累加,与 StatefulSet 的每个 Pod 实例一一对应。

更重要的是,这些 Pod 的创建,也是严格按照编号顺序进行的。比如,在 web-0 进入到 Running 状态、并且细分状态(Conditions)成为 Ready 之前,web-1 会一直处于 Pending 状态。

备注:Ready 状态再次提醒了我们,为 Pod 设置 livenessProbe 和 readinessProbe 的重要性。

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

$ kubectl exec web-0 -- sh -c 'hostname'
web-0
$ kubectl exec web-1 -- sh -c 'hostname'	
web-1

这两个 Pod 的 hostname 与 Pod 名字是一致的,都被分配了对应的编号。我们再试着以 DNS 的方式,访问一下这个 Headless Service:

$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh 

这条命令启动了一个一次性的 Pod,–rm 意味着 Pod 退出后就被删除。然后,在这个 Pod 的容器里面,我们尝试用 nslookup 命令,解析一下 Pod 对应的 Headless Service:

image.png

在访问 web-0.nginx 时最后解析到的,正是 web-0 这个 Pod 的 IP 地址;而访问 web-1.nginx 时解析到的则是 web-1 的 IP 地址。

这时如果你在另外一个 Terminal 里把这两个“有状态应用”的 Pod 删掉:

$ kubectl delete pod -l app=nginx

pod "web-0" deleted
pod "web-1" deleted

再在当前 Terminal 里 Watch 一下这两个 Pod 的状态变化,就会发现一个有趣的现象:

image.png

当我们把这两个 Pod 删除之后,K8s 会按照原先编号的顺序,创建出了两个新的 Pod。并且,K8s 依然为它们分配了与原来相同的“网络身份”:web-0.nginx 和 web-1.nginx。

通过这种严格的对应规则,StatefulSet 就保证了 Pod 网络标识的稳定性

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

所以,如果我们再用 nslookup 命令,查看一下这个新 Pod 对应的 Headless Service 的话:

image.png

在这个 StatefulSet 中,这两个新 Pod 的“网络标识”(比如:web-0.nginx),再次解析到了正确的 IP 地址(比如:web-0 Pod 的 IP 地址 10.244.1.8)。

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

这些状态,在 StatefulSet 的整个生命周期里都会保持不变,绝不会因为对应 Pod 的删除或者重新创建而失效。

相信你也注意到了,尽管 web-0.nginx 这条记录本身不会变,但它解析到的 Pod 的 IP 地址,并不是固定的。这意味着,对于“有状态应用”实例的访问,你必须使用 DNS 记录或者 hostname 的方式,而绝不应该直接访问这些 Pod 的 IP 地址。

总结

StatefulSet 控制器的主要作用之一,就是使用 Pod 模板创建 Pod 时,对它们进行编号,并且按照编号顺序逐一完成创建工作。而当 StatefulSet 的“控制循环”发现 Pod 的“实际状态”与“期望状态”不一致,需要新建或者删除 Pod 进行“调谐”时,它会严格按照这些 Pod 编号的顺序,逐一完成这些操作。

所以,StatefulSet 其实可以认为是对 Deployment 的改良。

与此同时,通过 Headless Service 的方式,StatefulSet 为每个 Pod 创建了一个固定并且稳定的 DNS 记录,来作为它的访问入口。

在部署“有状态应用”时,应用的每个实例拥有唯一并且稳定的“网络标识”,是一个非常重要的假设。