关于 Kubernetes 系列其他文章的传送门
-
《 部署 Kubernetes 也能如此简单吗? 》
-
《 初识 k8s,新时代的宠儿 》
-
《 深度学习 Pod 》
-
《 学会这 5 种 Pod 控制器,搞定发布、更新、回滚,从此告别人肉运维!! 》
-
《 k8s 中的网络入口 – Service 资源管理 》
-
《 k8s 中的 7 层调度 – Ingress 》
-
《 k8s 中的存储系统 – Volumes 》
-
《 k8s 中强大的配置中心 – ConfigMap 》
-
《 k8s 中的部署神器 – Statefulset 控制器 》 您当前所在位置
-
《 k8s 中的安全守护神 – 你不知道不代表它不在 》
-
《 k8s 中的图形接口 – Dashboard 》
-
《 k8s 中的网络插件 – flannel 和 Calico 》
-
《 k8s 中的调度器 – 亲和性、污点和容忍性 》
-
《 k8s 中对资源的监控 – 资源指标、Prometheus、弹性伸缩器 HPA 》
-
《 k8s 中的云原生应用管理利器 – Helm 》
-
《 理解 k8s 高可用,让你的集群稳如泰山 》

一、Statefulset 是什么?
为每一种有状态的应用,比如 Redis、MySQL 等,把这些程序运维的操作步骤,比如扩容、缩容等,用代码写出来,测试完成后,封装成一个应用程序,以 Pod 的形式,运行在 k8s 上,这就叫 Operator。以后如果想创建一个集群,或者对集群扩容、缩容,交给这个 Operator 处理就行了。这个集群对于 Opeartor 来说是一个实例。 Opeartor 也可以理解为是这个集群的控制器,只是没有打包进 Controller-manager 中。
由于云原生的火热,很多有状态应用项目的官方都开始自己专门开发 Operator 控制器,比如 Redis、MySQL、Zookeeper 都发布了官方的 Opeartor 控制器。
而 Opeartor 控制器 只不过是把 Statefulset 控制器,结合某一个应用程序特有的运行逻辑代码,做了二次封装而已,
Statefulset 控制器特性:
- 每个实例都有唯一的固定的网络标识符;
- 唯一的持久存储;
- 有序优雅的部署和扩展;
- 有序优雅的删除和终止;
- 有序自动的完成滚动更新;
要求:
- 各 Pod 用到的存储卷必须使用由 SC 动态供给或由管理员事先创建好的 PV;
- 删除 StatefulSet 或缩减其规模导致 Pod 被删除时不应该自动删除其存储卷,以确保数据安全;
- StatefulSet 控制器依赖于一个事先存在的 Headless Service 对象,实现对 Pod 对象的持久、唯一的标识符配置;此 Headless Service 需要由用户手动配置;
每个 StatefulSet 都需要拥有两个模板,Pod Template 和 PVC Template,因为每个 Pod 都应该有一个专用的存储卷,不应该和其他 Pod 共享。
- 而无状态应用则可以不使用 PVC Template。
POd 的标识符:
- 为了保证每个 Pod 的标识符固定且唯一,StatefulSet 使用有序索引(Ordinal Index)按顺序来固定。
关于 Updata Strategies(更新策略)
在 1.7 版本后支持自定义更新策略,有两种方式:
- On Delete:由用户手动删除现有的 Pod,从而触发其更新过程;手动删一个,自动更新一个;
- Rolling Updates:默认策略,通过自动更新机制完成更新过程,期待更新过程时,它自动删除每个 Pod 对象,并以新配置进行重建;与 Deployment 不同的是,Deploy 更新时是随机选取 Pod 并更新,而这个是有序的,必须先更新索引号最大的,然后按照逆序一个一个更新。
二、配置 Statefulset 控制器
这里先不考虑各个有状态应用特有的扩缩容逻辑,先使用 Nginx 来模拟一个有状态应用,并演示如何使用 Statefulset 控制器。
创建:
Statefulset 控制器;
- 对应的
无头 Service;
- 他们所在的名称空间
sts。
-> # vim statefulset-demo.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| -> apiVersion: v1 kind: Namespace metadata: name: sts
---
apiVersion: v1 kind: Service metadata: name: myapp-sts-svc namespace: sts labels: app: myapp spec: ports: - port: 80 name: web clusterIP: None selector: app: myapp-pod
---
apiVersion: apps/v1 kind: StatefulSet metadata: name: statefulset-demo namespace: sts spec: selector: matchLabels: app: myapp-pod serviceName: "myapp-sts-svc" replicas: 2 template: metadata: namespace: sts labels: app: myapp-pod spec: terminationGracePeriodSeconds: 10 containers: - name: myapp image: ikubernetes/myapp:v1 ports: - containerPort: 80 name: web volumeMounts: - name: myapp-pvc mountPath: /usr/share/nginx/html volumeClaimTemplates: - metadata: name: myapp-pvc namespace: sts spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 2Gi
|
这里使用 nfs 作为存储,不支持动态供给,所以需要手动创建 PV 来静态供给:
1 2 3 4 5 6 7 8 9 10 11 12
| ~]# mkdir /vols/v0
~]# vim /etc/exports /vols/v0 192.168.50.0/16(rw,no_root_squash) /vols/v1 192.168.50.0/16(rw,no_root_squash) /vols/v2 192.168.50.0/16(rw,no_root_squash) /vols/v3 192.168.50.0/16(rw,no_root_squash) /vols/v4 192.168.50.0/16(rw,no_root_squash) /vols/v5 192.168.50.0/16(rw,no_root_squash) /vols/v6 192.168.50.0/16(rw,no_root_squash)
~]# exportfs -rav
|
创建 PV 的配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| ~]
apiVersion: v1 kind: PersistentVolume metadata: name: pv-nfs-v0 labels: storsys: nfs spec: accessModes: ["ReadWriteOnce","ReadWriteMany","ReadOnlyMany"] capacity: storage: 5Gi volumeMode: Filesystem persistentVolumeReclaimPolicy: Retain nfs: server: 192.168.50.14 path: /vols/v0
---
apiVersion: v1 kind: PersistentVolume metadata: name: pv-nfs-v1 labels: storsys: nfs spec: accessModes: ["ReadWriteOnce","ReadWriteMany","ReadOnlyMany"] capacity: storage: 5Gi volumeMode: Filesystem persistentVolumeReclaimPolicy: Retain nfs: server: 192.168.50.14 path: /vols/v1
... ...
|
创建 PV:
1 2
| ~]# kubectl apply -f pv.yaml persistentvolume/pv-nfs-v0 created
|
创建 Statefulset:
1 2 3 4
| ~]# kubectl apply -f statefulset-demo.yaml namespace/sts created service/myapp-sts-svc created statefulset.apps/statefulset-demo created
|
查看创建的所有资源,可以看到 Pod 名字最后的序号是有序增长的:
1 2 3 4 5 6 7 8 9 10 11
| root@master [02:49:11 PM] [~] -> NAME READY STATUS RESTARTS AGE pod/statefulset-demo-0 1/1 Running 0 60s pod/statefulset-demo-1 1/1 Running 0 58s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/myapp-sts-svc ClusterIP None <none> 80/TCP 60s
NAME READY AGE statefulset.apps/statefulset-demo 2/2 60s
|
查看 PV 的绑定关系:
1 2 3 4 5 6 7 8 9
| -> NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv-nfs-v0 5Gi RWO,ROX,RWX Retain Available 6m7s pv-nfs-v1 5Gi RWO,ROX,RWX Retain Bound sts/myapp-pvc-statefulset-demo-0 6m7s pv-nfs-v2 5Gi RWO,ROX,RWX Retain Bound sts/myapp-pvc-statefulset-demo-1 6m7s pv-nfs-v3 5Gi RWO,ROX,RWX Retain Available 6m7s pv-nfs-v4 5Gi RWO,ROX,RWX Retain Available 6m7s pv-nfs-v5 5Gi RWO,ROX,RWX Retain Available 6m7s pv-nfs-v6 5Gi RWO,ROX,RWX Retain Available 6m7s
|
1. 扩容操作
把副本数扩容至 4 个:
1 2 3
| ~]# kubectl scale sts statefulset-demo --replicas=4 -n sts statefulset.apps/statefulset-demo scaled
|
在扩容时,我们单独开一个终端来监控 Pod 创建的实时状态,可以看到 Pod 名是按序列顺序创建的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ~]# kubectl get pods -n sts -w NAME READY STATUS RESTARTS AGE statefulset-demo-0 1/1 Running 0 24m statefulset-demo-1 1/1 Running 0 24m statefulset-demo-2 0/1 Pending 0 0s statefulset-demo-2 0/1 Pending 0 0s statefulset-demo-2 0/1 Pending 0 2s statefulset-demo-2 0/1 ContainerCreating 0 2s statefulset-demo-2 1/1 Running 0 4s statefulset-demo-3 0/1 Pending 0 0s statefulset-demo-3 0/1 Pending 0 0s statefulset-demo-3 0/1 Pending 0 1s statefulset-demo-3 0/1 ContainerCreating 0 1s statefulset-demo-3 1/1 Running 0 3s
|
2. 版本更新操作
常规更新
版本更新时,更新逻辑应该是按名字序列的逆序进行更新:
1 2 3
| # 这里把镜像更新为 v2 版本 ~]# kubectl set image sts statefulset-demo myapp=ikubernetes/myapp:v2 -n sts statefulset.apps/statefulset-demo image updated
|
观察到更新顺序确实为逆序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| -> NAME READY STATUS RESTARTS AGE statefulset-demo-0 1/1 Running 0 28m statefulset-demo-1 1/1 Running 0 28m statefulset-demo-2 1/1 Running 0 4m30s statefulset-demo-3 1/1 Running 0 4m26s statefulset-demo-3 1/1 Terminating 0 4m37s statefulset-demo-3 0/1 Terminating 0 4m38s statefulset-demo-3 0/1 Terminating 0 4m43s statefulset-demo-3 0/1 Terminating 0 4m43s statefulset-demo-3 0/1 Pending 0 0s statefulset-demo-3 0/1 Pending 0 0s statefulset-demo-3 0/1 ContainerCreating 0 0s statefulset-demo-3 1/1 Running 0 8s statefulset-demo-2 1/1 Terminating 0 4m55s statefulset-demo-2 0/1 Terminating 0 4m56s statefulset-demo-2 0/1 Terminating 0 5m3s statefulset-demo-2 0/1 Terminating 0 5m3s statefulset-demo-2 0/1 Pending 0 0s statefulset-demo-2 0/1 Pending 0 0s statefulset-demo-2 0/1 ContainerCreating 0 0s statefulset-demo-2 1/1 Running 0 8s statefulset-demo-1 1/1 Terminating 0 29m statefulset-demo-1 0/1 Terminating 0 29m statefulset-demo-1 0/1 Terminating 0 29m statefulset-demo-1 0/1 Terminating 0 29m statefulset-demo-1 0/1 Pending 0 0s statefulset-demo-1 0/1 Pending 0 0s statefulset-demo-1 0/1 ContainerCreating 0 0s statefulset-demo-1 1/1 Running 0 7s statefulset-demo-0 1/1 Terminating 0 29m statefulset-demo-0 0/1 Terminating 0 29m statefulset-demo-0 0/1 Terminating 0 29m statefulset-demo-0 0/1 Terminating 0 29m statefulset-demo-0 0/1 Pending 0 0s statefulset-demo-0 0/1 Pending 0 0s statefulset-demo-0 0/1 ContainerCreating 0 0s statefulset-demo-0 1/1 Running 0 2s
|
使用金丝雀发布的方式
如果要做金丝雀发布逻辑,可以使用更新分区(partition)实现。假如当前有 5 个 Pod,序列从 0-4,当 partition=3 时,会先更新大于等于 3 号以后的 Pod,也就是 3 和 4 号,之后就自动停下来。之后如果要继续更新,把 partition 改为 0 即可全部更新。
使用金丝雀发布时,通过定义 partition 参数,实现以逆序的方式,滚动更新到指定序列号的 Pod。
有两种方式:
方法一:在线修改
先使用 kubectl edit sts statefulset-demo -n sts 在线更改,把 partition: 2 定义为 2,即可逆序更新至 3 号,
然后变更镜像版本,触发更新:
1 2
| -> statefulset.apps/statefulset-demo image updated
|
查看更新状态,可以看到只更新至 2 号 Pod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| -> NAME READY STATUS RESTARTS AGE statefulset-demo-0 1/1 Running 0 24m statefulset-demo-1 1/1 Running 0 24m statefulset-demo-2 1/1 Running 0 25m statefulset-demo-3 1/1 Running 0 8s statefulset-demo-3 1/1 Terminating 0 35s statefulset-demo-3 0/1 Terminating 0 35s statefulset-demo-3 0/1 Terminating 0 36s statefulset-demo-3 0/1 Terminating 0 36s statefulset-demo-3 0/1 Pending 0 0s statefulset-demo-3 0/1 Pending 0 0s statefulset-demo-3 0/1 ContainerCreating 0 0s statefulset-demo-3 1/1 Running 0 3s statefulset-demo-2 1/1 Terminating 0 25m statefulset-demo-2 0/1 Terminating 0 25m statefulset-demo-2 0/1 Terminating 0 25m statefulset-demo-2 0/1 Terminating 0 25m statefulset-demo-2 0/1 Pending 0 0s statefulset-demo-2 0/1 Pending 0 0s statefulset-demo-2 0/1 ContainerCreating 0 0s statefulset-demo-2 1/1 Running 0 7s
|
之后如果运行没问题,想全部更新,更改 partition: 0 定义为 0 即可全部更新。
方法二:编辑配置文件
更改 statefulset-demo 的配置文件,增加如下参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| -> apiVersion: v1 kind: Namespace metadata: name: sts
---
apiVersion: v1 kind: Service metadata: name: myapp-sts-svc namespace: sts labels: app: myapp spec: ports: - port: 80 name: web clusterIP: None selector: app: myapp-pod
---
apiVersion: apps/v1 kind: StatefulSet metadata: name: statefulset-demo namespace: sts spec: selector: matchLabels: app: myapp-pod serviceName: "myapp-sts-svc" replicas: 2 updateStrategy: type: RollingUpdate rollingUpdate: partition: 2 template: metadata: namespace: sts labels: app: myapp-pod spec: terminationGracePeriodSeconds: 10 containers: - name: myapp image: ikubernetes/myapp:v1 ports: - containerPort: 80 name: web volumeMounts: - name: myapp-pvc mountPath: /usr/share/nginx/html volumeClaimTemplates: - metadata: name: myapp-pvc namespace: sts spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 2Gi
|
之后的操作就都一样了,更新镜像版本触发更新即可。
3. 缩容
缩容时,只会删除 Pod,PVC 和 PV 会保留。
这样以后要扩展的时候,会使用同一个 PVC 和 PV,因为他们的标识符是一样的,这样数据也能持久保留下来。
如果手动删除了 PVC,PV 是否删除则取决于定义的删除策略。