K8s详细学习笔记 第五章:数据持久化:Volume与Persistent Storage
第五章:数据持久化:Volume与Persistent Storage
到目前为止,我们所部署的应用都是**无状态(Stateless)的。这意味着Pod可以被随意销毁和替换,因为它们不保存任何需要长期存在的数据。Web服务器、API网关等都属于此类。然而,现实世界中的大多数应用,如数据库、消息队列、用户上传的文件系统等,都是有状态(Stateful)**的。它们产生的数据必须在Pod甚至节点发生故障后依然存在。
容器的文件系统是临时的(Ephemeral)。当一个容器崩溃或被删除时,Kubelet会清理其文件系统,所有写入的数据都会永久丢失。这对于无状态应用是可接受的,但对于有状态应用来说是致命的。
为了解决这个问题,Kubernetes引入了一套强大而抽象的存储体系。本章将深入剖析这套体系,从最基础的Volume概念,到生产级的PersistentVolume、PersistentVolumeClaim和StorageClass。
5.1 Volume: Pod中容器共享数据的解决方案
在Kubernetes的存储世界中,最基础的概念是Volume(卷)。Volume是Pod的一个组成部分,其生命周期与Pod绑定。这意味着只要Pod存在,Volume就会存在,即使其中的容器因为重启而消亡,Volume中的数据也会被保留下来。
Volume的核心作用:
- 数据持久化(Pod级别):在容器重启之间保持数据。
- 数据共享:在同一个Pod内的多个容器之间共享文件。
一个Pod可以定义多个Volume,而Pod内的每个容器都可以选择将哪些Volume挂载到其文件系统的特定路径上。
Kubernetes支持多种类型的Volume,它们有不同的用途和后端实现。以下是一些最常见的类型:
| Volume类型 | 描述 | 主要用途 |
|---|---|---|
emptyDir |
当Pod被分配到节点时,会创建一个初始为空的目录。只要Pod在该节点上运行,该目录就会一直存在。如果Pod被删除或迁移到其他节点,emptyDir及其数据将被永久删除。 |
|
hostPath |
将宿主Node节点上的文件或目录直接挂载到Pod中。如果Pod被删除,hostPath上的数据不会被删除。 |
|
configMap / secret |
如第四章所述,将ConfigMap或Secret的内容作为只读文件挂载到容器中,用于配置注入。 |
|
persistentVolumeClaim |
这是最重要、最常用的类型。它允许Pod使用独立于Pod生命周期的、真正的持久化网络存储。我们将在下一节详细讲解。 |
|
实践示例:使用emptyDir在两个容器间共享数据
这个例子将演示Sidecar模式,一个容器持续向一个文件写入数据,另一个容器则读取并展示该文件的内容。
# pod-with-emptydir.yaml
apiVersion: v1
kind: Pod
metadata:
name: shared-volume-pod
spec:
containers:
# 'writer' 容器
- name: writer-container
image: busybox
# 每5秒向 /pod-data/index.html 写入当前时间和主机名
command: ["/bin/sh", "-c", "while true; do echo \"Hello from $(hostname) at $(date)\" > /pod-data/index.html; sleep 5; done"]
volumeMounts:
- name: shared-data # 挂载名为 'shared-data' 的Volume
mountPath: /pod-data # 挂载到容器的 /pod-data 目录
# 'reader' 容器 (模拟一个Web服务器)
- name: reader-container
image: nginx:1.21
ports:
- containerPort: 80
volumeMounts:
- name: shared-data # 挂载同一个名为 'shared-data' 的Volume
mountPath: /usr/share/nginx/html # 挂载到Nginx的默认Web根目录
# 定义Pod级别的Volume
volumes:
- name: shared-data # Volume的名称
emptyDir: {} # Volume的类型是 emptyDir
kubectl apply -f pod-with-emptydir.yamlkubectl port-forward pod/shared-volume-pod 8080:80- 在浏览器或另一个终端访问
http://localhost:8080。你会看到writer-container写入的内容。每隔5秒刷新一次,内容会发生变化。
这个例子清晰地展示了emptyDir Volume如何成为同一Pod内两个容器之间的“桥梁”。但它的局限性也很明显:如果这个Pod被删除,所有的数据都将消失。
5.2 持久化存储概述:解耦存储与计算
为了解决有状态应用的核心问题——数据的生命周期必须独立于Pod的生命周期——Kubernetes设计了一套精巧的存储供应和消费机制。这套机制的核心是三个API对象:
- PersistentVolume (PV): 持久卷
- PersistentVolumeClaim (PVC): 持久卷声明
- StorageClass: 存储类
这套机制的目标是将**存储的提供(Provisioning)与存储的消费(Consumption)**彻底解耦。
- 集群管理员 (Administrator): 负责准备和管理实际的存储资源(如NFS服务器、云硬盘、Ceph集群等)。他们将这些资源以**PersistentVolume (PV)**的形式注册到Kubernetes集群中,供用户使用。
- 开发者/用户 (User): 不需要关心底层存储的具体实现细节。他们只需要通过一个**PersistentVolumeClaim (PVC)**对象,向集群“声明”他们需要什么样的存储(例如,“我需要5GB的快速读写存储”)。
Kubernetes会作为一个中间人,自动将用户的“声明”(PVC)与管理员提供的、符合条件的“资源”(PV)进行绑定(Binding)。
PersistentVolume (PV):由管理员创建的存储资源
PV是对底层共享存储资源(如NFS卷、iSCSI、AWS EBS、GCE PD)在Kubernetes集群中的一种抽象表示。它包含了存储实现的细节,但将这些细节与Pod的使用隔离开。
一个PV对象包含了以下关键信息:
-
capacity: 存储容量,例如5Gi、100Mi。 -
volumeMode: 卷模式,可以是Filesystem(默认,卷被格式化为文件系统)或Block(卷作为原始块设备)。 -
accessModes: 访问模式,定义了PV应如何被节点挂载。这是非常重要的一个属性。访问模式 简称 描述 ReadWriteOnceRWO 单节点读写。该卷只能被一个节点以读写方式挂载。适用于大多数块存储(如AWS EBS, GCE PD)。 ReadOnlyManyROX 多节点只读。该卷可以被多个节点以只读方式挂载。 ReadWriteManyRWX 多节点读写。该卷可以被多个节点以读写方式同时挂载。适用于文件存储(如NFS, CephFS)。 ReadWriteOncePodRWOP 单Pod读写。该卷只能被单个Pod以读写方式挂载。适用于CSI驱动。 -
persistentVolumeReclaimPolicy: 回收策略。定义了当与其绑定的PVC被删除后,这个PV以及其上的数据该如何处理。回收策略 描述 Retain(保留)(默认和推荐) 当PVC被删除时,PV的状态变为 Released,但PV对象和底层的存储卷(以及所有数据)都不会被删除。需要管理员手动清理和回收。这是生产环境中最安全的选择,防止数据误删。Delete(删除)当PVC被删除时,PV对象和底层的存储卷(例如AWS EBS卷)都会被自动删除。适用于测试环境或数据不重要的情况。 Recycle(回收)(已废弃) 当PVC被删除时,会对卷执行基本清理( rm -rf /thevolume/*),然后使其可用于新的PVC。 -
storageClassName: 存储类的名称。用于将PV与特定的StorageClass关联起来,是实现动态供给的关键。 -
存储后端详情: 例如,NFS服务器的地址和路径、AWS EBS的卷ID等。
PersistentVolumeClaim (PVC):用户对存储资源的请求
PVC是用户对存储资源的一次“申请”或“声明”。Pod在其volumes定义中引用PVC,而不是直接引用PV。这使得Pod的定义可以保持高度的可移植性,因为它不依赖于任何特定的底层存储技术。
一个PVC对象主要包含以下请求信息:
accessModes: 用户期望的访问模式。resources.requests.storage: 用户期望的最小存储容量。storageClassName: 用户希望使用哪个StorageClass来满足这个声明。
PV和PVC的绑定过程:
- 用户创建一个PVC,请求特定大小和访问模式的存储。
- Kubernetes的控制平面会遍历集群中所有可用的PV。
- 它会寻找一个尚未绑定的PV,该PV的
capacity大于等于PVC请求的storage,并且其accessModes包含PVC请求的accessModes。 - 如果找到了匹配的PV,Kubernetes就会将这个PV和PVC绑定在一起。此时,PV和PVC的状态都变为
Bound。 - 一旦绑定成功,这个PV就独占地分配给了这个PVC,其他PVC无法再使用它。
- Pod就可以通过在其
volumes中引用这个PVC的名称来挂载并使用这块持久化存储了。
5.3 StorageClass: 实现动态存储供给
上面描述的PV和PVC模型被称为静态供给(Static Provisioning)。它要求集群管理员预先创建好一系列PV。这种模式在小型或存储需求固定的集群中是可行的。但在大型、多租户的云环境中,如果每次开发者需要存储,都需要管理员手动创建一个PV,这将是巨大的运维瓶颈。
为了解决这个问题,Kubernetes引入了StorageClass,实现了动态供给(Dynamic Provisioning)。
StorageClass对象定义了一种存储的“类别”或“模板”。它告诉Kubernetes如何(通过哪个provisioner)以及使用什么参数(parameters)来动态地创建一个新的PV。
一个StorageClass的关键字段:
provisioner: 指定使用哪个卷插件(Volume Plugin)来创建PV。例如,kubernetes.io/aws-ebs用于AWS EBS,kubernetes.io/gce-pd用于GCE PD,或者各种CSI驱动。parameters: 传递给provisioner的参数。这些参数完全取决于provisioner的类型。例如,对于AWS EBS,可以指定type: gp3(卷类型)、fsType: ext4(文件系统类型)。reclaimPolicy: 指定由这个StorageClass动态创建的PV的回收策略(Delete或Retain)。
动态供给的工作流程:
- 管理员不再需要预先创建PV,而是创建并配置一个或多个StorageClass。例如,可以创建一个
fast-ssd类和一个slow-hdd类。 - 开发者创建一个PVC,并在其
spec.storageClassName字段中明确指定要使用的StorageClass的名称(例如fast-ssd)。 - Kubernetes看到这个PVC后,不会去寻找现成的PV。相反,它会找到名为
fast-ssd的StorageClass。 - 它调用该StorageClass定义的
provisioner,并传入parameters和PVC请求的容量。 provisioner(例如AWS EBS的插件)会调用云提供商的API,在后台实时创建一个新的EBS卷。- EBS卷创建成功后,
provisioner会自动在Kubernetes集群中创建一个对应的PV对象,并将其信息(如卷ID)填入。 - 最后,这个新创建的PV会自动与用户的PVC进行绑定。
整个过程对用户来说是完全自动化的。用户只需提交一个PVC,几秒或几十秒后,一块全新的、符合其要求的云硬盘就已经准备好并可以被Pod使用了。
5.4 实战: 为一个有状态应用(如数据库)挂载持久化存储
让我们来为一个PostgreSQL数据库Pod提供持久化存储。我们将使用动态供给的方式,这是云原生环境下的标准做法。
(前提:你的K8s集群必须配置了默认的StorageClass。大多数云厂商的K8s服务和本地工具如Minikube、Rancher Desktop都会自动配置好。你可以通过kubectl get storageclass命令查看。)
步骤1:创建一个PersistentVolumeClaim (PVC)
我们将创建一个PVC,请求1Gi的存储空间。我们不指定storageClassName,这样它会使用集群中被标记为**(default)**的那个StorageClass。
# postgres-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce # 请求单节点读写模式,适合数据库
resources:
requests:
storage: 1Gi # 请求1GB的存储空间
kubectl apply -f postgres-pvc.yaml
现在,立即检查PVC和PV的状态:
kubectl get pvc postgres-pvc
# 输出中 STATUS 应该是 Bound
kubectl get pv
# 你会看到一个由StorageClass自动创建的、与postgres-pvc绑定的PV
这证明了动态供给已经成功工作!
步骤2:创建一个使用PVC的PostgreSQL Deployment
现在我们创建一个Deployment来运行PostgreSQL。注意,在生产环境中,StatefulSet是管理数据库等有状态应用的更好选择,但为了简化本章的例子,我们先使用Deployment来演示核心的存储挂载。
# postgres-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres-deployment
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:13
ports:
- containerPort: 5432
env:
# 设置数据库密码,注意生产环境应使用Secret
- name: POSTGRES_PASSWORD
value: "mysecretpassword"
volumeMounts:
- name: postgres-storage # 挂载点的名称
# PostgreSQL的数据默认存储在这个目录
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-storage # Volume的名称
persistentVolumeClaim:
claimName: postgres-pvc # 引用我们刚刚创建的PVC
kubectl apply -f postgres-deployment.yaml
步骤3:验证数据持久性
-
等待Pod启动:
kubectl get pods -l app=postgres,等待Pod变为Running。 -
连接到数据库并创建数据:
# 获取Pod名称 POD_NAME=$(kubectl get pods -l app=postgres -o jsonpath='{.items[0].metadata.name}') # 进入Pod的shell kubectl exec -it $POD_NAME -- bash # 在Pod内部,连接到psql # psql -U postgres # 创建一个表并插入数据 # CREATE TABLE mydata (message TEXT); # INSERT INTO mydata VALUES ('Hello Persistent World!'); # \q # exit -
模拟Pod故障:删除Pod:
kubectl delete pod $POD_NAME -
观察Pod重建: Deployment控制器会立即发现Pod数量不符合期望,并创建一个新的PostgreSQL Pod。
kubectl get pods -l app=postgres -w(等待新的Pod变为Running) -
连接到新Pod并验证数据:
# 获取新Pod的名称 NEW_POD_NAME=$(kubectl get pods -l app=postgres -o jsonpath='{.items[0].metadata.name}') # 进入新Pod的shell kubectl exec -it $NEW_POD_NAME -- bash # 再次连接到psql并查询数据 # psql -U postgres # SELECT * FROM mydata;你应该能看到之前插入的
Hello Persistent World!这条记录。
结论:数据被成功地保留了下来!这是因为数据实际存储在由PV代表的后端存储卷上。当旧Pod被删除时,PVC和PV都保持Bound状态。当新Pod被创建时,它通过引用同一个PVC,重新挂载了同一个PV,从而访问到了之前写入的所有数据。
【重难点】 理解PV、PVC和StorageClass之间的绑定关系和生命周期管理
这是掌握Kubernetes有状态应用管理的核心,也是最容易混淆的地方。让我们来系统地梳理一下。
三者关系总结:
- Pod -> PVC (消费关系): Pod是存储的最终消费者。它在其
volumes定义中通过名称引用一个PVC,从而获得存储。Pod的定义与具体的存储实现完全解耦。 - PVC <-> PV (绑定关系): PVC是用户对存储的请求,PV是集群中可用的存储资源。Kubernetes负责将满足PVC条件的PV与其进行一对一绑定。PVC是连接Pod和PV的桥梁。
- StorageClass -> PV (创建关系): StorageClass是创建PV的模板或工厂。在动态供给模式下,它根据PVC的请求,指示
provisioner自动创建出PV。
生命周期管理——数据安全的保障
理解当PVC被删除时会发生什么是至关重要的,这直接关系到你的数据安全。这个行为由PV的persistentVolumeReclaimPolicy决定。
| Reclaim Policy | 当kubectl delete pvc my-pvc时… |
适用场景 |
|---|---|---|
Retain (保留) |
1. PVC被删除。 2. 与之绑定的PV的状态变为 Released。 3. PV对象不会被删除。 4. 后端存储卷(如AWS EBS)不会被删除。数据完好无损。 5. 这个 Released状态的PV无法被新的PVC自动绑定,需要管理员介入:可以选择备份数据,然后手动删除PV和后端存储卷;或者清除PV的claimRef字段,使其变回Available状态供其他PVC使用。 |
生产环境、关键数据。 这是最安全的选择,提供了在意外删除PVC后的“反悔”机会,可以手动恢复数据。 |
Delete (删除) |
1. PVC被删除。 2. Kubernetes会立即触发对后端存储卷的删除操作(调用云API)。 3. 后端存储卷被删除后,PV对象也会被自动删除。 4. 数据永久丢失! |
开发/测试环境、临时数据、CI/CD流水线中的临时数据库。 方便自动化清理,但风险极高。 |
为什么这个体系如此设计?
这种将存储抽象为PV、PVC、StorageClass三层模型的设计,体现了Kubernetes面向角色的关注点分离的哲学:
- 存储管理员: 专注于提供和管理底层存储设施。他们定义
StorageClass,决定提供哪些类型的存储、性能如何、成本如何,并设置好回收策略,保证数据安全。 - 应用开发者: 专注于应用程序本身。他们不需要了解NFS、Ceph或AWS EBS的细节。他们只需要像申请CPU和内存一样,通过一个简单的PVC来申请所需大小和访问模式的存储即可。
这种清晰的职责划分,使得在复杂的组织和云环境中,存储的管理和使用变得既标准化又高效。掌握了这套体系,你就真正具备了在Kubernetes上驾驭有状态应用的能力。
K8s 详细学习笔记 目录
以下是整个系列的10章目录,点击章节标题即可跳转阅读:
- K8s详细学习笔记 第一章:K8s入门与核心概念
- K8s详细学习笔记 第二章:部署你的第一个应用:Pod与Deployment
- K8s详细学习笔记 第三章:应用访问与服务发现:Service与Ingress
- K8s详细学习笔记 第四章:应用配置管理:ConfigMap与Secret
- K8s详细学习笔记 第五章:数据持久化:Volume与Persistent Storage
- K8s详细学习笔记 第六章:应用的健康检查与自愈能力
- K8s详细学习笔记 第七章:资源的限制与调度
- K8s详细学习笔记 第八章:应用的扩缩容与更新策略
- K8s详细学习笔记 第九章:K8s安全机制:认证、授权与准入控制
- K8s详细学习笔记 第十章:集群运维与故障排查
更多推荐
所有评论(0)