#yyds干货盘点# Kubernetes 如何通过 StatefulSet 支持有状态应用((07))

愿君学长松,慎勿作桃李。这篇文章主要讲述#yyds干货盘点# Kubernetes 如何通过 StatefulSet 支持有状态应用?(07)相关的知识,希望能为你提供帮助。
我们学习了 Kubernetes 中的无状态工作负载,并上手实践了 Deployment 对象,相信现在你已经慢慢喜欢上 Kubernetes 了。
那么本节课,我们来一起看看 Kubernetes 中的另外一种工作负载 StatefulSet。从名字就可以看出,这个工作负载主要用于有状态的服务发布。关于有状态服务和无状态服务,你可以参考上一章节的内容。
我们从一个具体的例子来逐渐了解、认识 StatefulSet。在 kubectl 命令行中,我们一般将 StatefulSet 简写为 sts。在部署一个 StatefulSet 的时候,有个前置依赖对象,即 Headless Services。这个对象在 StatefulSet 中的作用,我们在下文中会一一道来。另外,关于这个对象的详细介绍和其他作用,我们会在后面的课程中单独讲解。在此,你可以先暂时略过对 Service 的感知。我们先看如下一个 Headless Services:

$ cat nginx-svc.yaml apiVersion: v1 kind: Service metadata: name: nginx-demo namespace: demo labels: app: nginx spec: clusterIP: None ports: - port: 80 name: web selector: app: nginx

上面这段 yaml 的意思是,在 demo 这个命名空间中,创建一个名为 nginx-demo 的服务,这个服务暴露了 80 端口,可以访问带有app=nginx这个 label 的 Pod。
我们现在利用上面这段 yaml 在集群中创建出一个 Service:
$ kubectl create ns demo $ kubectl create -f nginx-svc.yaml service/nginx-demo created $ kubectl get svc -n demo NAMETYPECLUSTER-IPEXTERNAL-IPPORT(S)AGE nginx-demoClusterIPNone< none> 80/TCP5s

创建好了这个前置依赖的 Service,下面我们就可以开始创建真正的 StatefulSet 对象,可参照如下的 yaml 文件:
$ cat web-sts.yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: web-demo namespace: demo spec: serviceName: "nginx-demo" replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.19.2-alpine ports: - containerPort: 80 name: web $ kubectl create -f web-sts.yaml $ kubectl get sts -n demo NAMEREADYAGE web-demo0/29s

可以看到,到这里我已经将名为 web-demo 的 StatefulSet 部署完成了。
下面我们来一点点探索 StatefulSet 的秘密,看看它有哪些特性,为何可以保障服务有状态的运行。
StatefulSet 的特性
通过 kubectl 的watch功能(命令行加参数-w),我们可以观察到 Pod 状态的一步步变化。
$ kubectl get pod -n demo -w NAMEREADYSTATUSRESTARTSAGE web-demo-00/1ContainerCreating018s web-demo-01/1Running020s web-demo-10/1Pending00s web-demo-10/1Pending00s web-demo-10/1ContainerCreating00s web-demo-11/1Running02s

通过 StatefulSet 创建出来的 Pod 名字有一定的规律,即$(statefulset名称)-$(序号),比如这个例子中的 web-demo-0、web-demo-1。
这里面还有个有意思的点,web-demo-0 这个 Pod 比 web-demo-1 优先创建,而且在 web-demo-0 变为 Running 状态以后,才被创建出来。为了证实这个猜想,我们在一个终端窗口观察 StatefulSet 的 Pod:
$ kubectl get pod -n demo -w -l app=nginx

我们再开一个终端端口来 watch 这个 namespace 中的 event:
$ kubectl get event -n demo -w

现在我们试着改变这个 StatefulSet 的副本数,将它改成 5:
$ kubectl scale sts web-demo -n demo --replicas=5 statefulset.apps/web-demo scaled

此时我们观察到另外两个终端端口的输出:
$ kubectl get pod -n demo -w NAMEREADYSTATUSRESTARTSAGE web-demo-01/1Running020m web-demo-11/1Running020m web-demo-20/1Pending00s web-demo-20/1Pending00s web-demo-20/1ContainerCreating00s web-demo-21/1Running02s web-demo-30/1Pending00s web-demo-30/1Pending00s web-demo-30/1ContainerCreating00s web-demo-31/1Running03s web-demo-40/1Pending00s web-demo-40/1Pending00s web-demo-40/1ContainerCreating00s web-demo-41/1Running03s

我们再一次看到了 StatefulSet 管理的 Pod 按照 2、3、4 的顺序依次创建,名称有规律,跟上一节通过 Deployment 创建的随机 Pod 名有很大的区别。
通过观察对应的 event 信息,也可以再次证实我们的猜想。
$ kubectl get event -n demo -w LAST SEENTYPEREASONOBJECTMESSAGE 20mNormalScheduledpod/web-demo-0Successfully assigned demo/web-demo-0 to kraken 20mNormalPullingpod/web-demo-0Pulling image "nginx:1.19.2-alpine" 20mNormalPulledpod/web-demo-0Successfully pulled image "nginx:1.19.2-alpine" 20mNormalCreatedpod/web-demo-0Created container nginx 20mNormalStartedpod/web-demo-0Started container nginx 20mNormalScheduledpod/web-demo-1Successfully assigned demo/web-demo-1 to kraken 20mNormalPulledpod/web-demo-1Container image "nginx:1.19.2-alpine" already present on machine 20mNormalCreatedpod/web-demo-1Created container nginx 20mNormalStartedpod/web-demo-1Started container nginx 20mNormalSuccessfulCreatestatefulset/web-democreate Pod web-demo-0 in StatefulSet web-demo successful 20mNormalSuccessfulCreatestatefulset/web-democreate Pod web-demo-1 in StatefulSet web-demo successful 0sNormalSuccessfulCreatestatefulset/web-democreate Pod web-demo-2 in StatefulSet web-demo successful 0sNormalScheduledpod/web-demo-2Successfully assigned demo/web-demo-2 to kraken 0sNormalPulledpod/web-demo-2Container image "nginx:1.19.2-alpine" already present on machine 0sNormalCreatedpod/web-demo-2Created container nginx 0sNormalStartedpod/web-demo-2Started container nginx 0sNormalSuccessfulCreatestatefulset/web-democreate Pod web-demo-3 in StatefulSet web-demo successful 0sNormalScheduledpod/web-demo-3Successfully assigned demo/web-demo-3 to kraken 0sNormalPulledpod/web-demo-3Container image "nginx:1.19.2-alpine" already present on machine 0sNormalCreatedpod/web-demo-3Created container nginx 0sNormalStartedpod/web-demo-3Started container nginx 0sNormalSuccessfulCreatestatefulset/web-democreate Pod web-demo-4 in StatefulSet web-demo successful 0sNormalScheduledpod/web-demo-4Successfully assigned demo/web-demo-4 to kraken 0sNormalPulledpod/web-demo-4Container image "nginx:1.19.2-alpine" already present on machine 0sNormalCreatedpod/web-demo-4Created container nginx 0sNormalStartedpod/web-demo-4Started container nginx

现在我们试着进行一次缩容:
$ kubectl scale sts web-demo -n demo --replicas=2 statefulset.apps/web-demo scaled

此时观察另外两个终端窗口,分别如下:
web-demo-41/1Terminating011m web-demo-40/1Terminating011m web-demo-40/1Terminating011m web-demo-40/1Terminating011m web-demo-31/1Terminating012m web-demo-30/1Terminating012m web-demo-30/1Terminating012m web-demo-30/1Terminating012m web-demo-21/1Terminating012m web-demo-20/1Terminating012m web-demo-20/1Terminating012m web-demo-20/1Terminating012m 0sNormalSuccessfulDeletestatefulset/web-demodelete Pod web-demo-4 in StatefulSet web-demo successful 0sNormalKillingpod/web-demo-4Stopping container nginx 0sNormalKillingpod/web-demo-3Stopping container nginx 0sNormalSuccessfulDeletestatefulset/web-demodelete Pod web-demo-3 in StatefulSet web-demo successful 0sNormalSuccessfulDeletestatefulset/web-demodelete Pod web-demo-2 in StatefulSet web-demo successful 0sNormalKillingpod/web-demo-2Stopping container nginx

可以看到,在缩容的时候,StatefulSet 关联的 Pod 按着 4、3、2 的顺序依次删除。
可见,对于一个拥有 N 个副本的 StatefulSet 来说,Pod 在部署时按照 0 …… N-1 的序号顺序创建的,而删除的时候按照逆序逐个删除,这便是我想说的第一个特性。
接着我们来看,StatefulSet 创建出来的 Pod 都具有固定的、且确切的主机名,比如:
$ for i in 0 1; do kubectl exec web-demo-$i -n demo -- sh -c hostname; done web-demo-0 web-demo-1

我们再看看上面 StatefulSet 的 API 对象定义,有没有发现跟我们上一节中 Deployment 的定义极其相似,主要的差异在于spec.serviceName这个字段。它很重要,StatefulSet 根据这个字段,为每个 Pod 创建一个 DNS 域名,这个域名的格式为$(podname).(headless service name),下面我们通过例子来看一下。
当前 Pod 和 IP 之间的对应关系如下:
$ kubectl get pod -n demo -l app=nginx -o wide NAMEREADYSTATUSRESTARTSAGEIPNODENOMINATED NODEREADINESS GATES web-demo-01/1Running03h17m10.244.0.39kraken< none> < none> web-demo-11/1Running03h17m10.244.0.40kraken< none> < none>

Pod web-demo-0 的IP 地址是 10.244.0.39,web-demo-1的 IP 地址是 10.244.0.40。这里我们通过kubectl run在同一个命名空间demo中创建一个名为 dns-test 的 Pod,同时 attach 到容器中,类似于docker run -it --rm这个命令。
我么在容器中运行 nslookup 来查询它们在集群内部的 DNS 地址,如下所示:
$ kubectl run -it --rm --image busybox:1.28 dns-test -n demo If you dont see a command prompt, try pressing enter. / # nslookup web-demo-0.nginx-demo Server:10.96.0.10 Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local Name:web-demo-0.nginx-demo Address 1: 10.244.0.39 web-demo-0.nginx-demo.demo.svc.cluster.local / # nslookup web-demo-1.nginx-demo Server:10.96.0.10 Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local Name:web-demo-1.nginx-demo Address 1: 10.244.0.40 web-demo-1.nginx-demo.demo.svc.cluster.local

可以看到,每个 Pod 都有一个对应的 A 记录。
我们现在删除一下这些 Pod,看看会有什么变化:
$ kubectl delete pod -l app=nginx -n demo pod "web-demo-0" deleted pod "web-demo-1" deleted $ kubectl get pod -l app=nginx -n demo -o wide NAMEREADYSTATUSRESTARTSAGEIPNODENOMINATED NODEREADINESS GATES web-demo-01/1Running015s10.244.0.50kraken< none> < none> web-demo-11/1Running013s10.244.0.51kraken< none> < none>

删除成功后,可以发现 StatefulSet 立即生成了新的 Pod,但是 Pod 名称维持不变。唯一变化的就是 IP 发生了改变。
我们再来看看 DNS 记录:
$ kubectl run -it --rm --image busybox:1.28 dns-test -n demo If you dont see a command prompt, try pressing enter. / # nslookup web-demo-0.nginx-demo Server:10.96.0.10 Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.localName:web-demo-0.nginx-demo Address 1: 10.244.0.50 web-demo-0.nginx-demo.demo.svc.cluster.local / # nslookup web-demo-1.nginx-demo Server:10.96.0.10 Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.localName:web-demo-1.nginx-demo Address 1: 10.244.0.51 web-demo-1.nginx-demo.demo.svc.cluster.local

可以看出,DNS 记录中 Pod 的域名没有发生变化,仅仅 IP 地址发生了更换。因此当 Pod 所在的节点发生故障导致 Pod 飘移到其他节点上,或者 Pod 因故障被删除重建,Pod 的 IP 都会发生变化,但是 Pod 的域名不会有任何变化,这也就意味着服务间可以通过不变的 Pod 域名来保障通信稳定,而不必依赖 Pod IP。
有了spec.serviceName这个字段,保证了 StatefulSet 关联的 Pod 可以有稳定的网络身份标识,即 Pod 的序号、主机名、DNS 记录名称等。
最后一个我想说的是,对于有状态的服务来说,每个副本都会用到持久化存储,且各自使用的数据是不一样的。
StatefulSet 通过 PersistentVolumeClaim(PVC)可以保证 Pod 的存储卷之间一一对应的绑定关系。同时,删除 StatefulSet 关联的 Pod 时,不会删除其关联的 PVC。
我们会在后续网络存储的章节中来专门介绍,再次先略过。
如何更新升级 StatefulSet
那么,如果想对一个 StatefulSet 进行升级,该怎么办呢?
在 StatefulSet 中,支持两种更新升级策略,即 RollingUpdate 和 OnDelete。
RollingUpdate策略是默认的更新策略。可以实现 Pod 的滚动升级,跟我们上一节课中 Deployment 介绍的RollingUpdate策略一样。比如我们这个时候做了镜像更新操作,那么整个的升级过程大致如下,先逆序删除所有的 Pod,然后依次用新镜像创建新的 Pod 出来。这里你可以通过kubectl get pod -n demo -w -l app=nginx来动手观察下。
同时使用 RollingUpdate 更新策略还支持通过 partition 参数来分段更新一个 StatefulSet。所有序号大于或者等于 partition 的Pod 都将被更新。你这里也可以手动更新 StatefulSet 的配置来实验下。
当你把更新策略设置为 OnDelete 时,我们就必须手动先删除 Pod,才能触发新的 Pod 更新。
最后
现在我们就总结下 StatefulSet 的特点:
  • 具备固定的网络标记,比如主机名,域名等;
  • 支持持久化存储,而且最好能够跟实例一一绑定;
  • 可以按照顺序来部署和扩展;
  • 可以按照顺序进行终止和删除操作;
  • 在进行滚动升级的时候,也会按照一定顺序。
借助 StatefulSet 的这些能力,我们就可以去部署一些有状态服务,比如 mysql、ZooKeeper、MongoDB 等。你可以跟着这个教程在 Kubernetes 中搭建一个 ZooKeeper 集群。
欢迎大家扫码关注,获取更多信息【#yyds干货盘点# Kubernetes 如何通过 StatefulSet 支持有状态应用((07))】
#yyds干货盘点# Kubernetes 如何通过 StatefulSet 支持有状态应用((07))

文章图片


    推荐阅读