【云原生 • Kubernetes】(六)Kubernetes “卷” 在哪 ?

2,869 阅读14分钟

📣 大家好,我是Zhan,一名个人练习时长一年半的大二后台练习生🏀

📣 这篇文章是学习 Kubernetes第六篇 学习笔记📙

📣 如果有不对的地方,欢迎各位指正🙏🏼

📣 与君同舟渡,达岸各自归🌊


🔔 引语

在学习 Docker 的时候我们就了解到了数据卷的挂载,Container 中的文件在磁盘上是临时存放的,如果不存入宿主机的硬盘中,就很有可能导致容器崩溃时导致文件丢失,还有一个问题就是:同一 Pod 中运行多个容器并共享文件时会出差错,而 Kubernetes Volume 这一抽象概念可以解决这两个问题。本文将从以下几个方面来介绍数据卷:

  • 常用卷有哪些类型?
  • 如何去使用数据卷?
  • 对比 Persistent Volume 和 Persistent Volume Claim
  • 静态供给和动态供给分别是什么意思?

1️⃣ 卷的类型

数据卷是一个概念上的抽象,我们可以把数据存在里面,同时也可以从数据卷重读取数据,但是在计算机的角度,存储数据肯定是要有一个实现的方式,那么卷有哪些类型的实现呢?

🎈 1.1 根据数据卷的生命周期

Kubernetes 支持很多类型的卷,Pod 可以同时使用任意数目的卷类型,而卷可以分为:临时卷类型持久卷类型

  • 临时卷类型 :生命周期与 Pod 相同,当 Pod 不再存在的时候,Kubernetes 会销毁与之对应的临时卷
  • 持久卷类型 :比 Pod 的存活期场,即使 Pod 不再存在也不会删除数据卷
  • 注意:无论是何种类型的数据卷,当 Pod 重启,数据卷都不会丢失

📲 1.2 根据存储的介质

卷的核心是一个目录,其中可能存有数据,Pod 中的容器可以访问该目录中的数据,采用不同卷的类型,它们目录的形成是不同的,也就是说用于保存数据以及目录中存放内容的介质是不同的,常用的卷类型有 ConfigMap、EmptyDir、Local、NFS、Secret 等:

  • ConfigMap可以把配置文件以键值对的形式存在其中,然后在 Pod 中以 文件 或者 环境变量 的形式进行使用。
    • 通过使用 ConfigMap,你可以将应用程序与配置数据解耦,提高了灵活性和可维护性,同时也方便了在容器化环境中进行配置管理。
  • EmptyDir : 是一个空目录,可以在 Pod 中用来存储临时数据,当 Pod 被删除时,该目录也会被删除
    • 某些应用程序需要在容器内使用缓存数据来提高性能。EmptyDir 可以用作缓存存储卷,容器可以将数据写入 EmptyDir,供其他容器或同一容器中的其他进程读取。
  • Local :将本地文件系统的目录或者文件映射到 Pod 的一个 Volume 中,可以用来在 Pod 中共享文件或者数据
    • 在之前的文章中,在讲解 StatefulSet 这个控制器的时候,我们有讲到过,该数据卷只能保存在 Pod 所在结点上,而如果 Pod 重新调度到其他的节点,就无法获取之前的数据,因此有了下面的 NFS
  • NFS :将网络上的一个或者多个 NFS 共享目录挂载到 Pod 中的 Volume 中,可以用来在多个 Pod 之间共享数据
    • 通过使用 NFS 我们可以做到:共享存储、数据持久化、数据备份和恢复、跨集群数据共享、存储静态文件
  • Secret :将敏感信息以密文的形式保存在 Secret 中,并且可以在 Pod 中以文件或者环境变量的形式使用

2️⃣ 使用方式

使用卷时,在 .spec.volumes 字段中为 Pod 设置数据卷,并在 .spec.containers[*].volumeMounts 字段中声明卷在容器中的挂载位置:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
    - name: my-app-container
      image: my-app-image
      volumeMounts:
        - name: config-volume
          mountPath: /path/to/config-file.conf
          subPath: config-file.conf
  volumes:
    - name: config-volume
      configMap:
        name: my-config

可以看到是:通过 config-volume 这个 volumeName 进行配置,在 Container 中进行通过名字进行数据卷的挂载

⚙️ 2.1 EmptyDir 临时共享空间

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: writer-container
          image: writer-image
          volumeMounts:
            - name: shared-data
              mountPath: /shared-data
          command: ["sh", "-c"]
          args:
            - while true; do echo $(date) >> /shared-data/data.txt; sleep 5; done
        - name: reader-container
          image: reader-image
          volumeMounts:
            - name: shared-data
              mountPath: /shared-data
          command: ["sh", "-c"]
          args:
            - tail -f /shared-data/data.txt
      volumes:
        - name: shared-data
          emptyDir: {}

模板中定义了两个容器 writer-containerreader-container:

  • writer-container 容器使用了 writer-image 镜像,并通过一个无限循环来写入数据到 /shared-data/data.txt 文件中,每隔 5 秒写入一次。
  • reader-container 容器使用了 reader-image 镜像,并通过 tail -f 命令来实时读取 /shared-data/data.txt 文件的内容。
  • 两个容器都挂载了一个名为 shared-dataEmptyDir 卷,它们可以通过共享/shared-data 路径进行数据交互
  • 这里可以看出这个共享空间对于我们是不可见的,或者说是不知道这个共享空间具体路径,只能操作这个共享空间 总结:EmptyDir 是 Host 上创建的临时目录,优点是能够方便地为 Pod 中的容器提供共享存储,不需要额外的配置。它并不具备持久性,会随着 Pod 的销毁而删除,因此它比较适合 Pod 中的容器需要临时共享存储空间的场景,例如上述的 生产者消费者 模式

🖥️ 2.2 HostPath 本机挂载

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app-container
          image: my-app-image
          volumeMounts:
            - name: hostpath-volume
              mountPath: /path/on/pod
      volumes:
        - name: hostpath-volume
          hostPath:
            path: /path/on/host

在上述配置文件中,我们创建了一个 Deployment,其中有一个名为 my-app-container 的容器。我们使用了 my-app-image 镜像,并将主机上的 /path/on/host 目录挂载到容器中的 /path/on/pod 路径上。

总结:如果 Pod 被销毁了,HostPath 对应的目录还是会被保留,从这一点来看,HostPath 的持久性比 EmptyDir 强,不过一旦 Host 崩溃,HostPath 也就无法访问了,并且这种方式也带来了另一个问题:增加了 Pod 与 节点的耦合

📡 2.3 NFS 网络文件存储系统

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app-container
          image: my-app-image
          volumeMounts:
            - name: nfs-volume
              mountPath: /data
      volumes:
        - name: nfs-volume
          nfs:
            server: nfs-server-ip
            path: /path/to/nfs/share

在上述配置文件中,我们创建了一个 Deployment,其中包含一个容器 my-app-container,使用了 my-app-image 镜像。我们定义了一个名为 nfs-volume 的卷,并将其挂载到容器的 /data 路径上。

volumes 部分,我们使用了 nfs 卷类型,并指定了 NFS 服务器的 IP 地址和共享路径。这将使容器能够将数据存储到 NFS 服务器上的指定路径中。上文我们有写如何进行搭建一个本机的 NFS: 【云原生 • Kubernetes】(五)学了这么久的 Kubernetes 终于可以访问 Pod 了!!

总结:相对于 EmptyDir 和 HostPath,这种 Volume 类型的最大特点就是不依赖 Kuberess Volume 的底层基础设施而独立的存储系统管理,与 Kubernetes 集群是分离的。数据持久化后,即使是整个 Kubernetes 崩溃也不会受损。当然,运维这样的系统也不是一项简单的工作!


3️⃣ PV & PVC

Volume 提供了非常好的数据持久化方案,不过在可管理性上还有不足,以 NFS 为例子,要使用 Volume,Pod 必须事先知道以下信息:

  1. 当前Volume的类型并明确Volume已经创建好了
  2. 必须知道 Volume 的具体地址信息 但是Pod开发人员,和Volume运维人员通常不是一个人,也就是说要么开发人员询问管理员,要么开发人员自己当管理员,也就是出现了职责的耦合

📂 3.1 什么是 PV 和 PVC

Kubernetes 给出的解决方案是 Persistent Volume 和 Persistent Volume Claim

  • Persistent Volume 是外部存储系统的一块存储空间,由运维人员创建和维护,与 Volume 一样,PV 具有持久性,生命周期独立于 Pod
  • Persistent Volume Claim 是对 PV 的申请。PVC 通常由普通用户创建和维护。需要为 Pod 分配存储资源的时候,需要带上 存储资源的容量大小和访问方式等信息 进行申请,Kubernetes 会查找并提供满足条件的 PV。

也就是说 PV 和 PVC 提供了一种抽象层,使应用程序能够独立于底层存储技术进行操作,并提供了一些额外的好处,而这些好处在原生的存储技术下并不容易实现:

  1. 动态分配和管理:PV 和 PVC 支持动态分配和管理存储资源。在使用 PV 和 PVC 的情况下,Kubernetes 可以根据 PVC 的需求动态创建和分配存储卷。这消除了手动管理存储卷的需要,提高了自动化和可伸缩性
  2. 声明式配置和生命周期管理: PV 和 PVC 允许以声明式的方式定义和管理存储配置和生命周期。通过定义 PVC,可以明确指定应用程序对存储的要求,包括容量、访问模式和存储类别等。这简化了存储配置的过程,并使存储的生命周期管理更加方便。
  3. 抽象化和灵活性: 使用 PV 和 PVC 可以将底层存储技术(如 NFS)与应用程序解耦。PV 和 PVC 提供了一种抽象化的方式来描述存储需求和存储资源,并使应用程序能够以一种统一的方式进行存储操作,而不用关心底层实现细节。

学到这里,不禁感叹:经典白学!!! 抱着尝试的心态,去问了一下GPT:

总的来说就是:PV 和 PVC 集成了控制器等其他资源,对存储系统做了抽象和封装,方便了我们的管理和使用。其中用户只需要递交 Claim 申请,而 PV 的底层维护交给管理员来处理,即 PV 创建的细节信息是由 管理员 关心

🛠️ 3.2 基本使用

🟥 创建 Persistent Volume

apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-pv
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    server: nfs-server-ip
    path: /path/to/nfs/share

在上述配置文件中,我们创建了一个 PV(PersistentVolume)对象,并使用了 nfs 类型的存储卷:

  • 我们给 PV 指定了一个名称为 my-pv容量为 5Gi
  • 访问模式ReadWriteOnce,表示只能被 单个 Pod读写 模式挂载
    • 如果访问模式为 ReadWriteMany 表示能被 多个 Pod读写 模式挂载
    • 如果访问模式为 ReadOnlyMany 表示能被 多个 Pod只写 的方式挂载
  • nfs 指定了 NFS 服务器的 IP 地址 nfs-server-ip共享路径 /path/to/nfs/share
  • 回收策略Retain,表示在 PVC 删除后,保留 PV 和 数据,手动清理 PV 中的数据
    • 如果回收策略为 Delete 表示在 PVC 被删除后,自动删除 PV 和其数据
    • 如果回收策略为 Recycle 表示在 PVC 被删除后,通过删除 PV 中的数据让 PV 再次可以被使用
  • storageClassName 相当于给 PV 打了一个标签,在进行 Claim 的时候可以根据它来选择

🟩 创建 Persistent Volume Claim

根据需求寻找符合条件的 PV 分配给 PVC,至此 PVC 就可以作为一个“数据卷”进行使用了。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: nfs

在上述配置文件中,我们创建了一个 PVC(PersistentVolumeClaim)对象,其用于请求一个符合需求的 PV:

  • 我们给 PVC 指定了一个名称为 my-pvc,访问模式为 ReadWriteOnce,请求了 1Gi 的存储空间。
  • storageClassName 表示我们要找名字为 nfs 的PV

🟧 使用 Persistent Volume Claim

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app-container
          image: my-app-image
          volumeMounts:
            - name: data-volume
              mountPath: /data
      volumes:
        - name: data-volume
          persistentVolumeClaim:
            claimName: my-pvc

在上述配置文件中,我们创建了一个 Deployment,其中的容器使用了 my-app-image 镜像。我们定义了一个名为 data-volume 的卷,并将其与 PVC my-pvc 关联。

这样,容器中的 /data 路径将会与 PV 关联的 NFS 存储卷进行映射。应用程序可以在容器中读取和写入该路径,数据将持久化存储在 NFS 服务器上。

通过以上步骤,我们创建了一个 PV 和 PVC,并在 Deployment 中使用了该 PVC,PV 使用了 NFS 作为底层存储。这样,应用程序可以使用持久化的存储,并且数据将持久化存储在 NFS 服务器上。

4️⃣ 动态供给

在前面的例子中,我们提前创建了 PV,然后通过 PVC 申请 PV 并在 Pod 中使用,这种方式叫做 静态供给,与之对应的是 动态供给,那如果没有满足 PVC 的 PV,静态供给的 Pod 会一直处于 Pending 状态,而 动态供给动态创建 PV,而不是提前创建 ,减少了管理员的工作量、效率高。

动态供给是通过 StorageClass 实现的,StorageClass 定义了如何创建 PV,但需要注意的是每个 StorageClass 都有一个制备器 provisioner,用来决定使用哪个卷插件来制备 PV。

我们可以这么理解:

  • StorageClass 是一个类,而 PV 是 StorageClass 的一个实体对象,也就是:StorageClass pv = new StorageClass();
  • 而 StorageClass 只是一个 模版,如何进行实例化需要一个“类加载器”,也就是 Provisioner,因为我们使用的存储服务不同,可能是亚马逊的,可能是 NFS,下面我们会做 NFS 的示例:
  1. 创建一个名为 nfs-client-provisioner.yaml 的 Provisioner 配置文件,并添加以下内容:
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
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: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "delete"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["nfs.openebs.io"]
    resources: ["nfses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["nfs.openebs.io"]
    resources: ["nfsservers"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["get", "list", "watch", "create", "update", "delete"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "create", "update", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: run-nfs-client-provisioner
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: nfs-client-provisioner-runner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
spec:
  selector:
    matchLabels:
      app: nfs-client-provisioner
  replicas: 1
  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
          env:
            - name: PROVISIONER_NAME
              value: nfs.openebs.io/nfs-client

在上述配置文件中,我们创建了一个NFS Client ProvisionerDeployment、ClusterRole 和 ClusterRoleBinding 。Provisioner 使用了 nfs.openebs.io/nfs-client 作为其名称。

  1. 创建一个名为 nfs-storage-class.yaml 的 StorageClass 配置文件,并添加以下内容:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-storage-class
provisioner: nfs.openebs.io/nfs-client

在上述配置文件中,我们创建了一个 StorageClass,并指定 Provisioner 的名称为 nfs.openebs.io/nfs-client。这样,我们就创建了 NFS Client Provisioner 和对应的 StorageClass。

接下来,你可以创建一个 PVC,并将其与上述的 StorageClass 关联,以实现动态供给:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: nfs-storage-class

NFS Client Provisioner 将会自动创建一个 PV,并将其与 PVC 关联起来,从而实现了动态供给的 PVC。


💬 总结

为了解决”数据会随着 Pod 的销毁而销毁“这个问题:

  1. 我们首先引入了数据卷这个概念,使用数据卷挂载在宿主机上
  2. 一般而言,我们使用的是 Kubernetes 集群,如果使用数据卷挂载在宿主机上的话,当 Pod 被重新调度到其他节点的时候,数据也会无法找到
  3. 因此又引入了 NFS 网络文件存储系统,配合 StatefulSet 可以解决以上问题
  4. 为了解开 运维人员 和 开发人员 的职责耦合,也为了降低开发的难度(直接使用存储系统),可以使用 PV 和 PVC 这一抽象层,让数据卷的使用更加的灵活,可扩展性更高
  5. 静态使用 PV 和 PVC 需要手动创建和管理 PV,而动态供给可以实现自动创建和管理,达到 简化存储资源的管理过程 的作用

🍁 友链


✒ 写在最后

都看到这里啦~,给个点赞再走呗~,也欢迎各位大佬指正以及补充,在评论区一起交流,共同进步!也欢迎加微信一起交流:Goldfish7710。咱们明天见~