📣 大家好,我是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-container
和 reader-container
:
writer-container
容器使用了writer-image
镜像,并通过一个无限循环来写入数据到/shared-data/data.txt
文件中,每隔 5 秒写入一次。reader-container
容器使用了reader-image
镜像,并通过tail -f
命令来实时读取/shared-data/data.txt
文件的内容。- 两个容器都挂载了一个名为
shared-data
的 EmptyDir 卷,它们可以通过共享的/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 必须事先知道以下信息:
- 当前
Volume
的类型并明确Volume
已经创建好了- 必须知道 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 提供了一种抽象层,使应用程序能够独立于底层存储技术进行操作,并提供了一些额外的好处,而这些好处在原生的存储技术下并不容易实现:
- 动态分配和管理:PV 和 PVC 支持动态分配和管理存储资源。在使用 PV 和 PVC 的情况下,Kubernetes 可以根据 PVC 的需求动态创建和分配存储卷。这消除了手动管理存储卷的需要,提高了自动化和可伸缩性。
- 声明式配置和生命周期管理: PV 和 PVC 允许以声明式的方式定义和管理存储配置和生命周期。通过定义 PVC,可以明确指定应用程序对存储的要求,包括容量、访问模式和存储类别等。这简化了存储配置的过程,并使存储的生命周期管理更加方便。
- 抽象化和灵活性: 使用 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 的示例:
- 创建一个名为
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 Provisioner
的 Deployment、ClusterRole 和 ClusterRoleBinding
。Provisioner 使用了 nfs.openebs.io/nfs-client
作为其名称。
- 创建一个名为
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 的销毁而销毁“这个问题:
- 我们首先引入了数据卷这个概念,使用数据卷挂载在宿主机上
- 一般而言,我们使用的是 Kubernetes 集群,如果使用数据卷挂载在宿主机上的话,当 Pod 被重新调度到其他节点的时候,数据也会无法找到
- 因此又引入了
NFS
网络文件存储系统,配合StatefulSet
可以解决以上问题 - 为了解开 运维人员 和 开发人员 的职责耦合,也为了降低开发的难度(直接使用存储系统),可以使用
PV 和 PVC
这一抽象层,让数据卷的使用更加的灵活,可扩展性更高 - 静态使用
PV 和 PVC
需要手动创建和管理PV
,而动态供给可以实现自动创建和管理,达到 简化存储资源的管理过程 的作用
🍁 友链
- 【ChatGPT】
- 【编程不良人】Kubernetes(k8s)实战教程
- 【Kubernetes文档】| Kubernetes 官方文档
- 【云原生 • Kubernetes】(一) 初识 Kubernetes
- 【云原生 • Kubernetes】(二) Kubernetes 的组件与架构
- 【云原生 • Kubernetes】(三) k8s 如何运行 Container?
- 【云原生 • Kubernetes】(四) k8s 原来不是直接创建 Pod!!
- 【云原生 • Kubernetes】(五)学了这么久的 Kubernetes 终于可以访问 Pod 了!!
- 【云原生 • Kubernetes】专栏 - 欢迎大家订阅
✒ 写在最后
都看到这里啦~,给个点赞再走呗~,也欢迎各位大佬指正以及补充,在评论区一起交流,共同进步!也欢迎加微信一起交流:Goldfish7710。咱们明天见~