K8S 存储卷-PV、PVC、NFS 和 StorageClass 深度讲解 (下)
四 网络存储
一般来讲,不会通过本地存储来持久化数据,因为Pod的调度不是固定的,一般会通过网络的方式来存储数据,比如NFS;
nfs使的我们可以挂在已经存在的共享到的我们的Pod中,和emptyDir不同的是,emptyDir会被删除当我们的Pod被删除的时候,但是nfs不会被删除,仅仅是解除挂在状态而已,这就意味着NFS能够允许我们提前对数据进行处理,而且这些数据可以在Pod之间相互传递.并且,nfs可以同时被多个pod挂在并进行读写
这里我们新增加一台虚拟机(192.168.108.100)来充当NFS服务器,安装NFS命令如下(本例中,我们在mananger节点安装nfs)
# 服务端软件安装
yum install -y nfs-utils rpcbind # 安装nfs-utils和rpcbind两个包
# 客户端软件安装(在所有node节点安装)
yum install -y nfs-utils
# 创建共享目录
mkdir -p /data/nfs
# 配置共享目录
cat > /etc/exports <<EOF
/data/nfs *(rw,no_root_squash)
EOF
# 启动nfs服务
systemctl start nfs
systemctl enable nfs
# 查看服务是否启动成功 ps aux | grep nfs
如果有如下回显,说明启动成功
root 75267 0.0 0.0 0 0 ? S< 08:44 0:00 [nfsd4_callbacks]
root 75273 0.0 0.0 0 0 ? S 08:44 0:00 [nfsd]
接下来就是改PV,只需要改动挂载类型即可,如下
[root@master01 ~]# cat PV.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-pv
labels:
type: remote # 改为remote
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
nfs: #声明nfs存储
path: /data/nfs
server: 10.1.1.106
PVC和web.yaml都不用改,然后应用
# 先删除,否则会提示重名
kubectl delete -f web-test.yaml
kubectl apply -f PV.yaml
kubectl apply -f PVC.yaml
kubectl apply -f web-test.yaml
在NFS服务端写入文件
[root@manager ~]# echo "hello big egon" > /data/nfs/a.txt
然后进行测试
[root@node01 ~]# curl 10.2.73.25/a.txt
hello big egon
五 Storage Class
先清理一下之前的实验环境
[root@master01 ~]# kubectl delete -f web-test.yaml
deployment.apps "web-test" deleted
[root@master01 ~]# kubectl delete -f PVC.yaml
persistentvolumeclaim "my-pvc" deleted
[root@master01 ~]# kubectl delete -f PV.yaml
persistentvolume "my-pv" deleted
5.1 什么是StorageClass
Kubernetes提供了一套可以自动创建PV的机制,即:Dynamic Provisioning.而这个机制的核心在于:StorageClass这个API对象.
我们可以创建不同种类StorageClass,如图:金存储类、银存储类、铜存储类等,此时用户便无需关系存储后端了
StorageClass对象会定义下面两部分内容:
1,PV的属性.比如,存储类型,Volume的大小等.
2,创建这种PV需要用到的存储插件
有了这两个信息之后,Kubernetes就能够根据用户提交的PVC,找到一个对应的StorageClass,之后Kubernetes就会调用该StorageClass声明的存储插件,进而创建出需要的PV.
但是其实使用起来是一件很简单的事情,你只需要根据自己的需求,编写YAML文件即可,然后使用kubectl create命令执行即可
完整图解如下:
5.2 为什么需要StorageClass
在pod中定义一个存储卷(该存储卷类型为PVC),定义的时候直接指定大小,pvc必须根据自己的定义去找到相对应的pv才可以建立关系;
而问题的关键是,在pvc申请存储空间时,未必就有现成的pv符合pvc申请的需求,上面nfs在做pvc可以成功的因素是因为我们做了指定的需求处理
而且,在一个大规模的Kubernetes集群里,可能有成千上万个PVC,这就意味着运维人员必须实现创建出这个多个PV,此外,随着项目的需要,会有新的PVC不断被提交,那么运维人员就需要不断的添加新的,满足要求的PV,否则新的Pod就会因为PVC绑定不到PV而导致创建失败.而且通过 PVC 请求到一定的存储空间也很有可能不足以满足应用对于存储设备的各种需求
还有,不同的应用程序对于存储性能的要求可能也不尽相同,比如读写速度、并发性能等,为了解决这一问题,Kubernetes 又为我们引入了一个新的资源对象:StorageClass,通过 StorageClass 的定义,管理员可以将存储资源定义为某种类型的资源,比如快速存储、慢速存储等,用户根据 StorageClass 的描述就可以非常直观的知道各种存储资源的具体特性了,这样就可以根据应用的特性去申请合适的存储资源了。
5.3 StorageClass运行原理及部署流程
要使用 StorageClass,我们就得安装对应的自动配置程序,比如我们这里存储后端使用的是 nfs,那么我们就需要使用到一个 nfs-client 的自动配置程序,我们也叫它 Provisioner,这个程序使用我们已经配置好的 nfs 服务器,来自动创建持久卷,也就是自动帮我们创建 PV。
1.自动创建的 PV 以${namespace}-${pvcName}-${pvName}这样的命名格式创建在 NFS 服务器上的共享数据目录中
2.而当这个 PV 被回收后会以archieved-${namespace}-${pvcName}-${pvName}这样的命名格式存在 NFS 服务器上。
1.原理及部署流程说明
详细的运作流程可以参考下图:
搭建StorageClass+NFS,大致有以下几个步骤:
1.创建一个可用的NFS Server
2.创建Service Account.这是用来管控NFS provisioner在k8s集群中运行的权限
3.创建StorageClass.负责建立PVC并调用NFS provisioner进行预定的工作,并让PV与PVC建立管理
4.创建NFS provisioner.有两个功能,一个是在NFS共享目录下创建挂载点(volume),另一个则是建了PV并将PV与NFS的挂载点建立关联
步骤1:略,我们已经安装好了
[root@manager ~]# cat /etc/exports
/data/nfs *(rw,no_root_squash)
步骤2:使用以下文档配置 account及相关权限,rbac.yaml
: 唯一需要修改的地方只有namespace,根据实际情况定义
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default #根据实际环境设定namespace,下面类同
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: nfs-client-provisioner-runner
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "update", "patch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: run-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
roleRef:
kind: ClusterRole
name: nfs-client-provisioner-runner
apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
rules:
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
roleRef:
kind: Role
name: leader-locking-nfs-client-provisioner
apiGroup: rbac.authorization.k8s.io
步骤3:创建NFS资源的StorageClass,nfs-StorageClass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: managed-nfs-storage
provisioner: egon-nfs-storage #这里的名称要和provisioner配置文件中的环境变量PROVISIONER_NAME保持一致
parameters:
archiveOnDelete: "false"
步骤4:创建NFS provisioner,nfs-provisioner.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
labels:
app: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default #与RBAC文件中的namespace保持一致
spec:
replicas: 1
selector:
matchLabels:
app: nfs-client-provisioner
strategy:
type: Recreate
selector:
matchLabels:
app: nfs-client-provisioner
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
image: quay.io/external_storage/nfs-client-provisioner:latest
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: egon-nfs-storage #provisioner名称,请确保该名称与 nfs-StorageClass.yaml文件中的provisioner名称保持一致
- name: NFS_SERVER
value: 10.1.1.106 #NFS Server IP地址
- name: NFS_PATH
value: /data/nfs #NFS服务共享出来的挂载卷,去nfs服务端查看cat /etc/exports获取
volumes:
- name: nfs-client-root
nfs:
server: 10.1.1.106 #NFS Server IP地址
path: /data/nfs #NFS 挂载卷
步骤5:应用
[root@master01 ~]# kubectl apply -f rbac.yaml
[root@master01 ~]# kubectl apply -f nfs-StorageClass.yaml
[root@master01 ~]# kubectl apply -f nfs-provisioner.yaml
=================>测试使用<=================
1、创建pvc文件指定需要的存储空间大小,无需创建pv,文件名test-claim.yaml
复制代码
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: test-claim-xxx
annotations:
volume.beta.kubernetes.io/storage-class: "managed-nfs-storage" #与nfs-StorageClass.yaml metadata.name保持一致
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Mi # 指定需要的空间大小
2、确保PVC状态为Boud
[root@master01 ~]# kubectl apply -f test-claim.yaml
persistentvolumeclaim/test-claim-xxx created
# 这期间还没有立即创建好
[root@master01 ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
test-claim-xxx Pending managed-nfs-storage 5s
[root@master01 ~]# kubectl get pv
No resources found in default namespace.
[root@master01 ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
test-claim-xxx Pending managed-nfs-storage 12s
# 过一会就创建好了,并且自动创建出了pv
[root@master01 ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
test-claim-xxx Bound pvc-bc28e56d-e10f-405e-8b3b-e27b8ef0195a 10Mi RWX managed-nfs-storage 13s
[root@master01 ~]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-bc28e56d-e10f-405e-8b3b-e27b8ef0195a 10Mi RWX Delete Bound default/test-claim-xxx managed-nfs-storage 2s
复制代码
3、创建控制器,应用上面的pvc
[root@master01 ~]# cat web-test.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: web-test
name: web-test
spec:
replicas: 1
selector:
matchLabels:
app: web-test
strategy: {}
template:
metadata:
labels:
app: web-test
spec:
containers:
- image: nginx:1.14
name: nginx
volumeMounts:
- name: wwwroot
mountPath: /usr/share/nginx/html
volumes:
- name: wwwroot
persistentVolumeClaim:
claimName: test-claim-xxx # 与pvc名保持一致
status: {}
[root@master01 ~]#
应用
[root@master01 ~]# kubectl apply -f web-test.yaml
[root@master01 ~]# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nfs-client-provisioner-6578dc77d9-pjxnq 1/1 Running 0 16m 10.2.60.122 10.1.1.105 <none> <none>
web-test-56bd8bdcbf-4fd2d 1/1 Running 0 4m58s 10.2.43.178 10.1.1.104 <none> <none>
4、去nfs主机里操作挂载点
[root@manager ~]# ll /data/nfs/
总用量 0
drwxrwxrwx 2 root root 6 9月 1 16:17 default-test-claim-xxx-pvc-bc28e56d-e10f-405e-8b3b-e27b8ef0195a
[root@manager ~]# echo 'egon111' > /data/nfs/default-test-claim-xxx-pvc-bc28e56d-e10f-405e-8b3b-e27b8ef0195a/index.html
5、去任意一台安装有kubelet的主机访问
[root@node03 ~]# curl 10.2.43.178
egon111
[root@node03 ~]#
六、关于StorageClass回收策略对数据的影响
1.第一种配置
archiveOnDelete: "false"
reclaimPolicy: Delete #默认没有配置,默认值为Delete
测试结果:
1.pod删除重建后数据依然存在,旧pod名称及数据依然保留给新pod使用
2.sc删除重建后数据依然存在,旧pod名称及数据依然保留给新pod使用
3.删除PVC后,PV被删除且NFS Server对应数据被删除
2.第二种配置
archiveOnDelete: "false"
reclaimPolicy: Retain
测试结果:
1.pod删除重建后数据依然存在,旧pod名称及数据依然保留给新pod使用
2.sc删除重建后数据依然存在,旧pod名称及数据依然保留给新pod使用
3.删除PVC后,PV不会别删除,且状态由Bound变为Released,NFS Server对应数据被保留
4.重建sc后,新建PVC会绑定新的pv,旧数据可以通过拷贝到新的PV中
3.第三种配置
archiveOnDelete: "ture"
reclaimPolicy: Retain
结果:
1.pod删除重建后数据依然存在,旧pod名称及数据依然保留给新pod使用
2.sc删除重建后数据依然存在,旧pod名称及数据依然保留给新pod使用
3.删除PVC后,PV不会别删除,且状态由Bound变为Released,NFS Server对应数据被保留
4.重建sc后,新建PVC会绑定新的pv,旧数据可以通过拷贝到新的PV中
4.第四种配置
archiveOnDelete: "ture"
reclaimPolicy: Delete
结果:
1.pod删除重建后数据依然存在,旧pod名称及数据依然保留给新pod使用
2.sc删除重建后数据依然存在,旧pod名称及数据依然保留给新pod使用
3.删除PVC后,PV不会别删除,且状态由Bound变为Released,NFS Server对应数据被保留
4.重建sc后,新建PVC会绑定新的pv,旧数据可以通过拷贝到新的PV中
总结:除以第一种配置外,其他三种配置在PV/PVC被删除后数据依然保留
七、常见问题
1.如何设置默认的StorageClass
我们可以用 kubectl patch 命令来更新
[root@master01 ~]# kubectl get sc #查看当前sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
managed-nfs-storage egon-nfs-storage Delete Immediate false 47m
[root@master01 ~]# kubectl patch storageclass managed-nfs-storage -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' #设置managed-nfs-storage为默认后端存储
storageclass.storage.k8s.io/managed-nfs-storage patched
[root@master01 ~]# kubectl get sc #再次查看,注意是否有default标识
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
managed-nfs-storage (default) egon-nfs-storage Delete Immediate false 47m
[root@master01 ~]# kubectl patch storageclass managed-nfs-storage -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}' #取消默认存储后端
storageclass.storage.k8s.io/managed-nfs-storage patched
[root@master01 ~]# kubectl get sc #再次查看,注意是否有default标识
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
managed-nfs-storage egon-nfs-storage Delete Immediate false 48m
YAML文件
[root@master01 ~]# cat nfs-StorageClass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: managed-nfs-storage
annotations:
"storageclass.kubernetes.io/is-default-class": "true" #添加此注释
provisioner: egon-nfs-storage #or choose another name, must match deployment's env PROVISIONER_NAME'
parameters:
archiveOnDelete: "false"
2.如何使用默认的StorageClass
如果集群有一个默认的StorageClass能够满足我们的需求,那么剩下所有需要做的就是创建PersistentVolumeClaim(PVC),剩下的都有默认的动态配置搞定,包括无需去指定storageClassName:
[root@master01 ~]# cat test-claim.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: test-claim-xxx
# 注释掉这两行即可
#annotations:
# volume.beta.kubernetes.io/storage-class: "managed-nfs-storage"
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Mi # 指定需要的空间大小
3.修改默回收策略(默认为Delete)
[root@master01 ~]# cat nfs-StorageClass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: managed-nfs-storage
annotations:
provisioner: egon-nfs-storage #or choose another name, must match deployment's env PROVISIONER_NAME'
parameters:
archiveOnDelete: "true" #暂时不清楚该值对回收策略产生什么影响
reclaimPolicy: Retain #只有NFS 和hostPth支持两种回收策略
4.能过删除/关闭默认的StorageClass
不能删除默认的StorageClass,因为它是作为集群的add-on安装的,如果它被删除,会被重新安装。 当然,可以停掉默认的StorageClass行为,通过删除annotation:storageclass.beta.kubernetes.io/is-default-class,或者设置为false。 如果没有StorageClass对象标记默认的annotation,那么PersistentVolumeClaim对象(在不指定StorageClass情况下)将不自动触发动态配置。相反,它们将回到绑定可用的PersistentVolume(PV)的状态。
5.当删除PersistentVolumeClaim(PVC)会发生什么
如果一个卷是动态配置的卷,则默认的回收策略为“删除”。这意味着,在默认的情况下,当PVC被删除时,基础的PV和对应的存储也会被删除。如果需要保留存储在卷上的数据,则必须在PV被设置之后将回收策略从delete更改为retain。
实战
客户提供NAS作为共享存储,使用NAS挂载到 NFS 做为pv使用,结果在部署Harbor的时候,pvc动态绑定pv成功了,但是无法写入NAS,报权限问题,错误如下:
下边是 pv挂载,但是在部署Harbor时报挂载目录的权限问题,如下,请根据pv挂载及错误,找出问题:
[root@kube-master-01 ~]# kubectl logs -f harbor-hub-database-0 -n harbor
init DB, DB version:14
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.
The database cluster will be initialized with locales
COLLATE: en_US.UTF-8
CTYPE: en_US.UTF-8
MESSAGES: C
MONETARY: C
NUMERIC: C
TIME: C
The default text search configuration will be set to "english".
Data page checksums are disabled.
creating directory /var/lib/postgresql/data/pgdata/pg14 ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 20
selecting default shared_buffers ... 400kB
selecting default time zone ... UTC
creating configuration files ... ok
2024-06-11 10:16:28.754 UTC [56] FATAL: data directory "/var/lib/postgresql/data/pgdata/pg14" has wrong
kind: PersistentVolume
apiVersion: v1
spec:
capacity:
storage: 5Gi
nfs:
server: 10.4.147.230
path: >-
/vol/mds_034513/harbor-database-data-harbor-hub-database-0-pvc-dc669652-8930-4a02-92dc-df7886707dd2
accessModes:
- ReadWriteOnce
claimRef:
kind: PersistentVolumeClaim
namespace: harbor
name: database-data-harbor-hub-database-0
uid: dc669652-8930-4a02-92dc-df7886707dd2
apiVersion: v1
resourceVersion: '2195533'
persistentVolumeReclaimPolicy: Delete
storageClassName: standard
mountOptions:
- hard
- vers=4
- resvport
volumeMode: Filesystem
status:
phase: Bound
经过最终的问题排查,发现是由于nas 使用的是版本三,而系统pvc 的 storageClass
standard 默认版本为 vers=4
,修改为 vers=3
之后,解决了NAS挂载无法写入的问题;
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: standard
uid: 8e539d26-985e-45be-bc8d-94becd36ae1a
resourceVersion: '2244607'
creationTimestamp: '2024-06-03T14:05:01Z'
labels:
app: nfs-subdir-external-provisioner
app.kubernetes.io/managed-by: Helm
chart: nfs-subdir-external-provisioner-4.0.18
heritage: Helm
release: nfs-subdir-external
annotations:
meta.helm.sh/release-name: nfs-subdir-external
meta.helm.sh/release-namespace: kube-system
storageclass.kubernetes.io/is-default-class: 'true'
managedFields:
- manager: helm-dashboard
operation: Update
apiVersion: storage.k8s.io/v1
time: '2024-06-03T14:05:01Z'
fieldsType: FieldsV1
- manager: dashboard
operation: Update
apiVersion: storage.k8s.io/v1
time: '2024-06-12T12:57:57Z'
fieldsType: FieldsV1
fieldsV1:
f:mountOptions: {}
provisioner: cluster.local/nfs-subdir-external-nfs-subdir-external-provisioner
parameters:
archiveOnDelete: 'true'
reclaimPolicy: Delete
mountOptions:
- hard
- vers=4
- resvport
allowVolumeExpansion: true
volumeBindingMode: Immediate
修改之后:
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: standard
... (省略)
parameters:
archiveOnDelete: 'true'
reclaimPolicy: Delete
mountOptions:
- hard
- vers=3 # 修改版本号为3
- resvport
allowVolumeExpansion: true
volumeBindingMode: Immediate
修改之后可以正常部署起来了,这个问题排查了好几天,终于解决了 ^_^
相关文章:
K8S 存储卷-PV、PVC、NFS 和 StorageClass 深度讲解
为者常成,行者常至
自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)