0%

k8s 中的部署神器 -- Statefulset 控制器

关于 Kubernetes 系列其他文章的传送门

  1. 《 部署 Kubernetes 也能如此简单吗? 》

  2. 《 初识 k8s,新时代的宠儿 》

  3. 《 深度学习 Pod 》

  4. 《 学会这 5 种 Pod 控制器,搞定发布、更新、回滚,从此告别人肉运维!! 》

  5. 《 k8s 中的网络入口 – Service 资源管理 》

  6. 《 k8s 中的 7 层调度 – Ingress 》

  7. 《 k8s 中的存储系统 – Volumes 》

  8. 《 k8s 中强大的配置中心 – ConfigMap 》

  9. 《 k8s 中的部署神器 – Statefulset 控制器 》     您当前所在位置

  10. 《 k8s 中的安全守护神 – 你不知道不代表它不在 》

  11. 《 k8s 中的图形接口 – Dashboard 》

  12. 《 k8s 中的网络插件 – flannel 和 Calico 》

  13. 《 k8s 中的调度器 – 亲和性、污点和容忍性 》

  14. 《 k8s 中对资源的监控 – 资源指标、Prometheus、弹性伸缩器 HPA 》

  15. 《 k8s 中的云原生应用管理利器 – Helm 》

  16. 《 理解 k8s 高可用,让你的集群稳如泰山 》



logo

一、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 TemplatePVC 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
-> # vim statefulset-demo.yaml
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 # 定义为无头 Service
selector:
app: myapp-pod # 一定要选择下面 StatefulSet 中 Pod 模板的标签

---

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: statefulset-demo
namespace: sts
spec:
selector:
matchLabels:
app: myapp-pod
serviceName: "myapp-sts-svc" # 必须指明上面无头 Service 名字
replicas: 2
template: # 下面是定义 Pod 模板
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
~] # vim pv.yaml

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 # 此行改为 1 号
labels:
storsys: nfs
spec:
accessModes: ["ReadWriteOnce","ReadWriteMany","ReadOnlyMany"]
capacity:
storage: 5Gi
volumeMode: Filesystem
persistentVolumeReclaimPolicy: Retain
nfs:
server: 192.168.50.14
path: /vols/v1 # 此行改为 1 号

...
...
# 以此类推,直到 5 号

创建 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] [~] 
-> # kubectl get all -n sts
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
-> # kubectl get pv       
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
-> # kubectl get pods -n sts -w                            
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,也就是 34 号,之后就自动停下来。之后如果要继续更新,把 partition 改为 0 即可全部更新。

使用金丝雀发布时,通过定义 partition 参数,实现以逆序的方式,滚动更新到指定序列号的 Pod。

有两种方式:

方法一:在线修改

先使用 kubectl edit sts statefulset-demo -n sts 在线更改,把 partition: 2 定义为 2,即可逆序更新至 3 号,

  • 即只更新 3、2 号 Pod。

然后变更镜像版本,触发更新:

1
2
-> # kubectl set image sts statefulset-demo myapp=ikubernetes/myapp:v3 -n sts
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
-> # 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 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
-> # vim statefulset-demo.yaml
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
rollingUpdate: #
partition: 2 # 定义更新到第几号,注意是逆序更新,定义为 2 就是指更新 3、2 号 Pod
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 是否删除则取决于定义的删除策略。