更新时间:2023年3月
简介
持久卷(PersistentVolume,PV)、**持久卷申领(PersistentVolumeClaim,PVC)**用于实现应用程序与存储的解耦,将存储的细节信息从应用实例中剥离
持久卷(PersistentVolume,PV) 是集群中的一块存储,可以由管理员事先制备, 或者使用 存储类(Storage Class)来动态制备。 持久卷是集群资源,就像节点也是集群资源一样。PV 持久卷和普通的 Volume 一样, 也是使用卷插件来实现的,只是它们拥有独立于任何使用 PV 的 Pod 的生命周期。 此 API 对象中记述了存储的实现细节,无论其背后是 NFS、iSCSI 还是特定于云平台的存储系统
持久卷申领(PersistentVolumeClaim,PVC) 表达的是用户对存储的请求。概念上与 Pod 类似。 Pod 会耗用节点资源,而 PVC 申领会耗用 PV 资源。Pod 可以请求特定数量的资源(CPU 和内存);同样 PVC 申领也可以请求特定的大小和访问模式 (例如,可以要求 PV 卷能够以 ReadWriteOnce、ReadOnlyMany 或 ReadWriteMany 模式之一来挂载,参考 访问模式)
通常不同用户需要的是具有不同属性(如,性能)的 PersistentVolume 卷,集群需要能够提供不同性质的 PersistentVolume, 并且这些 PV 卷之间的差别对用户侧暴露仅限于卷大小和访问模式,同时又不能将卷的实现细节暴露给用户。为了满足这类需求,就有了存储类(StorageClass)
总结
持久卷(PersistentVolume,PV) 抽象了存储资源,屏蔽了存储资源的细节。整合不同的存储资源,统一存储资源暴露的属性
**持久卷申领(PersistentVolumeClaim,PVC)**抽象了用户对存储资源的需求,统一存储需求的属性
**存储类(StorageClass)**可以动态制备 持久卷(PersistentVolume,PV),进一步抽象存储资源
静态 PV 与动态 PV
静态 PV
手动创建 PV,然后创建 PVC 并绑定到指定的 PV。PVC 挂载至 Pod 使用。适用于 PV 和 PVC 比较固定的场景
动态 PV
先创建一个 StorageClass(存储类),创建 PVC 时指定 StorageClass,由 StorageClass 动态创建符合 PVC 申请条件的 PV ,并将 PVC 绑定到 PV
对比图
PV
PV 的类型
PV 持久卷通过插件的形式来实现与不同存储资源的连接调度。Kubernetes 目前支持以下插件:
cephfs
- CephFS volumecsi
- 容器存储接口 (CSI)fc
- Fibre Channel (FC) 存储hostPath
- HostPath 卷 (仅供单节点测试使用;不适用于多节点集群;请尝试使用local
卷作为替代)iscsi
- iSCSI (SCSI over IP) 存储local
- 节点上挂载的本地存储设备nfs
- 网络文件系统 (NFS) 存储rbd
- Rados 块设备 (RBD) 卷
Kubernetes 支持的插件随版本变化较大,使用前请先查看当前使用的版本是否支持对应的存储插件
阶段
每个 PV 会处于以下阶段(Phase)之一
-
Available(可用)
PV 是一个空闲资源,尚未绑定到任何申领
-
Bound(已绑定)
该 PV 已经绑定到某 PVC
-
Released(已释放)
所绑定的 PVC 已被删除,但是资源尚未被集群回收
-
Failed(失败)
PV 的自动回收操作失败
命令行接口能够显示绑定到某 PV 卷的 PVC 对象
简单示例
PV 对象 yaml 文件示例
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv0003
spec:
# 容量
capacity:
storage: 5Gi
# 卷模式
volumeMode: Filesystem
# 访问模式
accessModes:
- ReadWriteOnce
# 回收策略
persistentVolumeReclaimPolicy: Recycle
# 指定存储类
storageClassName: slow
# 挂载选项
mountOptions:
- hard
- nfsvers=4.1
nfs:
path: /tmp
server: 172.17.0.2
注:在集群中使用 PV 存储通常需要一些特定于具体卷类型的辅助程序。 在这个例子中,PV 是 NFS 类型的,因此需要辅助程序
/sbin/mount.nfs
来支持挂载 NFS 文件系统
主要参数
容量
每个 PV 卷都有确定的存储容量。 可以使用 PV 对象的 capacity
属性来设置
目前,存储大小是可以设置和请求的唯一一个资源。 未来可能会包含 IOPS、吞吐量等属性
卷模式
Kubernetes 目前支持两种卷模式,由
Filesystem(文件系统)
Block(块)
卷模式由 volumeMode
参数指定。 如果该参数未设置,默认的卷模式是 Filesystem
volumeMode
属性设置为 Filesystem
的卷会被 Pod 挂载(Mount)到某个目录。 如果卷的存储来自某块设备而该设备目前为空,Kuberneretes 会在第一次挂载卷之前在设备上创建文件系统
volumeMode
设置为 Block
时。卷以块设备的方式交给 Pod 使用,其上没有任何文件系统。Pod 中运行的应用必须知道如何处理原始的块设备。支持以块设备形式挂载的插件请参考:持久卷 - 原始块卷支持 | Kubernetes
访问模式
PV 可以用资源提供者所支持的任何访问方式挂载到宿主系统上,由参数 volumeMode
指定。不同存储资源插件支持的访问模式,可以参考 持久卷 - 访问模式 | Kubernetes
访问模式有:
-
ReadWriteOnce (RWO)
卷可以被一个节点以读写方式挂载。 ReadWriteOnce 访问模式也允许运行在同一节点上的多个 Pod 访问卷
-
ReadOnlyMany (ROX)
卷可以被多个节点以只读方式挂载
-
ReadWriteMany (RWX)
卷可以被多个节点以读写方式挂载
-
ReadWriteOncePod (RWOP)
卷可以被单个 Pod 以读写方式挂载。 该模式仅支持 CSI 卷,且需要 Kubernetes 1.22 以上版本。如果你想确保整个集群中只有一个 Pod 可以读取或写入该 PVC, 请使用 ReadWriteOncePod 访问模式
注:每个卷同一时刻只能以一种访问模式挂载,即使该卷能够支持多种访问模式。 例如,一个 GCEPersistentDisk 卷可以被某节点以 ReadWriteOnce 模式挂载,或者被多个节点以 ReadOnlyMany 模式挂载,但不可以同时以两种模式挂载
存储类(StorageClass)
每个 PV 可以属于某个存储类(Class),通过将 storageClassName
参数设置为某个 StorageClass 的名称来指定。特定存储类的 PV 只能绑定到请求该类存储卷的 PVC 。 未设置 storageClassName
的 PV 没有类的设定,只能绑定到那些没有指定特定存储类的 PVC
回收策略
PV 通过 persistentVolumeReclaimPolicy
参数指定回收策略,目前支持的回收策略有
Retain
:手动回收Recycle
:基本擦除 (rm -rf /thevolume/*
)Delete
:诸如 AWS EBS、GCE PD、Azure Disk 或 OpenStack Cinder 卷这类关联存储资产也被删除
目前,仅 NFS 和 HostPath 支持回收(Recycle)
挂载选项
可以通过 mountOptions
参数指定 PV 被挂载到节点上时使用的附加挂载选项
注:并非所有持久卷类型都支持挂载选项
以下卷类型支持挂载选项:
awsElasticBlockStore
azureDisk
azureFile
cephfs
cinder
(于 v1.18 弃用)gcePersistentDisk
iscsi
nfs
rbd
vsphereVolume
Kubernetes 不对挂载选项执行合法性检查。如果挂载选项是非法的,挂载就会失败
PVC
简单示例
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myclaim
spec:
# 访问模式
accessModes:
- ReadWriteOnce
# 卷模式
volumeMode: Filesystem
# 资源申请
resources:
requests:
storage: 8Gi
# 存储类
storageClassName: slow
# 选择器
selector:
matchLabels:
release: "stable"
matchExpressions:
- {key: environment, operator: In, values: [dev]}
主要参数
访问模式
PVC 的访问模式指定申请的卷应该具有的访问模式,在请求具有特定访问模式的存储时,只会绑定到具有相同访问模式的 PV 上
卷模式
PVC 定义申请卷的类型,来表明是将卷作为文件系统还是块设备来使用。从而绑定到相同的卷模式的 PV 上,默认为 Filesystem
资源申请
PVC 和 Pod 一样,也可以请求特定数量的资源,或者限制资源的使用最大值。PVC 请求的资源是存储
选择器
PV 可以通过选择器来进一步过滤要绑定的卷的集合,只有标签(Label)与选择器(Selector)相匹配的卷才会绑定 PVC。选择器可以包含两个字段
matchLabels
:卷必须包含带有此值的标签matchExpressions
:通过设定键(key)、值列表和操作符(operator) 来构造的需求。合法的操作符有 In、NotIn、Exists 和 DoesNotExist
matchLabels
和 matchExpressions
的所有需求都按逻辑与的方式组合在一起。 这些需求都必须被满足才被视为匹配
存储类(StorageClass)
PVC 可以通过为 storageClassName
属性设置 StorageClass 的名称来请求特定的存储类。 只有所请求的类的 PV ,即 storageClassName
值与 PVC 设置相同的 PV , 才能绑定到 PVC
PVC 可以不指定存储类,或者 storageClassName
属性值设置为 ""
。这一 PVC 只能绑定到未设置存储类的 PV (未声明或者 storageClassName
为 ""
)。没有 storageClassName
的 PVC 并不完全相同,集群对它的处理方式也不同,这取决于是否启用了 DefaultStorageClass
准入控制器插件)
-
启用
DefaultStorageClass
准入控制器插件时,管理员可以设置一个默认的StorageClass
(默认存储类)。 所有未设置storageClassName
的 PVC 都只能绑定到隶属于默认存储类的 PV 。 设置默认StorageClass
的工作是通过将对应StorageClass
对象的注解storageclass.kubernetes.io/is-default-class
赋值为true
来完成的。 如果管理员未设置默认存储类,集群对 PVC 创建的处理方式与未启用准入控制器插件时相同。 如果设定的默认存储类不止一个,准入控制插件会禁止所有创建 PVC 操作 -
关闭
DefaultStorageClass
准入控制器插件时,则不存在默认StorageClass
(默认存储类)。 所有将storageClassName
设为""
的 PVC 只能被绑定到也将storageClassName
设为""
的 PV。注:一旦默认的
StorageClass
由不可用变为可用,建议稍后更新缺少storageClassName
的 PVC,因为如果这个 PVC 更新了,它将不再绑定到也将storageClassName
设为""
的 PV,可能产生数据丢失风险
当某 PVC 除了请求 StorageClass
之外还设置了 selector
,则这两种需求会按逻辑与关系处理: 只有隶属于所请求存储类且带有所请求标签的 PV 才能绑定到 PVC
注:目前 kubernetes 版本(< 1.26),设置了非空
selector
的 PVC 对象无法让集群为其动态制备 PV
示例-静态制备存储卷
此示例使用 NFS 提供存储资源
编写对象 yaml 声明文件
PV
$ vim test-static-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: test-static-pv
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
mountOptions:
- soft
- nfsvers=4.2
nfs:
# 共享目录需在 NFS 服务端创建并配置
path: /data/share/static
server: 192.168.111.171
PVC
$ vim test-static-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-static-pvc
spec:
volumeName: test-static-pv
accessModes:
- ReadWriteMany
# 卷模式
volumeMode: Filesystem
# 资源申请
resources:
requests:
storage: 3Gi
limits:
storage: 5Gi
应用
$ vim nginx-deploy-static-storage.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deploy-static-storage
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.22.1
ports:
- containerPort: 80
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: static-dir
volumes:
- name: static-dir
persistentVolumeClaim:
claimName: test-static-pvc
创建对象
# 创建 PV
$ kubectl apply -f test-static-pv.yaml
# 创建 PVC
$ kubectl apply -f test-static-pvc.yaml
# 创建应用,使用 PVC
$ kubectl apply -f nginx-deploy-static-storage.yaml
检查
# 检查 pod 运行状态
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deploy-static-storage-6fc7767456-h9cmf 1/1 Running 0 34s 172.20.195.36 192.168.111.187 <none> <none>
# 测试往共享目录写入数据
$ kubectl exec -it nginx-deploy-static-storage-6fc7767456-h9cmf bash
root@nginx-deploy-static-storage-6fc7767456-h9cmf:/# echo "${HOSTNAME}" > /usr/share/nginx/html/index.html
# NFS 服务器查看数据是否写入
$ ll /data/share/static/
total 4
-rw-r--r--. 1 nfsnobody nfsnobody 45 Mar 10 20:35 index.html
# 测试访问
$ curl http://172.20.195.36:80/index.html
nginx-deploy-static-storage-6fc7767456-h9cmf
示例-动态制备存储卷
此示例使用 NFS 提供存储资源
NFS 服务器设置
# 创建共享目录
$ mkdir -p /data/share/dynamic
# 授权
$ chmod o+w /data/share/dynamic
$ cat /etc/exports
/data 192.168.111.0/24(rw)
# 重启服务,nfs 依赖于 rpcbind,先重启 rpcbind
$ systemctl restart rpcbind
$ systemctl restart nfs
# 导出 nfs 维护表,重新共享目录
$ exportfs -r
准备制备器
Kubernetes 不包含内部 NFS 驱动(制备器,参考:存储类 - provisioner | Kubernetes)。需要使用外部驱动为 NFS 创建 StorageClass。可以参考以下示例 :
此处使用 nfs-subdir-external-provisioner 作为 provisioner,安装流程参考:kubernetes-sigs/nfs-subdir-external-provisioner: Dynamic sub-dir volume provisioner on a remote NFS server. (github.com)
下载
# 下载安装的 yaml
$ wget https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner/archive/refs/tags/nfs-subdir-external-provisioner-4.0.17.tar.gz
# 解压
$ tar -zxvf nfs-subdir-external-provisioner-4.0.17.tar.gz
$ mv nfs-subdir-external-provisioner-nfs-subdir-external-provisioner-4.0.17 nfs-subdir-external-provisioner
$ cd nfs-subdir-external-provisioner
授权
# 将 RBAC 对象配置部署在当前命名空间,默认在 default
$ NS=$(kubectl config get-contexts|grep -e "^\*" |awk '{print $5}')
$ NAMESPACE=${NS:-default}
$ sed -i'' "s/namespace:.*/namespace: $NAMESPACE/g" ./deploy/rbac.yaml ./deploy/deployment.yaml
# 创建 RBAC 对象
$ kubectl create -f deploy/rbac.yaml
安装配置
安装 nfs-client-provisioner
,注意需要配置镜像地址和 NFS 连接信息
$ vim ./deploy/deployment.yaml
kind: Deployment
apiVersion: apps/v1
metadata:
name: nfs-client-provisioner
spec:
replicas: 1
selector:
matchLabels:
app: nfs-client-provisioner
strategy:
type: Recreate
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
# 设置镜像地址,默认镜像地址在国内不能访问
image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: k8s-sigs.io/nfs-subdir-external-provisioner
# 设置环境变量 NFS_SERVER 为 NFS 的地址或域名
- name: NFS_SERVER
value: 192.168.111.171
# 设置环境变量 NFS_PATH 为 NFS 共享目录
- name: NFS_PATH
value: /data/share/dynamic
volumes:
- name: nfs-client-root
nfs:
# 设置 NFS 地址和共享目录
server: 192.168.111.171
path: /data/share/dynamic
应用声明
$ kubectl apply -f ./deploy/deployment.yaml
部署存储类
配置 StorageClass
nfs-subdir-external-provisioner
支持的StorageClass
参数请参考:nfs-subdir-external-provisioner
$ vim deploy/class.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # or choose another name, must match deployment's env PROVISIONER_NAME'
reclaimPolicy: Retain # PV 的删除策略
mountOptions:
- soft
- nfsvers=4.2
- noatime # 访问文件时不更新文件 inode 中的时间戳,高并发环境可提高性能
parameters:
pathPattern: "${.PVC.namespace}/${.PVC.annotations.nfs.io/storage-path}" # waits for nfs.io/storage-path annotation, if not specified will accept as empty string.
archiveOnDelete: "true" # 删除 pod 时保留 pod 数据,默认为 false,不保留数据
应用声明
$ kubectl apply -f deploy/class.yaml
# 检查状态
$ kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs-client k8s-sigs.io/nfs-subdir-external-provisioner Retain Immediate false 8s
创建 PVC
配置 PVC 声明文件
$ vim demo-dynamic-pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: demo-dynamic-pvc
annotations:
nfs.io/storage-path: "test-path" # 配合 StorageClass 的 pathPattern 参数,自定义路径
spec:
storageClassName: nfs-client # 调用的 StorageClass 名称
accessModes:
- ReadWriteMany # 访问权限
resources:
requests:
storage: 500Mi # 空间大小
应用声明
$ kubectl apply -f demo-dynamic-pvc.yaml
# 查看状态
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
demo-dynamic-pvc Bound pvc-54b4c5da-f64c-4a65-9de8-2cbf6fbb6dc7 500Mi RWX nfs-client 41s
# 查看绑定的 PV
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-54b4c5da-f64c-4a65-9de8-2cbf6fbb6dc7 500Mi RWX Retain Bound default/demo-dynamic-pvc nfs-client 69s
挂载使用
创建应用的声明文件
$ vim nginx-deploy-svc-dynamic-pvc.yaml
kind: Deployment
apiVersion: apps/v1
metadata:
labels:
app: nginx-deploy
name: nginx-deploy-dynamic-pvc
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.22.1
volumeMounts:
- mountPath: "/usr/share/nginx/html/dynamic"
name: dynamic-dir
volumes:
- name: dynamic-dir
persistentVolumeClaim:
claimName: demo-dynamic-pvc
---
kind: Service
apiVersion: v1
metadata:
labels:
app: nginx-svc
name: nginx-svc
spec:
type: NodePort
ports:
- name: http
port: 80
targetPort: 80
nodePort: 30080
selector:
app: nginx
应用声明
$ kubectl apply -f nginx-deploy-svc-dynamic-pvc.yaml
# 检查运行情况
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deploy-dynamic-pvc-9b9cd67bc-44htl 1/1 Running 0 56s
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-svc NodePort 10.68.52.65 <none> 80:30080/TCP 59s
测试
在 pod 中写入数据
$ kubectl exec -it nginx-deploy-dynamic-pvc-9b9cd67bc-44htl bash
root@nginx-deploy-dynamic-pvc-9b9cd67bc-44htl:/# echo "${HOSTNAME}" > /usr/share/nginx/html/dynamic/index.html
在 NFS 服务器上检查数据是否正常写入
$ ll /data/share/dynamic/default/test-path/
total 4
-rw-r--r--. 1 nfsnobody nfsnobody 41 Mar 11 02:23 index.html
测试访问
$ curl http://192.168.111.185:30080/dynamic/index.html
nginx-deploy-dynamic-pvc-9b9cd67bc-44htl
删除 Pod,看数据是否保留
$ kubectl delete -f nginx-deploy-svc-dynamic-pvc.yaml
# 由于 StorageClass 的回收策略为 Retain,不会删除数据,数据依旧存在,
$ ll /data/share/dynamic/default/test-path/
total 4
-rw-r--r--. 1 nfsnobody nfsnobody 41 Mar 11 02:23 index.html