11 存储:通过数据卷持久化存储文件
你好,我是雪飞。
网络访问的问题解决了,我们再来看一下存储吧。应用中处理数据存储主要有两个场景:一是将数据保存到数据库中,通过代码连接到数据库服务,然后使用数据库 SQL 语句读写数据,这种方式不需要 K8s 集群提供存储;二是文件存储,由于 Pod 中的容器每次重启后,都会重新生成容器内的文件系统,所以应用在容器中保存的文件都无法在下次重启后保留。但是应用又需要把某些文件(如日志、用户上传文件等)长期保存,以便下次重启容器后还能再次访问使用,这就是文件的持久化存储问题,这种场景需要 K8s 集群来提供存储方案。
认识数据卷(Volume)
K8s 使用一个抽象的数据卷(Volume)来解决文件的持久化存储问题。在 Pod 中我们先定义了代表某种存储空间的 Volume,然后在 Pod 的容器中,通过把这个 Volume 挂载到容器中的某个目录,就可以建立一个空间映射关系,之后在容器中操作这个目录(例如创建目录、读写文件等),就相当于在该存储空间中进行操作,所以这些操作结果就自然地保留到了该存储空间中,从而实现了持久化存储。
数据卷有多种类型,以下是常用的几种:
- 节点本地存储: 这类存储是在 Pod 所在的节点宿主机本地文件系统上进行文件存储,如 emptyDir 和 hostPath 类型。
- 网络共享存储: 这类存储直接使用共享的网络存储服务,如 NFS 和 Ceph 等类型。
- ConfigMap 和 Secret: 这两种资源对象也可以作为 Volume,以配置文件的形式挂载到 Pod 中使用。之前已经介绍过它们。
- persistentVolume(PV)和 persistentVolumeClaim (PVC): 它们是 K8s 提供的用来解耦实际存储空间和存储需求的资源对象。PV 代表了某些存储空间,而 PVC 代表 Pod 对存储的需求,K8s 会自动根据存储需求 PVC 来选择合适的存储空间 PV 进行绑定,然后 Pod 就可以在容器中挂载 PVC 从而使用 PV 代表的存储空间。这个机制实现了存储空间管理和使用的分离。
这些数据卷为我们应用的持久化存储需求提供了丰富的选择。首先,我来介绍两种最简单的节点本地存储的方式:emptyDir 和 hostPath。
节点本地存储
emptyDir 卷
emptyDir 卷是一种临时存储,它会在 Pod 所在的节点宿主机上创建一个目录,用于 Pod 内部容器的临时数据交换。就像名字意思一样,emptyDir 卷创建时是空的。Pod 中容器都可以读写 emptyDir 卷中的文件。当 Pod 从节点上删除时,emptyDir 卷中的数据也会被永久清空。当然,Pod 中容器崩溃并不会导致 Pod 从节点上移除,因此容器崩溃期间 emptyDir 卷中的数据是安全的。我之前在介绍 Pod 的容器共享存储时就使用了 emptyDir 卷,这里就不再举例。
hostPath 卷
hostPath 卷也是一种节点本地存储,它允许你将 Pod 所在节点宿主机上的文件目录挂载到容器中使用。通过使用 hostPath 卷,容器中应用可以直接操作节点文件系统中的目录和文件,就像它们是容器内部文件系统目录和文件一样。
hostPath 卷通常用于容器中的应用需要访问宿主机上的日志或配置文件。hostPath 卷会存在一些安全风险,因为它可以允许容器中的应用修改宿主机上的敏感文件。此外,使用 hostPath 卷也会导致对宿主机环境的依赖,从而影响应用的可移植性和一致性。因此,如果必须使用 hostPath 卷时,应将其使用范围限制在必要的文件或目录,并且以只读方式挂载,以确保安全性和稳定性。
下面是一个 hostPath 卷的 YAML 文件(my-hostpath.yaml)示例。我们定义了一个 hostPath 卷,并将其挂载到 busybox 容器内的 “/data/tmp” 目录。这样,就可以在容器内操作节点宿主机上对应的 hostPath 目录,包括创建子目录、读写文件等等。
# my-hostpath.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-hostpath-pod
spec:
containers:
- name: busybox
image: busybox
command: ["/bin/sh","-c","sleep 3600"]
volumeMounts:
- name: host-path-volume
mountPath: /data/tmp
# readOnly: true # 如果挂载的是文件,可以使用 readOnly 限制只读文件
volumes:
- name: host-path-volume
hostPath:
path: /tmp
type: Directory # 说明挂载路径的是目录(Directory)还是文件
- volumeMounts:表示容器中挂载的数据卷信息,包含要挂载的数据卷名称(name)和挂载到容器内部文件系统的路径(mountPath),注意这个属性的层级是在容器内部,而且它是列表,可以同时挂载多个数据卷。
- volumes:表示创建数据卷,注意这个属性的层级是和容器相同,而且它是列表,可以同时创建多个不同种类的数据卷。volumes 和 volumeMounts 成对出现,共同完成容器挂载存储空间的过程。
Pod 部署成功后,在 busybox 容器内就可以直接使用 “/data/tmp” 文件目录。我们进入容器内部并创建一个 “aaa.txt” 文件,然后查看一下这个 Pod 被调度到的节点是 k8s-worker1。
[root@k8s-master ~]# kubectl apply -f my-hostpath.yaml
pod/my-hostpath-pod created
# 进入容器 busybox 中查看挂载的文件目录
[root@k8s-master ~]# kubectl exec -it my-hostpath-pod -c busybox -- sh
/ # cd data/tmp
/data/tmp # echo "Hello hostpath!" > aaa.txt
/data/tmp # cat aaa.txt
Hello hostpath!
/data/tmp # exit
[root@k8s-master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
my-hostpath-pod 1/1 Running 0 2m1s 10.244.194.126 k8s-worker1 <none> <none>
这时,我们远程连接到 k8s-worker1 节点上,查看 “/tmp” 目录,就可以看到 “aaa.txt” 文件。这表明了 hostPath 卷成功将宿主机的目录映射到了容器内部。
网络共享存储
节点本地存储的两种数据卷用起来非常简单方便,但是无法解决不同节点的多个 Pod 之间对文件的共享场景。例如,通过 Deployment 部署了多个 Pod 应用,你首次访问 Pod 时上传了一个文件,存储在这个 Pod 所在的 k8s-worker1 节点上,但是过一会你要读文件时,Service 负载均衡把请求发送到 k8s-worker2 节点上的另一个 Pod 副本,这时你肯定无法再读取到之前上传的文件。显然,这并不是我们期望的结果。因此,我们需要一个文件共享存储解决方案。下面我就介绍一下最常用的 NFS 文件共享存储服务。
NFS 卷
NFS(Network File System)是一种网络文件系统,它允许多个 Pod 中的容器访问同一个网络文件系统,所有的文件都存储在网络文件系统服务端的共享目录中,而不是在 Pod 所在节点宿主机上,所以非常适合需要文件共享或持久化存储的场景。
NFS 组成分为服务端和客户端两部分,其中服务端负责提供共享文件目录,客户端负责连接到服务端并使用服务端提供的共享目录。所以网络文件系统实现了多个客户端通过网络传输来共享服务端的文件目录。
接下来,我带你搭建一个 NFS。通常 NFS 服务端应该部署在 K8s 集群外部服务器上。为了节省资源,我们选择集群中的一个节点 k8s-worker2 作为 NFS 服务端,将 k8s-worker1 和 k8s-master 作为 NFS 客户端,以下是搭建过程。
# 1、安装 NFS 服务,服务端和客户端都需要安装。
yum install nfs-utils
# 2、在服务端创建一个共享目录,我这里用目录 "/nfs/k8s/shared"
[root@k8s-worker2 ~]# mkdir -p /nfs/k8s/shared
# 3、编辑修改服务端上的 "/etc/exports" 文件,配置使用这个共享文件目录
[root@k8s-worker2 ~]# vi /etc/exports
/nfs/k8s/shared *(rw,no_root_squash) # 在配置文件中增加一条记录
# /nfs/k8s/shared 是共享文件目录
# * 表示所有 IP 的客户端都可以访问
# rw 表示可以客户端可以读写目录
# no_root_squash 表示保持客户端访问目录的 root 用户权限,方便客户端操作共享目录
# 4、生效配置
[root@k8s-worker2 ~]# exportfs -arv
exporting *:/nfs/k8s/shared
# 5、在服务端上开启 NFS 服务
[root@k8s-worker2 ~]# systemctl start nfs
[root@k8s-worker2 ~]# systemctl enable nfs # 设置为开机启动
Created symlink from /etc/systemd/system/multi-user.target.wants/nfs-server.service to /usr/lib/systemd/system/nfs-server.service.
注意:
- 如果你的 NFS 服务器开了防火墙或使用了云服务商的防火墙,需要开放 2049、111、635、892 端口(TCP/UDP 协议都需要开放),否则会无法访问 NFS 服务器。
- 集群的所有节点都要安装 nfs-utils 工具,以确保无论 Pod 被调度到哪个节点,都可以通过 NFS 工具连接到 NFS 服务并正常使用。
NFS 搭建完成后,接下来我们需要在 K8s 集群中通过 YAML 文件(my-nfs-deployment.yaml)来使用 NFS 卷。
# my-nfs-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nfs-deployment
spec:
selector:
matchLabels:
app: busybox-pod
replicas: 3
template:
metadata:
labels:
app: busybox-pod
spec:
containers:
- name: busybox-c
image: busybox
command: ["/bin/sh","-c","sleep 3600"]
volumeMounts:
- name: nfs-shared-data
mountPath: /tmp/data
volumes:
- name: nfs-shared-data
nfs:
server: k8s-worker2 # NFS 服务器 IP、域名或者主机名
path: /nfs/k8s/shared # 共享目录
在 YAML 文件的 volumes 属性中,我们定义了一个名称为 “nfs-shared-data” 的 NFS 卷,然后把 NFS 共享目录 “/nfs/k8s/shared” 挂载到容器的 “/tmp/data” 目录,从而实现 Deployment 的多个 Pod 副本共享该目录。部署 Deployment,查看部署成功后的三个 Pod 副本。
[root@k8s-master ~]# kubectl apply -f my-nfs-deployment.yaml
deployment.apps/my-nfs-deployment created
[root@k8s-master ~]# kubectl get pod # 查看 Pod
NAME READY STATUS RESTARTS AGE
my-nfs-deployment-6f594bb4c5-fzt7j 1/1 Running 0 67s
my-nfs-deployment-6f594bb4c5-q6dr2 1/1 Running 0 67s
my-nfs-deployment-6f594bb4c5-r9smm 1/1 Running 0 67s
我们进入其中一个 Pod 的容器中操作 “/tmp/data” 目录,创建一个文件,然后进入到其他容器中查看该文件,从结果中可以看出,三个 Pod 的容器确实都可以共享该目录。
[root@k8s-master ~]# kubectl exec -it my-nfs-deployment-6f594bb4c5-fzt7j -c busybox-c -- sh
/ # cd /tmp/data/
/tmp/data # echo "Hello NFS!" > aaa.txt # 写入文件到NFS目录
/tmp/data # exit
[root@k8s-master ~]# kubectl exec -it my-nfs-deployment-6f594bb4c5-q6dr2 -c busybox-c -- sh
/ # cd /tmp/data/
/tmp/data # cat aaa.txt # 读取共享文件
Hello NFS!
即使删除了 Deployment 和 Pod,NFS 服务端共享目录中的文件依旧存在,不会随 Pod 删除而消失。
[root@k8s-master ~]# kubectl delete -f my-nfs-deployment.yaml
deployment.apps "my-nfs-deployment" deleted
查看 k8s-worker2 服务端上的共享目录,仍然可以看到在容器中创建的文件。
PV 与 PVC
使用 NFS 共享存储虽然方便,但若各应用的 Pod 无限制地读写存储资源,长期下来可能导致磁盘管理混乱。为了解决这个问题,K8s 集群引入了 PV 和 PVC 资源对象,避免 Pod 直接挂载各种存储空间,从而对存储资源的管理和使用做了解耦隔离。
PV(PersistentVolume,持久卷)主要用于管理存储资源,PV 中指定了存储类型、存储空间大小和读写模式等信息。PV 可以由运维人员预先分配,或者通过存储类(Storage Class)动态分配。PV 支持的存储资源类型以插件的形式实现,K8s 默认支持 HostPath 卷、NFS 卷、Local 卷(节点上挂载的本地存储设备)等,你也可以安装 CSI 驱动插件从而支持更多的存储资源类型。
PVC(PersistentVolumeClaim,持久卷声明)表达了 Pod 对存储资源的需求,例如存储空间大小、读写模式等。当 Pod 需要存储空间时,就可以创建并挂载 PVC,此时 K8s 会给这个 PVC 自动选择一个合适的 PV 进行绑定。一旦绑定成功,Pod 就可以使用 PV 指定的存储资源了。需要注意的是,Pod 不能直接挂载 PV,而是通过 PVC 来间接使用 PV 的存储空间。
如图所示,PV + PVC 这种模式符合研发和运维团队的组织架构需求,其中 PV 由运维人员预先分配并管理,而 Pod 的实际存储空间是通过 PVC 来申请,所以 PVC 由研发人员维护,从而确保了两个团队分工和职责的明确性,提高了存储资源管理的效率和灵活性。
部署 PV 与 PVC
下面我们动手实验一下,创建一个 PV 和 PVC,然后在 Pod 中挂载 PVC 来使用存储空间。
部署 PV
PV 的部署也是通过 YAML 文件(my-pv.yaml)来实现的。
# my-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-pv-100m # 自定义PV名字
spec:
capacity:
storage: 100M # 定义这个PV的存储大小
accessModes:
- ReadWriteMany # 访问模式
persistentVolumeReclaimPolicy: Retain # 默认Retain
nfs:
server: k8s-worker2 # 指定NFS主机的IP地址或者主机名
path: /nfs/k8s/shared # 绑定主机的的路径
- capacity:定义 PV 的存储空间的容量大小。
-
accessModes: 设置 PV 的访问模式,指定应用使用 PV 时对存储资源的访问权限,访问权限包括下面几种方式:
-
ReadWriteOnce(RWO):读写权限,但是只能被单个节点挂载。
- ReadOnlyMany(ROX):只读权限,可以被多个节点挂载。
- ReadWriteMany(RWX):读写权限,可以被多个节点挂载。
- persistentVolumeReclaimPolicy:指定 PV 回收策略,默认为 Retain,下一课会再详细介绍 PV 的回收策略。
部署 PV,然后查看 PV 的状态。
[root@k8s-master ~]# kubectl apply -f my-pv.yaml
persistentvolume/my-pv-100m created
[root@k8s-master ~]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
my-pv-100m 100M RWX Retain Available 7s
在返回结果中,我们看到已经成功部署了 PV,其中 STATUS 状态为 Available ,表示可以使用。
部署 PVC
编写一个 PVC 的 YAML 文件(my-pvc.yaml),申请一个 100M 容量的可以被多个 Pod 挂载的存储空间。
# my-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc-100m # 自定义pvc名字
spec:
accessModes:
- ReadWriteMany # 访问模式
resources:
requests:
storage: 100M # 定义要申请的空间大小
部署 PVC,然后查看 PVC 的状态。
[root@k8s-master ~]# kubectl apply -f my-pvc.yaml
persistentvolumeclaim/my-pvc-100m created
[root@k8s-master ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-pvc-100m Bound my-pv-100m 100M RWX 9s
可以看出,部署 PVC 的时候,K8s 会自动寻找并绑定满足条件的 PV。目前,STATUS 状态显示为 Bound(已绑定),VOLUME 显示已绑定名为 “my-pv-100m” 的 PV 卷,这个就是上面部署好的 PV。此时,这个 PVC 就可以被 Pod 挂载并使用了。
挂载 PVC
我们通过 YAML 文件(my-pvc-deployment.yaml)部署一个 Deployment,它的 Pod 中的容器会挂载已经部署好的 PVC。
# my-pvc-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-pvc-deployment
spec:
replicas: 3
selector:
matchLabels:
app: pvc-busybox-pod
template:
metadata:
labels:
app: pvc-busybox-pod
spec:
containers:
- name: busybox-c
image: busybox
command: ["/bin/sh","-c","sleep 3600"]
volumeMounts:
- name: pvc-shared-data
mountPath: /tmp/data
volumes:
- name: pvc-shared-data
persistentVolumeClaim:
claimName: my-pvc-100m # 部署好的PVC
部署好 Deployment 后,我们可以进入到任意 Pod 的容器,就像上面 Pod 直接挂载 NFS 卷的示例一样,通过操作 “/tmp/data” 目录来使用挂载的共享空间。
[root@k8s-master ~]# kubectl apply -f my-pvc-deployment.yaml
deployment.apps/my-pvc-deployment created
小结
今天,我介绍了 K8s 中的文件持久化存储方式:挂载数据卷 Volume。在 Pod 中先定义代表某种存储空间的 Volume,然后在 Pod 的容器中,通过把这个 Volume 挂载到容器的某个目录,从而在容器中使用这个目录,实现了持久化存储。
数据卷主要有节点本地存储、网络共享存储、ConfigMap 和 Secret 资源对象,以及 PV 和 PVC。接着我带你搭建了一个 NFS 网络文件系统并创建了共享目录,然后部署了多个 Pod 副本的 Deployment,这些 Pod 通过挂载 NFS 的共享目录实现了文件的共享和持久化存储。
之后,我们详细认识了 PV 和 PVC 两种资源对象。PV(PersistentVolume,持久卷)是预先配置的存储资源,PVC(PersistentVolumeClaim,持久卷声明)是应用对存储资源的需求,当 Pod 需要存储空间时,就可以创建并加载 PVC,此时 K8s 会给这个 PVC 自动选择一个合适的 PV 进行绑定。一旦绑定成功,Pod 就可以使用 PV 提供的存储资源了。
最后通过一个实验,我带你了解了 PV 和 PVC 的 YAML 文件及使用方式。
思考题
这就是今天的全部内容,在 CKA 中也会考到相关的知识点。我给你留一道练习题。
- 创建一个 hostPath 类型的 PV,存储空间 10M,读写模式为 ReadWriteMany,宿主机目录为 “/host/data”。
- 创建一个 Pod 并挂载一个 PVC,PVC 的存储空间要求 20M,读写模式为 ReadWriteMany,Pod 容器挂载目录为 “/tmp”。
动手实践一下,看看 Pod 是否可以成功使用 PV 的存储空间,如果不行,需要怎样修改才能成功?相信经过动手实践,会让你对知识的理解更加深刻。