K8S当中持久卷(PV)、持久卷声明(PVC)与StorageClass

1.PV和PVC 什么是PV和PVC? PV(PersistentVolume,持久卷),定义一个可以使用的数据卷,供K8S中的各个Pod使用,可以根据网络文件系统、云存储、Ceph存储等去作为持久卷的来源,比如我可以接入阿里云存储并封装成为PV,也可以接入NFS作为持久卷封装成为PV。 PVC(P

1.PV和PVC

什么是PV和PVC?

  • PV(PersistentVolume,持久卷),定义一个可以使用的数据卷,供K8S中的各个Pod使用,可以根据网络文件系统、云存储、Ceph存储等去作为持久卷的来源,比如我可以接入阿里云存储并封装成为PV,也可以接入NFS作为持久卷封装成为PV
  • PVC(PersistentVolumeClaim,持久卷声明),K8S集群根据现有的PV情况尝试去申请PV,会从众多PV当中选择出来一个合适的PV进行绑定。

为什么要有PV存在?因为容器本身是短暂的,一旦容器被删除,容器中的数据也会丢失。PV 提供了持久化存储,使得数据能够在容器删除、重启或迁移时得以保存。这对于大多数应用(如数据库、日志存储等)来说至关重要,因为它们需要持久的存储来存储数据。

PVC持久卷申请提交后,会从后端剩余的所有的可绑定的PV当中,根据预期的条件,选出来一个合适的PV进行最终的绑定。PVC和PV进行绑定的过程中,分为"预选"和"优选"两个阶段。

  • 预选主要匹配下面几项:匹配容量,访问模式(RWO/RWX/ROX)和StorageClass。

    • 容量匹配:选择PV预期容量>=PV容量条件满足的PV。比如业务方预期要10G的存储空间,现在有三个PV分别是5G,15G,30G,那么15G和30G满足我们的要求,5G的不满足我们的要求淘汰。
    • 访问模式匹配:要求PVC的访问模式必须和PV的访问模式完全一致。RWO意味着PV只能挂载给一个Pod进行读写,RWX代表PV可以挂载给多个Pod进行读写,ROX则代表PV可以挂载给多个节点进行读取,不能进行写入。
      • 比如业务方希望要一个RWX的PV,但是你给我一个RWO的PV,当然是不满足业务方的要求的。
    • StorageClass匹配:不同的StorageClass意味着底层可能是完全不同的存储介质,比如其中一个StorageClass是SSD,另外一个StorageClass是HDD,两者的特性是完全不一样的。如果不对StorageClass进行精确匹配则会出现预期的存储介质不符合业务方的要求,因此是必须要求PVC和PV对应的StorageClass完全一致的
  • 优选:在预选阶段根据PVC的要求已经筛选出来所有满足条件的PV,但是这个时候可能存在有多个PV都可以用,比如我希望要10G,现在有一个10G的PV、一个15G的PV,还有一个30G的PV,此时PVC应该尝试去选取一个最优的PV尽量减少存储资源的浪费的情况

1.1 创建持久卷

需要注意的是:持久卷是K8S集群维度的,不是namespace维度的,因此创建时不需要指定namespace

我们可以基于如下的K8S资源清单去创建一个持久卷,使用NFS的方式去作为持久卷。

# pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
spec:
  capacity:
    storage: 5Gi  # 根据需要调整存储大小
  accessModes:
    - ReadWriteMany  # NFS 通常使用 ReadWriteMany 访问模式
  nfs:
    path: /sharedata/nfs  # NFS 服务器上共享的目录路径
    server: ...   # NFS 服务器的 IP 地址

1.2 创建持久卷声明PVC

需要注意的是:PVC是namespace级别的,因此在声明时,需要明确指定namespace

我们使用如下的K8S资源清单去创建PVC,在创建时,PVC会自动去匹配所有的PV持久卷,去匹配到合适的PV完成绑定。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
  namespace: wanna-project
spec:
  accessModes:
    - ReadWriteMany  # 确保与 PV 的访问模式匹配
  resources:
    requests:
      storage: 5Gi  # 请求的存储大小

2.StorageClass

StorageClass(存储类),是一类存储的抽象,比如阿里云存储,AWS云存储,NFS,都可以作为一类StorageClass。

2.1 为什么要有StorageClass

想象以下场景:

  • (1)我们想要1.5G的持久卷,但是目前没有能完全匹配1.2G的持久卷,只有1G/2G/5G/10G/...的容量的持久卷,如果我们想要使用PVC去申请持久卷时,就必须申请一个2G的持久卷,就会造成0.5GB的存储空间浪费
  • (2)我们每次想要去使用持久卷时,都得先创建PV,再去创建PVC。公司的PV一般是由公司的存储相关的运维工程师维护的,PVC则一般是后端的开发过程师去进行申请的,此时就会涉及到大量的跨部门的无效沟通,运维工程师还得去找到存储空间并封装成为PV给业务方使用,涉及到众多繁琐流程

现在的云存储厂商(比如AWS,Google)普遍都支持动态申请空间/释放空间,调用存储厂商一个接口可以完成持久卷的自动创建,K8S支持将云存储厂商封装成为一个StorageClass(存储类),我们在申请存储空间时,只需要创建后端开发工程师去申请PVC,借助StorageClass就可以云存储厂商就能自动帮我们创建好持久卷,K8S则通过StorageClass去封装成为一个PV,并完成PV和PVC的自动绑定。比如我们想要1.5G的空间,那么只需要创建一个PVC,指定StorageClass,云厂商就可以自动为我们分配得到一个1.5GB的持久卷并挂载给Pod进行使用。StorageClass,可以理解成为一个动态的持久卷,支持根据PVC去动态创建PV。

使用StorageClass有以下的好处:

  • 1.支持动态分配PV持久卷,提高资源使用率,也提高PV创建的效率。
  • 2.支持多种后端存储(AWS、NFS、Ceph等)。
  • 3.支持为不同的应用使用不同的存储配置,比如高性能IO系统需要使用SSD,对IO没有要求的系统可以使用HDD。

下面我们演示一下,基于开源项目nfs-subdir-external-provisioner,支持去将NFS服务去转换成为持久卷,通过一个目录下划分多个子目录去实现逻辑上的持久卷的划分。

2.2 基于NFS封装成为StorageClass

需要注意的是:NFS作为StorageClass时不支持多机部署!!!只能部署单个节点!!!多机部署会出现数据的不一致性问题!!!

(1) 创建namespace存放相关的资源

# nfs-provisioner-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: nfs-provisioner

(2) 创建RBAC权限配置

创建可以访问集群资源的账号并配置相关的权限信息。

# nfs-provisioner-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-provisioner
  namespace: nfs-provisioner
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-provisioner-runner
rules:
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "update", "patch"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-provisioner
  namespace: nfs-provisioner
rules:
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-provisioner
  namespace: nfs-provisioner
roleRef:
  kind: Role
  name: leader-locking-nfs-provisioner
  apiGroup: rbac.authorization.k8s.io
subjects:
  - kind: ServiceAccount
    name: nfs-provisioner
    namespace: nfs-provisioner
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-provisioner
roleRef:
  kind: ClusterRole
  name: nfs-provisioner-runner
  apiGroup: rbac.authorization.k8s.io
subjects:
  - kind: ServiceAccount
    name: nfs-provisioner
    namespace: nfs-provisioner

(3) 创建Deployment部署nfs-provisioner

创建Deployment,并指定已经申请RBAC权限的账号serviceAccountName: nfs-provisioner

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-subdir-external-provisioner
  namespace: nfs-provisioner
  labels:
    app: nfs-provisioner
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nfs-provisioner
  template:
    metadata:
      labels:
        app: nfs-provisioner
    spec:
      serviceAccountName: nfs-provisioner
      containers:
      - name: nfs-provisioner
        image: kubesphere/nfs-subdir-external-provisioner:v4.0.2
        env:
        - name: PROVISIONER_NAME
          value: nfs-provisioner
        - name: NFS_SERVER
          value: <nfs-server-ip>  # 替换为 NFS 服务器的 IP 地址
        - name: NFS_PATH
          value: /sharedata/nfs # 替换为 NFS 服务器共享的根目录
        securityContext:
          capabilities:
            add:
            - DAC_READ_SEARCH
        volumeMounts:
        - name: nfs-client-root
          mountPath: /persistentvolumes
      volumes:
      - name: nfs-client-root
        nfs:
          server: <nfs-server-ip> # 替换为 NFS 服务器的 IP 地址
          path: /sharedata/nfs # 替换为 NFS 服务器共享的根目录

这里的镜像image可以使用下面的镜像,pull到本地并上传到远程服务器使用。

docker pull kubesphere/nfs-subdir-external-provisioner:v4.0.2

(4) 创建StorageClass

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-storage
provisioner: nfs-provisioner
parameters:
  pathPattern: ${.PVC.namespace}/${.PVC.name}
  archiveOnDelete: "false"

创建名叫nfs-storage的StorageClass,并指定provisionernfs-provisioner。通过pathPattern指定该StorageClass在创建PV时需要怎么去生成路径,我们通过${.PVC.namespace}/${.PVC.name}指定将创建的PV的路径使用PVC所在的namespace和PVC的name作为子路径,比如wanna-project下的test-pvc将会被放在NFS服务器的wanna-project/test-pvc目录下(算上相对路径的话,就是/sharedata/nfs/wanna-project/test-pvc)。

需要注意的是,这里的provisioner需要和Deployment当中指定的PROVISIONER_NAME保持一致。

        - name: PROVISIONER_NAME
          value: nfs-provisioner

(5) 执行K8S资源清单创建资源

通过执行下面的命令,去指定资源清单,部署nfs-provisioner服务,并声明StorageClass。

kubectl apply -f nfs-provisioner-namespace.yaml  -f nfs-provisioner-rbac.yaml  -f nfs-provisioner-deployment.yaml  -f nfs-storageclass.yaml

接着,我们可以通过如下的资源清单nfs-test-pvc.yaml创建PVC(命令kubectl apply -f nfs-test-pvc.yaml),通过刚刚我们创建出来的StorageClass去申请PV持久卷。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
  namespace: wanna-project
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
  storageClassName: nfs-storage

我们使用kubectl get pvc test-pvc -n wanna-project如下的命令去查看,PVC的状态,可以发现,PVC已经绑定PV成功,绑定的PV名称为pvc-f9a35955-1a44-4c74-b626-f24ec1285b34,是StorageClass默认生成的名称。

NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
test-pvc   Bound    pvc-f9a35955-1a44-4c74-b626-f24ec1285b34   1Gi        RWX            nfs-storage    1h
Comment