Kubernetes通过控制器维护所需的配置状态。Deployment、ReplicaSet、DaemonSet和StatefulSet就是一些常用的控制器。
StatefulSet是一种特殊类型的控制器,它可以使Kubernetes中运行集群工作负载变得很容易。集群工作负载通常有一个或多个主服务器(Masters)和多个从服务器(Slaves)。大多数数据库都以集群模式设计的,这样可以提供高可用性和容错能力。
有状态集群工作负载持续地在Masters和Slaves之间复制数据。为此,集群基础设施寄期望于参与的实体(Masters和Slaves)有一致且众所周知的Endpoints,以可靠地同步状态。但在Kubernetes中,Pod的设计寿命很短,且不会保证拥有相同的名称和IP地址。
有状态集群工作负载的另一个需求是持久的后端存储,它具有容错能力,以及能够处理IOPS。
为了方便在Kubernetes中运行有状态集群工作负载,引入了StatefulSets。StatefulSet里的Pod可以保证有稳定且唯一的标识符。它们遵循一种可预测的命名规则,并且还支持有序、顺畅的部署和扩展。
参与StatefulSet的每个Pod都有一个相应的Persistent Volume Claim(PVC),该声明遵循类似的命名规则。当一个Pod终止并在不同的Node上重新调度时,Kubernetes控制器将确保该Pod与同一个PVC相关联,以确保状态是完整的。
由于StatefulSet中的每个Pod都有专用的PVC和PV,所以没有使用共享存储的硬性规则。但还是期望StatefulSet是由快速、可靠、持久的存储层(如基于SSD的块存储设备)支持。在确保将写操作完全提交到磁盘之后,可以在块存储设备中进行常规备份和快照。
可供选择的存储:SSD、块存储设备,例如Amazon EBS、Azure Disks、GCE PD。
典型的工作负载:Apache ZooKeeper、Apache Kafka、Percona Server for MySQL、PostgreSQL Automatic Failover以及JupyterHub。
我们从一个具体的例子来逐渐了解、认识 StatefulSet。在 kubectl 命令行中,我们一般将 StatefulSet 简写为 sts。在部署一个 StatefulSet 的时候,有个前置依赖对象,即 Service(服务),我们先看如下一个 Service:
$ vim 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 apply -f nginx-svc.yaml
service/nginx-demo created
$ kubectl get svc -n demo
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-demo ClusterIP None 80/TCP 5s
创建好了这个前置依赖的 Service,下面我们就可以开始创建真正的 StatefulSet 对象,可参照如下的 yaml 文件:
$ vim 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 apply -f web-sts.yaml
$ kubectl get sts -n demo
NAME READY AGE
web-demo 0/2 9s
可以看到,到这里我已经将名为web-demo的StatefulSet部署完成了。
下面我们来一点点探索 StatefulSet 的秘密,看看它有哪些特性,为何可以保障服务有状态的运行。
StatefulSet 的特性
通过 kubectl 的watch功能(命令行加参数-w),我们可以观察到 Pod 状态的一步步变化。
$ kubectl get pod -n demo -w
NAME READY STATUS RESTARTS AGE
web-demo-0 0/1 ContainerCreating 0 18s
web-demo-0 1/1 Running 0 20s
web-demo-1 0/1 Pending 0 0s
web-demo-1 0/1 Pending 0 0s
web-demo-1 0/1 ContainerCreating 0 0s
web-demo-1 1/1 Running 0 2s
通过 StatefulSet 创建出来的 Pod 名字有一定的规律,即$(statefulset名称)-$(序号),比如这个例子中的 web-demo-0、web-demo-1。
这里面还有个有意思的点,web-demo-0 比 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
NAME READY STATUS RESTARTS AGE
web-demo-0 1/1 Running 0 20m
web-demo-1 1/1 Running 0 20m
web-demo-2 0/1 Pending 0 0s
web-demo-2 0/1 Pending 0 0s
web-demo-2 0/1 ContainerCreating 0 0s
web-demo-2 1/1 Running 0 2s
web-demo-3 0/1 Pending 0 0s
web-demo-3 0/1 Pending 0 0s
web-demo-3 0/1 ContainerCreating 0 0s
web-demo-3 1/1 Running 0 3s
web-demo-4 0/1 Pending 0 0s
web-demo-4 0/1 Pending 0 0s
web-demo-4 0/1 ContainerCreating 0 0s
web-demo-4 1/1 Running 0 3s
我们再一次看到扩容的时候, StatefulSet 管理的 Pod 按照 2、3、4 的顺序依次创建,名称有规律,跟上一节通过 Deployment 创建的随机 Pod 名有很大的区别。
通过观察对应的 event 信息,也可以再次证实我们的猜想。
$ kubectl get event -n demo -w
LAST SEEN TYPE REASON OBJECT MESSAGE
20m Normal Scheduled pod/web-demo-0 Successfully assigned demo/web-demo-0 to kraken
20m Normal Pulling pod/web-demo-0 Pulling image "nginx:1.19.2-alpine"
20m Normal Pulled pod/web-demo-0 Successfully pulled image "nginx:1.19.2-alpine"
20m Normal Created pod/web-demo-0 Created container nginx
20m Normal Started pod/web-demo-0 Started container nginx
20m Normal Scheduled pod/web-demo-1 Successfully assigned demo/web-demo-1 to kraken
20m Normal Pulled pod/web-demo-1 Container image "nginx:1.19.2-alpine" already present on machine
20m Normal Created pod/web-demo-1 Created container nginx
20m Normal Started pod/web-demo-1 Started container nginx
20m Normal SuccessfulCreate statefulset/web-demo create Pod web-demo-0 in StatefulSet web-demo successful
20m Normal SuccessfulCreate statefulset/web-demo create Pod web-demo-1 in StatefulSet web-demo successful
0s Normal SuccessfulCreate statefulset/web-demo create Pod web-demo-2 in StatefulSet web-demo successful
0s Normal Scheduled pod/web-demo-2 Successfully assigned demo/web-demo-2 to kraken
0s Normal Pulled pod/web-demo-2 Container image "nginx:1.19.2-alpine" already present on machine
0s Normal Created pod/web-demo-2 Created container nginx
0s Normal Started pod/web-demo-2 Started container nginx
0s Normal SuccessfulCreate statefulset/web-demo create Pod web-demo-3 in StatefulSet web-demo successful
0s Normal Scheduled pod/web-demo-3 Successfully assigned demo/web-demo-3 to kraken
0s Normal Pulled pod/web-demo-3 Container image "nginx:1.19.2-alpine" already present on machine
0s Normal Created pod/web-demo-3 Created container nginx
0s Normal Started pod/web-demo-3 Started container nginx
0s Normal SuccessfulCreate statefulset/web-demo create Pod web-demo-4 in StatefulSet web-demo successful
0s Normal Scheduled pod/web-demo-4 Successfully assigned demo/web-demo-4 to kraken
0s Normal Pulled pod/web-demo-4 Container image "nginx:1.19.2-alpine" already present on machine
0s Normal Created pod/web-demo-4 Created container nginx
0s Normal Started pod/web-demo-4 Started container nginx
现在我们试着进行一次缩容:
$ kubectl scale sts web-demo -n demo --replicas=2
statefulset.apps/web-demo scaled
此时观察另外两个终端窗口,分别如下:
web-demo-4 1/1 Terminating 0 11m
web-demo-4 0/1 Terminating 0 11m
web-demo-4 0/1 Terminating 0 11m
web-demo-4 0/1 Terminating 0 11m
web-demo-3 1/1 Terminating 0 12m
web-demo-3 0/1 Terminating 0 12m
web-demo-3 0/1 Terminating 0 12m
web-demo-3 0/1 Terminating 0 12m
web-demo-2 1/1 Terminating 0 12m
web-demo-2 0/1 Terminating 0 12m
web-demo-2 0/1 Terminating 0 12m
web-demo-2 0/1 Terminating 0 12m
0s Normal SuccessfulDelete statefulset/web-demo delete Pod web-demo-4 in StatefulSet web-demo successful
0s Normal Killing pod/web-demo-4 Stopping container nginx
0s Normal Killing pod/web-demo-3 Stopping container nginx
0s Normal SuccessfulDelete statefulset/web-demo delete Pod web-demo-3 in StatefulSet web-demo successful
0s Normal SuccessfulDelete statefulset/web-demo delete Pod web-demo-2 in StatefulSet web-demo successful
0s Normal Killing pod/web-demo-2 Stopping container ngi
可以看到,在缩容的时候,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
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-demo-0 1/1 Running 0 3h17m 10.244.0.39 kraken
web-demo-1 1/1 Running 0 3h17m 10.244.0.40 kraken
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 don't 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
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-demo-0 1/1 Running 0 15s 10.244.0.50 kraken
web-demo-1 1/1 Running 0 13s 10.244.0.51 kraken
删除成功后,可以发现 StatefulSet 立即生成了新的 Pod,但是 Pod 名称维持不变。唯一变化的就是 IP 发生了改变。
我们再来看看 DNS 记录:
$ kubectl run -it --rm --image busybox:1.28 dns-test -n demo
If you don't 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.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.local
Name: 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 中,支持两种更新升级策略,即 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 等。