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对象.

file

我们可以创建不同种类StorageClass,如图:金存储类、银存储类、铜存储类等,此时用户便无需关系存储后端了
file

StorageClass对象会定义下面两部分内容:
1,PV的属性.比如,存储类型,Volume的大小等.
2,创建这种PV需要用到的存储插件
有了这两个信息之后,Kubernetes就能够根据用户提交的PVC,找到一个对应的StorageClass,之后Kubernetes就会调用该StorageClass声明的存储插件,进而创建出需要的PV.
但是其实使用起来是一件很简单的事情,你只需要根据自己的需求,编写YAML文件即可,然后使用kubectl create命令执行即可

完整图解如下:
file

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.原理及部署流程说明

详细的运作流程可以参考下图:
file

搭建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 深度讲解

为者常成,行者常至