一、Volume 的类型
Kubernetes 的 Volume 有非常多的类型,在实际使用中使用最多的类型如下。
- emptyDir: 一种简单的空目录,主要用于临时存储。
- hostPath: 将主机某个目录挂载到容器中。
- ConfigMap、Secret: 特殊类型,将 Kubernetes 特定的对象类型挂载到Pod,
- persistentVolumeClaim : Kubernetes 的持久化存储类型
1.EmptyDir
EmptyDir 是最简单的一种 Volume 类型,根据名字就能看出,这个 Volume 挂载后就是一个空目录,应用程序可以在里面读写文件,emptyDir Volume 的生命周期与 Pod 相同,Pod 删除后 Volume 的数据也同时删除掉。
emptyDir 的一些用途
-
缓存空间,例如基于磁盘的归并排序。
-
为耗时较长的计算任务提供检查点,以便任务能从崩溃前状态恢复执行
2. HostPath
HostPath 是一种持久化存储,emptyDir 里面的内容会随着 Pod 的删除而消失,但 HostPath 不会,如果对应的 Pod 删除,HostPath Volume 里面的内容依然存在于节点的目录中,如果后续重新创建 Pod 并调度到同一个节点,挂载后依然可以读取到之前 Pod 写的内容。
HostPath 存储的内容与节点相关,所以它不适合像数据库这类的应用,想象下如果数据库的 Pod 被调度到别的节点了,那读取的内容就完全不一样了
yaml
apiVersion: v1
kind: Pod
metadata:
name: test-hostpath
spec:
containers:
- image: nginx:alpine
name: hostpath-container
volumeMounts:
- mountPath: /test-pd
name: test-volume
volumes:
- name: test-volume
hostPath:
path: /data
PV & PVC & StorageClass
详细介绍见下节
二、PV & PVC
1. PV 和 PVC
PV 描述的是持久化存储卷,主要定义的是一个持久化存储在宿主机上的目录,比如一个NFS的挂载目录。
PVC 描述的是 Pod 所希望使用的持久化存储的属性,比如,Volume 存储的大小、可读写权限等等。
Kubernetes 管理员设置好网络存储的类型,提供对应的PV描述符配置到 Kubernetes, 使用者需要存储的时候只需要创建 PVC,然后在 Pod 中使用 Volume 关联 PVC,即可让 Pod使用到存储资源,它们之间的关系如下图所示。

PV是集群级别的资源,并不属于某个命名空间
而PVC是命名空间级别 的资源,PV可以与任何命名空间的PVC资源绑定

上节说的PV和PVC方法虽然能实现屏蔽底层存储,但是存在缺点如下:
- 但是PV创建比较复杂,通常都是由集群管理员管理,这非常不方便
- 并且这种方式使得集群管理员通常需要提前创建好多个PV,比较不太方便,因为手动创建再多的PV如果没被使用很浪费空间,但是如果创建少了又会导致不够用
Kubernetes 解决这个问题的方法是提供动态配置PV的方法,可以自动创PV,也就是 StorageClass
2.StorageClass
管理员可以部署PV配置器(provisioner),然后定义对应的 StorageClass,这样开发者在创建 PVC的时候就可以选择需要创建存储的类型,PVC 会把 StorageClass 传递给 PV provisioner,由 provisioner 自动创建 PV
我们参考创建一个 local-path 类型的 Provisioner
Local Path Provisioner provides a way for the Kubernetes users to utilize the local storage in each node. Based on the user configuration, the Local Path Provisioner will create either
hostPath
orlocal
based persistent volume on the node automatically. It utilizes the features introduced by Kubernetes Local Persistent Volume feature, but makes it a simpler solution than the built-inlocal
volume feature in Kubernetes.
1.创建 provisioner
bash
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.24/deploy/local-path-storage.yaml
2.验证创建PV&PVC
创建 PVC
yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: local-path-pvc
namespace: default
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 2Gi
创建 Pod
yaml
apiVersion: v1
kind: Pod
metadata:
name: volume-test
namespace: default
spec:
containers:
- name: volume-test
image: nginx:stable-alpine
imagePullPolicy: IfNotPresent
volumeMounts:
- name: volv
mountPath: /data
ports:
- containerPort: 80
volumes:
- name: volv
persistentVolumeClaim: # 使用PVC之后这里只需要填写PVC的名字
claimName: local-path-pvc
可以看到PV和PVC都已经创建出来
lua
➜ ~ k get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
local-path-pvc Bound pvc-c049abb3-76c4-41c2-a5cf-a54045c542a1 2Gi RWO local-path 21h
➜ ~ k get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-c049abb3-76c4-41c2-a5cf-a54045c542a1 2Gi RWO Delete Bound default/local-path-pvc local-path 21h
这里的 default/local-path-pvc 就表示这个pvc绑定了default空间下的local-path-pvc的pvc
3.StatefulSet 有状态应用
1. 什么是 StatefulSet?
StatefulSet 是用来管理有状态应用的对象。和 Deployment 相同的是,StatefulSet管理了基于相同容器定义的一组Pod。但和 Deployment 不同的是,StatefulSet 为它们的每个Pod维护了一个固定的ID。这些Pod是基于相同的声明来创建的,但是不能相互替换,无论怎么调度,每个Pod都有一个永久不变的ID
特点:
- 部署有状态应用
- 解决 Pod 独立生命周期,保持 Pod 启动顺序和唯一性
- 有序,优雅的部署和扩展、删除和终止(例如: mysql 主从关系,先启动主,再启动从)
应用场景:数据库
附:有状态和无状态的区别:
-
无状态:1. deployment认为所有的pod都是一样的 2.不用考虑顺序的要求 3.不用考虑在哪个node节点上运行 4. 可以随意扩容和缩容
-
有状态:1.实例之间有差别,每个实例都有自己的独特性,元数据不同,例如 etcd, zookeeper 2.实例之间不对等的关系,以及依靠外部存储的应用
2. StatefulSet怎么运作
- StatefulSet 给每个 Pod 提供固定名称,Pod名称增加从0-N的固定后缀,Pod重新调度后 Pod 名称和 HostName不变。
- StatefulSet 通过Headless Service 给每个Pod提供固定的访问域名,Service的概念会在后面章节中详细介绍
- StatefulSet 通过创建 固定标识的 PVC ****保证Pod重新调度后还是能访问到相同的持久化数据
总结就是使用了 headless Service 和 PVC
3. 实践
1. 什么是 headless service?
前面讲的Service解决了Pod的内外部访问问题,但还有下面这些问题没解决。
- 同时访问所有Pod
- 一个Service内部的Pod互相访问
Headless Service 正是解决这个问题的,Headless Service不会创建ClusterIP,并且查询会返回所有 Pod 的DNS记录,这样就可查询到所有Pod的IP地址
总结:也就是 headless 可以理解为比 service 更加细粒度,使用 headless service 可以按照 pod 的域名(比如下面的 nginx-0.nginx [pod名+service名] )访问到一个具体的 pod,这就是为什么 headless service 为什么总是和 StatefulSet 一起使用,因为 StatefulSet 创建出来的 pod 的名称总是有序并且固定的,即使 pod 发生了重启也不会改表
2. 创建 headless service 和 StatefulSet
yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- name: nginx
port: 80
selector:
app: nginx
clusterIP: None # headless service和普通的servide的区别就是这里ClusterIP定义为None
yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nginx
spec:
serviceName: nginx
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: container-0
image: nginx:alpine
resources:
limits:
cpu: 100m
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts: # Pod挂载的存储
- name: data
mountPath: /usr/share/nginx/html
imagePullSecrets:
- name: default-secret
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: local-path # 指定storageclass
创建一个Pod来查询DNS,可以看到能返回所有Pod的记录
注意:不能在StatefulSet创建出来的pod里查DNS记录,必须要新建一个其他的pod查询
Headless Service创建后,每个Pod的IP都会有下面格式的域名
...svc.cluster.local 例如上面的三个Pod的域名就是:
- nginx-0.nginx.default.svc.cluster.local
- nginx-1.nginx.default.svc.cluster.local
- nginx-2.nginx.default.svc.cluster.local
实际访问时可以省略后面的 ..svc.cluster.local。
进入容器查看容器的hostname,可以看到同样是nginx-0、nginx-1和nginx-2
bash
➜ ~ k exec -it nginx-0 -- /bin/sh -c 'hostname'
nginx-0
➜ ~ k exec -it nginx-1 -- /bin/sh -c 'hostname'
nginx-1
➜ ~ k exec -it nginx-2 -- /bin/sh -c 'hostname'
nginx-2
同时可以看一下 StatefulSet 创建的PVC,可以看到这些 PVC,都以"PVC名称- StatefulSet名称-编号"的方式命名,并且处于 Bound 状态
lua
➜ ~ k get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-nginx-0 Bound pvc-9d1f461e-46ed-437b-977c-6b50de279dab 1Gi RWO local-path 7h50m
data-nginx-1 Bound pvc-4aedc304-b5d4-4bab-adb4-ffe837fa02d6 1Gi RWO local-path 7h50m
data-nginx-2 Bound pvc-699c9134-ef29-476a-812a-7012197c6611 1Gi RWO local-path 7h50m
pvc 创建出的 pv 的默认回收策略是 Delete,即删除PVC时删除对应的PV
sql
➜ ~ k get pv pvc-4aedc304-b5d4-4bab-adb4-ffe837fa02d6
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-4aedc304-b5d4-4bab-adb4-ffe837fa02d6 1Gi RWO Delete Bound default/data-nginx-1 local-path 21d
➜ ~
3. 存储状态
上面说了 StatefulSet 可以通过 PVC 做持久化存储,保证 Pod 重新调度后还是能访问到相同的持久化数据,在删除Pod 时,PVC 不会被删除

执行下面的命令,在 nginx-1 的目录 /usr/share/nginx/html 中写入一些内容,例如将 index.html 的内容修改为"hello world"
sql
k exec nginx-1 -- sh -c 'echo hello world > /usr/share/nginx/html/index.html'
修改后再访问就会返回 hello world
➜ ~ k exec -it nginx-1 -- curl localhost
hello world
删除这个nginx-1的pod
➜ ~ k delete pod nginx-1
pod "nginx-1" deleted
再次访问,还是会得到 "hello world"
➜ ~ k exec -it nginx-1 -- curl localhost
hello world
删除 StatefulSet 中的 pod 时,其创建的 PVC 是不会自动删除的,必须手动删除;但是 k8s 1.24 版本引入了一个新的特性支持删除 pod 时自动删除对应的 PVC
4. 手动清理PVC
删除掉 StatefulSet 之后对应的 PVC 还是存在
lua
➜ ~ k get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-nginx-0 Bound pvc-9d1f461e-46ed-437b-977c-6b50de279dab 1Gi RWO local-path 8h
data-nginx-1 Bound pvc-4aedc304-b5d4-4bab-adb4-ffe837fa02d6 1Gi RWO local-path 8h
data-nginx-2 Bound pvc-699c9134-ef29-476a-812a-7012197c6611 1Gi RWO local-path 8h
找到对应的 PV

查看对应目录的文件,可以发现之前写入的数据仍然在对应的 index.html 里面,并没有随着pod的删除而被清理掉
bash
➜ pvc-4aedc304-b5d4-4bab-adb4-ffe837fa02d6_default_data-nginx-1 ls
index.html
➜ pvc-4aedc304-b5d4-4bab-adb4-ffe837fa02d6_default_data-nginx-1 cat index.html
hello world
为什么创建出来的 volume 都是在 /opt/local-path-provisioner 目录,是因为在创建 provisioner 时会创建出一个 configmap,这个 cm 指定了对应的挂载路径
