k8s系列-StatefulSet-存储状态

K8S-StatefulSet

存储状态

StatefulSet 对存储状态的管理机制,主要通过使用 Persistent Volume Claim 的功能。

Pod 中需要将容器内部的存储进行挂载出来,只要在 Pod 中使用 Volume,定义一个具体类型的 Volume 就可以了。

比如下面这种最简单的 hostPath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Pod
metadata:
name: nginx-volume
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: nginx-storage1
volumes:
- name: nginx-storage1
hostPath:
path: /home/html

hostPath 类型的存储是直接将容器的文件目录挂载在当前运行这个容器的宿主机上面的,但是这种类型在集群部署的时候有一个最大的问题就在于,如果容器因为某些不可抗拒原因被调度到其他节点上面,那么之前的文件下次容器再启动可能就看不到了。

所以在集群下,我们如何保证应用的正确持久性,如果在不够了解各类持久化存储项目(比如 Ceph、ClusterFS)的情况下,正确定义容器的存储

比如下面这个最简单的nfs存储挂载,可以满足集群操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
name: nginx-volume-nfs
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: nginx-storage-nfs
volumes:
- name: nginx-storage-nfs
nfs:
path: /home/data
server: 192.168.50.234

这里我在我的虚拟机(192.168.50.234)上搭建了 nfs-server,挂载目录 /home/data,里面有两个 txt 文件。

image

我的nginx启动完成之后,通过如下命令进入容器内部查看是否存在这两个文件。

1
2
3
4
5
#进入容器
kubectl exec -ti nginx-volume-nfs /bin/bash

#切换到挂载目录
cd /usr/share/nginx/html

image

面向接口编程

PV、PVC简介

使用 nfs 来作为集群存储还算简单的话,如果是使用 ceph 等存储的时候就可能会搞得你头皮发麻,比如下面一个使用 Ceph RBD 作为 Volume 的 Pod。

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
apiVersion: v1
kind: Pod
metadata:
name: rbd
spec:
containers:
- image: kubernetes/pause
name: rbd-rw
volumeMounts:
- name: rbdpd
mountPath: /mnt/rbd
volumes:
- name: rbdpd
rbd:
monitors:
- '10.16.154.78:6789'
- '10.16.154.82:6789'
- '10.16.154.83:6789'
pool: kube
image: foo
fsType: ext4
readOnly: true
user: admin
keyring: /etc/ceph/keyring
imageformat: "2"
imagefeatures: "layering"

如上面这么复杂的配置,像我这种菜鸡没有学习过 ceph 的人看起来确实不太能理解。里面包含的是存储服务器地址、用户名、授权文件位置,后面再演化的过程中,K8S出现了一组新的概念:

  • Persistent Volume (PV)
  • Persistent Volume Claim (PVC)

概念不过多扩展,PV 就是我们的存储卷,由有专业的存储知识的同学定义好之后,PVC 就是我们开发需要定义的,我们只需要声明我们需要什么样的存储,然后由 K8S 自动将 PV 和 PVC 进行配对绑定。

PVC定义

比如我声明一个如下的 PVC:

1
2
3
4
5
6
7
8
9
10
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pv-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

这里的 PVC 对象定义,我们不需要关注底层的 PV 到底是什么,我们只是定义我们自己需要的是什么,这里我们需要 1G 空间的 Volume,ReadWriteOnce 表示的是这个 Volume 的挂载方式是可读写,并且只能挂载在一个节点上。

Pod 中使用 PVC

有了这个 PVC,我们就可以在 Pod 中对它进行使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: Pod
metadata:
name: pv-pod
spec:
containers:
- name: pv-container
image: nginx
resources:
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 80
volumeMounts:
- name: pv-storage
mountPath: "/usr/share/nginx/html"
volumes:
- name: pv-storage
persistentVolumeClaim:
claimName: pv-claim

只要运维人员创建好了对应的 PV ,我们这里的 Pod 就能直接运行起来,比前面的 hostPath 和 nfs 方式都要方便快捷很多,我们根本不需要关注底层的存储。

定义 PV

有了上面的 PVC 和绑定 PVC 的 Pod,现在只需要定义一个 PV,那我们的 Pod 就能正常运行起来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kind: PersistentVolume
apiVersion: v1
metadata:
name: pv-volume
spec:
capacity:
storage: 3Gi
accessModes:
- ReadWriteOnce
- ReadOnlyMany
- ReadWriteMany
nfs:
path: /home/data
server: 192.168.50.234

创建后我们可以看到我们创建的 pv

image

同理可以创建 PVC,发现已经跟 PV 绑定

image

最后再来创建 pod,创建完成之后执行如下命令进入 pod

1
2
3
4
kubectl exec -ti pv-pod /bin/bash

#进入html目录
cd /usr/share/nginx/html/

可以看到如下结果

image

以上就是一个完整的 PV 和 PVC 的一次简单使用,体现了在 K8S 中的类似 “接口” 和 “实现” 两种思想,开发只需要知道自己需要什么,也就是 PVC。而运维人员负责给 ”接口“ 绑定具体的实现,也就是 PV

也正是因为 PV、PVC的设计,让 StatefulSet 对存储状态的管理成为了可能。

一次简单的实践

我们创建一个两个副本的 nginx的有状态应用

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
66
67
68
69
70
71
72
73
74
75
76
77
78
#pv1
kind: PersistentVolume
apiVersion: v1
metadata:
name: nginx-test-pv
spec:
capacity:
storage: 3Gi
accessModes:
- ReadWriteOnce
- ReadOnlyMany
- ReadWriteMany
nfs:
path: /home/data/pv1
server: 192.168.50.234
---
#pv2
kind: PersistentVolume
apiVersion: v1
metadata:
name: nginx-test-pv-2
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteOnce
- ReadOnlyMany
- ReadWriteMany
nfs:
path: /home/data/pv2
server: 192.168.50.234
---
#service
apiVersion: v1
kind: Service
metadata:
name: nginx-svc-test
spec:
selector:
app: nginx-sts
clusterIP: None
ports:
- port: 80
targetPort: 80
---
#StatefulSet
kind: StatefulSet
apiVersion: apps/v1
metadata:
name: web-nginx
spec:
selector:
matchLabels:
app: nginx-sts
serviceName: nginx-svc-test
replicas: 2
template:
metadata:
labels:
app: nginx-sts
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: nginx-pvc-test
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: nginx-pvc-test
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

上面的声明式定义文件中,我们创建了两个 pv,创建了一个无头服务 nginx-svc-test,创建了一个有状态应用 web-nginx。

从 k8s 集群中我们可以看到如下的PV、PVC,我们在 volumeClaimTemplates 定义了PVC,从 PVC 的名字来看我们就知道,每个 Pod 的编号也对应了 PVC 的编号,并且 PVC 现在都是处于已经绑定的状态,PVC 的名字是以 PVC名字-StatefulSet名字-编号 组成的,并且已经处于 Bound 的状态

image

同时我们也能看到创建出来的两个 pod

image

StatefulSet 对应的文件写入

执行下面这条命令,分别给两个 pod 写入 不同的文件内容

1
for i in 0 1; do kubectl exec web-nginx-$i -- sh -c 'echo $(hostname) > /usr/share/nginx/html/index.html';done

现在我们再来执行下面的命令,可以访问到 pod 里面的 index.html 的内容

1
2
3
4
5
6
7
kubectl exec -ti web-nginx-0 /bin/bash
root@web-nginx-0:/# cat /usr/share/nginx/html/index.html
web-nginx-0

kubectl exec -ti web-nginx-1 /bin/bash
root@web-nginx-1:/# cat /usr/share/nginx/html/index.html
web-nginx-1

从上面的输出看到两个pod都有自己的存储,并且我现在如果删除一个pod,两个pod会按照编号的顺序被重新创建,你删除一个之后,再执行上面的命令,你会发现我们刚才写入的文件内容都还在。

原理如下:

  1. pod被删除之后,pod 对应的 pv 和 pvc 不会被删除,因为 volume 里面已经写入了数据
  2. StatefulSet 中 如果 web-nginx-0 被删除了,StatefulSet 看到这个情况后会里面创建一个新的,名字还是叫 web-nginx-0 的 pod 出来,并且这个新的 pod 使用的 pvc 名字还是 nginx-pvc-test-web-nginx-0
  3. 所以就算这个 pod 是新创建的,但是他对应的 pvc 的名字已经确定了,所以它能够找到老的这个 pvc 继续绑定使用

通过以上这种流程,StatefulSet 就实现了对应用存储状态的管理

image

总结

K8S 中通过 Headless Service 为 StatefulSet 的 Pod 创建不同的 DNS 记录,web-nginx-0.nginx-sts.default.svc.cluster.local,不管 Pod 在哪重建,它的 DNS 记录都不会变,这条 DNS 记录解析出来的 IP 地址会根据 Pod 的改变而改变,同时还能创建对应编号的 PVC,绑定到不同的 PV 上面去,Pod 删除之后对应的 PVC 也不会改变吗,保存了以前 Volume 里面的数据。

StatefulSet 的独特之处就在于 编号