一、存储卷基础
1.1 背景
Pod本身具有生命周期,其应用容器及生成的数据均无法独立于该生命周期之外持久存在。同一Pod中的容器默认共享PID、network、IPC(进程间通信)、UTS名称空间,但Mount和USER仍各自独立。因此跨容器间的进程彼此间默认无法基于共享的存储空间交换数据。由此看来,借助独立于Pod生命周期的存储设备实现数据持久化成了必然选择。
1.2 概述
存储卷是定义在Pod资源之上可被其内部所有容器挂载的共享目录,该目录关联至宿主机或某外部存储设备之上的存储空间。存储卷独立于Pod的生命周期,它存储的数据在容器重启或重建后能继续使用。
1.3 分类
In-Tree存储卷插件
- 临时存储卷:emptyDir
- 本地存储卷:hostPath,local
- 网络存储卷
- 文件系统:NFS、CephFS和Cinder
- 块设备:iscsi、FC、RBD和vSphereVolume
- 存储平台:Quobyte、PortworxVolume、StorageOS和ScaleIO
- 云存储:awsElasticBlockStore、gcePersistentDisk、azureDisk和azureFile
- 特殊存储卷:Secret、ConfigMap、DownwardAPI和Projected
Out-of-Tree存储卷插件
- CSI和FlexVolume
通常,kubernetes内置的存储卷插件可以归类为In-Tree类型,它们与kubernetes源码一起发布迭代,而由存储厂商借助CSI或FlexVolume接口扩展的独立于kubernetes源码的存储卷插件统称为Out-of-Tree类型,集群管理员可以根据需要创建自定义的扩展插件,CSI是比较常用的实现方式。
1.4 配置格式
spec:
volumes:
#卷名称标识,仅可使用DNS标签格式的字符,在当前Pod中必须惟一;
- name <string>
#存储卷插件及具体的目标存储供给方的相关配置;
VOL_TYPE <Object>
containers:
- name: …
image: …
volumeMounts:
#要挂载的存储卷的名称,必须匹配存储卷列表中某项的定义;
- name <string>
#容器文件系统上的挂载点路径;
mountPath <string>
#是否挂载为只读模式,默认为否;
readOnly <boolean>
#挂载存储卷上的一个子目录至指定的挂载点;
subPath <string>
#挂载由指定的模式匹配到的存储卷的文件或目录至挂载点;
subPathExpr <string>
#挂载卷的传播模式;
mountPropagation <string>
.spec.volumes字段定义存储卷列表,嵌套在容器内的volumeMounts字段只能挂载当前Pod对象中已定义的存储卷。
要想获取Kubernetes内置的存储卷插件类型,可使用如下命令:
kubectl explain pods.spec.volumes
1.5 临时存储卷
1.5.1 emptyDir存储卷
它可以理解为Pod对象上的一个临时目录,类似于docker的存储卷,在Pod对象启动时被创建,在Pod对象被移除时一并被删除(生命周期同Pod)。因此,emptyDir只能用于一些特殊场景中,比如同一Pod内多个容器间的文件共享,或作为容器数据的临时存储目录。
emptyDir存储卷嵌套定义在.spec.volumes.emptyDir字段中,可用字段介绍如下:
- medium:此目录所在的存储介质的类型,可用值为defalut或Memory,默认为default,标识使用节点的默认存储介质;Memory表示使用基于RAM的临时文件系统tmpfs,总体可用空间受限于内存,但性能好,通常用于为容器中的应用提供缓存存储
- sizeLimit:当前存储卷的空间限额,默认值为nil,表示不限制;但当medium字段值为Memory时,建议定义该限额。
下面给出一个示例。
vim volumes-emptydir-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: volumes-emptydir-demo
namespace: default
spec:
initContainers:
- name: config-file-downloader
image: ikubernetes/admin-box
imagePullPolicy: IfNotPresent
command: ['/bin/sh','-c','wget -O /data/envoy.yaml https://raw.githubusercontent.com/iKubernetes/Kubernetes_Advanced_Practical_2rd/main/chapter4/envoy.yaml']
volumeMounts:
- name: config-file-store
mountPath: /data
containers:
- name: envoy
image: envoyproxy/envoy-alpine:v1.13.1
command: ['/bin/sh','-c']
args: ['envoy -c /etc/envoy/envoy.yaml']
volumeMounts:
- name: config-file-store
mountPath: /etc/envoy
readOnly: true
volumes:
- name: config-file-store
emptyDir:
medium: Memory
sizeLimit: 16Mi
在该yaml中,为Pod对象定义了一个名为config-file-store、基于emptyDir存储插件的存储卷。初始化容器将存储卷挂载到/data目录下,下载envoy.yaml并保存到该挂载点目录下。主容器将存储卷挂载到/etc/envoy目录,通过自定义命令让容器启动时加载存储卷下的envoy.yaml。
查看Pod资源的详情,包括是否创建成功(events字段输出)、相关的类型及参数(Volumes字段输出)、以及容器中的挂载状态(Containers字段输出)。
kubectl describe pod volumes-emptydir-demo
……
Init Containers:
config-file-downloader:
……
Mounts:
/data from config-file-store (rw)
……
Containers:
envoy:
……
Mounts:
/etc/envoy from config-file-store (ro)
……
Volumes:
config-file-store:
Type: EmptyDir (a temporary directory that shares a pod's lifetime)
Medium: Memory
SizeLimit: 16Mi
……
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 17s default-scheduler Successfully assigned default/volumes-emptydir-demo to k8s-node2
Normal Pulling 16s kubelet Pulling image "ikubernetes/admin-box"
1.6 hostPath存储卷
hostPath存储卷插件是将工作节点上某文件系统的目录关联到Pod上的一种存储卷类型,其数据具有同工作节点生命周期一致的持久性。
配置参数介绍如下:
- path:指定工作节点的目录路径,必选字段
- type:指定节点之上的存储类型,分别有如下几种:
- DirectoryOrCreate:当指定的路径不存在时,自动创建0755权限的空目录,属主和数组均为kubelet
- Directory:事先必须存在的目录路径
- FileOnCreate:当指定的文件路径不存在时,自动创建0644权限的空文件,属主和数组均为kubelet
- File;事先必须存在的文件路径
- Socket:事先必须存在的Socket文件路径
- CharDevice:实现必须存在的字符设备文件路径
- BlockDevice:事先必须存在的块设备文件路径
- “”:空字符串,默认配置,在关联hostPath存储卷之前不做任何检查
注意:当Pod被删,后续被调度到别的节点,则数据依然会丢失。
下面给出一个例子,该例子定义了用于运行redis的Pod。
vim pod-with-hostpath.yml
apiVersion: v1
kind: Pod
metadata:
name: redis
spec:
containers:
- name: redis
image: redis:6
imagePullPolicy: IfNotPresent
volumeMounts:
- name: redis-data
#将存储卷挂载到容器的哪个目录
mountPath: /data
volumes:
- name: redis-data
hostPath:
#指定工作节点的存储路径(数据持久化目录)
path: /data/redis
type: DirectoryOrCreate
在K8S-Node3节点查看,发现果然有/data/redis路径。
进入redis容器里面写一些数据并保存,发现在容器内的/data目录生成rdb文件(redis默认持久化方式是RDB)。同样,在K8S-Node3的/data/redis目录下也能看到新生成的rdb文件。
此时删掉该Pod,再重新apply该yml文件。如果碰巧又被调度到K8S_Node3节点,则之前在Pod内生成的rdb文件还存在,否则调度到别的节点时rdb文件就丢失。由此说明,hostPath存储卷的生命周期为节点(Node)级别。
1.7 网络存储卷
Kubernetes内置了多种类型的网络存储卷插件。这些服务通常都是独立运行的存储系统,其相应的存储卷支持跨节点的数据持久性。
1.7.1 NFS存储卷
NFS(Network File System),网络文件系统,它是一种分布式文件系统协议,旨在允许客户端可以像访问本地存储一样通过网络访问服务端文件。
Kubernetes的NFS存储卷用于关联某个事先存在的NFS服务器上导出的存储空间到Pod对象中供容器使用,此类存储卷在Pod对象终止后被卸载而非被删除。而且NFS是文件系统级共享服务,支持同时存在的多路挂载请求,可由多个Pod对象同时关联使用。
相关字段介绍如下:
- server:NFS服务器地址或主机名,必选字段
- path:NFS服务器共享的文件系统路径,必选字段
- readOnly:是否以只读方式挂载,默认为false
#这里找一台服务器(就用K8S_Master2)部署NFS Server软件包
apt-get -y install nfs-kernel-server
#设置用户、数据目录,并授权
mkdir -p /data/redis
useradd -u 999 redis
chown redis /data/redis
#编辑/etc/exports配置文件,添加如下内容:
/data/redis 192.168.131.0/24(rw,no_subtree_check,no_root_squash)
#重启nfs服务器
systemctl restart nfs-server
#在Kubernetes的Node节点安装nfs客户端程序包
apt-get -y install nfs-common
#(K8S的Master节点)创建配置文件,使用nfs存储卷
vim pod-with-nfs.yml
apiVersion: v1
kind: Pod
metadata:
name: redis-nfs
spec:
containers:
- name: redis-nfs
image: redis:6
imagePullPolicy: IfNotPresent
volumeMounts:
- name: redis-data
mountPath: /data
volumes:
- name: redis-data
nfs:
server: 192.168.131.12
path: /data/redis
readOnly: false
资源创建完成后,进入Pod,通过redis-cli创建测试数据。
为了测试其持久化效果,先删除之前创建的Pod对象redis-nfs,之后重建该Pod对象,发现redis中仍然保存着先前创建的数据,在Node节点的/data/redis目录和容器内部的/data目录都能看到之前生成的rdb文件。这说明,删除Pod对象后,其关联的外部存储设备(nfs)及其存储的数据并不会被一同删除。
二、持久存储卷
在Pod级别定义存储卷有两个弊端:
- 每创建一个Pod,就要手动定义存储卷,比较麻烦。
- 用户必须要足够熟悉可用的存储服务及其配置细节才能在Pod上配置和使用卷。
为了实现存储的“生产”与“消费”的分离,PV(Persistent Volume,持久卷)和PVC(Persistent Volume Claim,持久卷申请)应运而生。
PV是由管理员创建并管理的,是集群级别的资源。但不能被Pod直接使用。
PVC表现的是用户对存储的请求。由用户创建。它是名称空间级别的资源,可被Pod直接使用。
PV与PVC是一对一的关系:一个PVC仅能绑定一个PV,而一个PV在某一时刻也只能被一个PVC所绑定。
PV如何满足PVC的期望?
管理员创建多种规模的PV;或者由使用者提交工单,管理员创建满足其要求的PV,用户创建PVC,但是这中间会存在沟通偏差等问题。这属于静态创建PV。
但更好的解决方案是管理员基于存储类(StorageClass)资源创建“PV模板”,这是动态创建PV机制的核心。
在YAML文件中定义PVC时,需要指定一个StorageClass名称,然后等到用户要创建这个PVC时,系统会根据PVC定义的需求,并参考StorageClass的存储细节,最后通过调用StorageClass声明的存储插件(Provisioner),动态创建出需要的PV与PVC绑定。之后用户将PVC作为存储对象挂载到Pod中进行使用。
总结:PVC与PV二者的关系是,由管理员创建PV连接到后端存储,使用人员创建PVC使用PV资源
2.1 PV和PVC
2.1.1 PV资源
PV是标准的资源类型,除了负责关联至后端存储系统外,它还需要定义支持的存储特性。
- capacity:指定PV的容量。
- AccessMode:指定当前PV支持的访问模式,分为ReadWriteOnce(单路读写,简称RWO)、ReadWriteMany(多路读写,简称RWX)、ReadOnlyMany(多路只读,简称ROX)和ReadWriteOncePod(单路Pod读写,简称RWOP,Kubernetes1.22版本以后出现)。ReadWriteMany(多路读写)指卷能被集群多个节点挂载并读写。ReadWriteOnce(单路读写)指卷只能被单一集群节点挂载读写。ReadOnlyMany(多路只读)指卷能被多个集群节点挂载且只能读。ReadWriteOncePod(单路Pod读写)指卷只能被集群内单个节点的单个Pod挂载读写。通常,块设备一般不支持多路读写,而文件系统级别的设备(eg:NFS)支持多路读写、多路只读。
- VolumeMode:当前PV的存储空间模型,分为块设备和文件系统两种。
- StorageClassName:当前PV隶属的存储类。不论静态还是动态创建PV,PVC只能在同一个SC(StorageClass)中找PV去绑定。如果PVC不属于任何一个SC,则其绑定的PV也不能属于任何一个SC。
当相应的PVC删除后自动解除与PV的绑定时,PV怎么处理?由其回收策略决定。回收策略介绍如下:
- Retain(保留):删除PVC后保留其绑定的PV及其存储的数据,PV为Released状态。不可被其它PVC绑定,需要手工处理。
- Recycle(回收):删除PVC时,其绑定的PV所关联的外部存储组件上的数据被清空。之后该PV转为Available状态,可再次接受其它PVC额度绑定请求。目前该策略已被废弃。
- Delete(删除):删除PVC将同时删除其绑定的PV资源和该PV关联的外部存储组件。动态的PV回收策略继承自StorageClass资源。
生产环境建议使用Retain策略,Delete策略有一定风险。
2.1.2 PVC资源
PVC也是标准的资源类型,它允许用户按需指定期望的用户特性,以此为条件,按特定的条件顺序进行PV过滤。
VolumeMode->LabelSelector->StorageClassName->AccessMode->Size
2.2 使用静态PV和PVC
使用静态PV和PVC配置卷的步骤如下:
- 集群管理员配置网络存储资源(eg:NFS)。
- 管理员基于NFS服务器信息创建PV。
- 用户创建PVC申请存储资源。
- Kubernetes找到一个足够大小和适配访问模式的PV,并将PVC与PV绑定。
- 此时Pod就可以使用PVC作为自身的存储空间使用了。
下面给出使用示例。
首先是NFS PV资源定义示例。
vim pv-nfs-demo.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs-demo
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
mountOptions:
- hard
- nfsvers=4.1
nfs:
path: "/data/redis"
server: 192.168.131.12
kubectl apply -f pv-nfs-demo.yaml
创建好PV后可以看到PV的名称、存储空间、访问模式、回收策略、状态、存储类别。状态为Available表示该PV当前未被其它PVC所绑定。此时它可以接受其它PVC的绑定。由于未定义存储类,所以存储类(StorageClass)显示为空。存储类别为Filesystem。
接着看PVC资源定义示例。
vim pvc-demo-0001.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-demo-0001
namespace: default
spec:
accessModes: ["ReadWriteMany"]
volumeMode: Filesystem
resources:
requests:
storage: 3Gi
limits:
storage: 10Gi
kubectl apply -f pvc-demo-0001.yaml
该PVC定义了名称空间为default,使用资源最小3G,最大10G。刚才创建的PV为5G,满足PVC资源定义需求。同时PVC设置的存储卷模式为Filesystem,跟刚才的PV也是一致的。所以刚才的PV(pv-nfs-demo)就与PVC(pvc-demo-0001)绑定了。此时PVC和PV的状态均为Bound。另外可以看到,PV和PVC均未设置存储类。
接着看Pod使用PVC的示例。
vim volumes-pvc-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: volumes-pvc-demo
#只能使用同一名称空间下的PVC,所以这里的名称空间是default
namespace: default
spec:
containers:
- name: redis
image: redis:6
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6379
name: redisport
volumeMounts:
#引用PVC卷并挂载到/data目录
- mountPath: /data
name: redis-pvc-vol
volumes:
#定义PVC
- name: redis-pvc-vol
persistentVolumeClaim:
#引用先前创建的PVC名称
claimName: pvc-demo-0001
kubectl apply -f volumes-pvc-demo.yaml
Pod资源创建完成后,进入Pod,通过redis-cli创建测试数据。
删除Pod,再重建Pod。进入Pod发现之前的测试数据还在。同时PV和PVC也都在。所以,即使删除Pod,但卷仍然存在,这得益于PV与PVC的加持。
如果删除了PVC,PV并不会删除,但PV的状态转为released(表示被PVC释放了),也不能被其它PVC绑定,需要手工处理(回收策略为Retain)。而且此前的绑定记录还在。
不同情况下,PV和PVC的状态变化如下表。
操作 | PV状态 | PVC状态 |
创建PV | Available | - |
创建PVC | Available | Pending |
过几秒钟 | Bound | Bound |
删除PV | -/Terminating | Lost/Bound |
重新创建PV | Bound | Bound |
删除PVC | Released | - |
后端存储不可用 | Failed | - |
删除PV的claimRef | Available | - |
2.3 StorageClass
StorageClass(SC)也是Kubernetes所支持的标准资源类型之一。简单的理解为,为了便于管理PV资源而按需创建的存储资源类别(逻辑组),是PVC筛选PV的过滤条件之一。
它为动态创建PV提供“模板”,相关资源参数定义在parameters字段中,还需要使用provisioner字段指明存储服务类型。由于StorageClass背后无需控制器支持,故配置时不用写spec字段。
StorageClass一般由集群管理员定义,隶属集群级别。PVC要么绑定某个静态PV,要么向SC请求置备动态PV。
三、CSI简介
存储卷管理器通过调用存储卷插件实现当前节点上存储卷相关的操作(挂载、卸载等)。对于未被Kubernetes内置(In-Tree)的卷插件所支持的存储服务而言,扩展定义新的卷插件是解决问题的唯一途径。但将存储供应商提供的第三方存储代码打包到Kubernetes的核心代码会导致可靠性和安全性方面的问题,为此需要一个外置于Kubernetes(Out-Of-Tree)的扩展方式,CSI(容器存储接口)就是这样的存储卷插件。
如下示例中,由于NFS默认不支持动态存储,需要使用第三方的NFS驱动。
3.1 Kubernetes使用NFS动态存储(CSI-NFS-Driver)
3.1.1 部署nfs服务
kubectl create namespace nfs
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/deploy/example/nfs-provisioner/nfs-server.yaml --namespace nfs
3.1.2 安装nfs driver
git clone https://github.com/iKubernetes/learning-k8s.git
cd learning-k8s/csi-driver-nfs/deploy/03-csi-driver-nfs-4.2/
kubectl apply -f .
检查状态
kubectl -n kube-system get pod -o wide -l 'app in (csi-nfs-node,csi-nfs-controller)'
3.1.3 创建StorageClass资源
其provisioner为nfs.csi.k8s.io,而parameters.server要指向准备好的NFS Server的访问入口。由于前面使用的名称空间为nfs,这里parameters.server的访问名称为nfs-server.nfs.svc.cluster.local(即第二个nfs的由来)。
vim storageclass-nfs.yml
--
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: nfs.csi.k8s.io
parameters:
#server: nfs-server.default.svc.cluster.local
server: nfs-server.nfs.svc.cluster.local
share: /
reclaimPolicy: Retain
volumeBindingMode: Immediate
mountOptions:
- hard
- nfsvers=4.1
kubectl apply -f storageclass-nfs.yml
此时,新创建的StorageClass资源也能查看到。
3.1.4 创建PVC,验证其动态置备功能
vim dynamic-pvc.yml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-nfs-dynamic
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
storageClassName: nfs-csi
kubectl apply -f dynamic-pvc.yml
此时查看PVC状态,发现已经处于Bound(绑定PV)状态。以pvc开头命名的PV根据PVC的创建需求自动创建,大小为10G。
3.1.5 创建Pod,验证动态创建的PV能否正常使用
vim volumes-pvc-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: redis-dynamic-pvc
namespace: default
spec:
containers:
- name: redis
image: redis:6
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6379
name: redisport
volumeMounts:
- mountPath: /data
name: redis-pvc-vol
volumes:
- name: redis-pvc-vol
persistentVolumeClaim:
claimName: pvc-nfs-dynamic
kubectl apply -f volumes-pvc-demo.yaml
资源创建成功后,同样进入Pod内的redis-cli客户端创建测试数据,再删除Pod,再创建Pod,发现测试数据仍然存在。说明动态创建PV也能实现持久化功能。
标签:存储,PV,Kubernetes,redis,PVC,nfs,Pod From: https://blog.51cto.com/u_15796303/6364999