第五章:数据持久化:Volume与Persistent Storage

到目前为止,我们所部署的应用都是**无状态(Stateless)的。这意味着Pod可以被随意销毁和替换,因为它们不保存任何需要长期存在的数据。Web服务器、API网关等都属于此类。然而,现实世界中的大多数应用,如数据库、消息队列、用户上传的文件系统等,都是有状态(Stateful)**的。它们产生的数据必须在Pod甚至节点发生故障后依然存在。

容器的文件系统是临时的(Ephemeral)。当一个容器崩溃或被删除时,Kubelet会清理其文件系统,所有写入的数据都会永久丢失。这对于无状态应用是可接受的,但对于有状态应用来说是致命的。

为了解决这个问题,Kubernetes引入了一套强大而抽象的存储体系。本章将深入剖析这套体系,从最基础的Volume概念,到生产级的PersistentVolumePersistentVolumeClaimStorageClass

5.1 Volume: Pod中容器共享数据的解决方案

在Kubernetes的存储世界中,最基础的概念是Volume(卷)。Volume是Pod的一个组成部分,其生命周期与Pod绑定。这意味着只要Pod存在,Volume就会存在,即使其中的容器因为重启而消亡,Volume中的数据也会被保留下来。

Volume的核心作用:

  1. 数据持久化(Pod级别):在容器重启之间保持数据。
  2. 数据共享:在同一个Pod内的多个容器之间共享文件。

一个Pod可以定义多个Volume,而Pod内的每个容器都可以选择将哪些Volume挂载到其文件系统的特定路径上。

Kubernetes支持多种类型的Volume,它们有不同的用途和后端实现。以下是一些最常见的类型:

Volume类型 描述 主要用途
emptyDir 当Pod被分配到节点时,会创建一个初始为空的目录。只要Pod在该节点上运行,该目录就会一直存在。如果Pod被删除或迁移到其他节点,emptyDir及其数据将被永久删除
  • Pod内多容器间共享数据的临时空间(例如,一个容器生成数据,另一个容器处理数据)。
  • 用作缓存、检查点等临时存储。
hostPath 将宿主Node节点上的文件或目录直接挂载到Pod中。如果Pod被删除,hostPath上的数据不会被删除。
  • 需要访问宿主机系统文件或Docker内部的Pod(如监控代理)。
  • 【警告】 强烈不推荐在大多数应用中使用。它将Pod与特定的Node紧密耦合,如果Pod被调度到其他Node,数据就丢失了。此外,它还存在严重的安全风险。
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
  1. kubectl apply -f pod-with-emptydir.yaml
  2. kubectl port-forward pod/shared-volume-pod 8080:80
  3. 在浏览器或另一个终端访问 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: 存储容量,例如5Gi100Mi

  • volumeMode: 卷模式,可以是Filesystem(默认,卷被格式化为文件系统)或Block(卷作为原始块设备)。

  • accessModes: 访问模式,定义了PV应如何被节点挂载。这是非常重要的一个属性。

    访问模式 简称 描述
    ReadWriteOnce RWO 单节点读写。该卷只能被一个节点以读写方式挂载。适用于大多数块存储(如AWS EBS, GCE PD)。
    ReadOnlyMany ROX 多节点只读。该卷可以被多个节点以只读方式挂载。
    ReadWriteMany RWX 多节点读写。该卷可以被多个节点以读写方式同时挂载。适用于文件存储(如NFS, CephFS)。
    ReadWriteOncePod RWOP 单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的绑定过程:

  1. 用户创建一个PVC,请求特定大小和访问模式的存储。
  2. Kubernetes的控制平面会遍历集群中所有可用的PV。
  3. 它会寻找一个尚未绑定的PV,该PV的capacity大于等于PVC请求的storage,并且其accessModes包含PVC请求的accessModes
  4. 如果找到了匹配的PV,Kubernetes就会将这个PV和PVC绑定在一起。此时,PV和PVC的状态都变为Bound
  5. 一旦绑定成功,这个PV就独占地分配给了这个PVC,其他PVC无法再使用它。
  6. 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的回收策略(DeleteRetain)。

动态供给的工作流程:

  1. 管理员不再需要预先创建PV,而是创建并配置一个或多个StorageClass。例如,可以创建一个fast-ssd类和一个slow-hdd类。
  2. 开发者创建一个PVC,并在其spec.storageClassName字段中明确指定要使用的StorageClass的名称(例如fast-ssd)。
  3. Kubernetes看到这个PVC后,不会去寻找现成的PV。相反,它会找到名为fast-ssd的StorageClass。
  4. 它调用该StorageClass定义的provisioner,并传入parameters和PVC请求的容量。
  5. provisioner(例如AWS EBS的插件)会调用云提供商的API,在后台实时创建一个新的EBS卷
  6. EBS卷创建成功后,provisioner会自动在Kubernetes集群中创建一个对应的PV对象,并将其信息(如卷ID)填入。
  7. 最后,这个新创建的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:验证数据持久性

  1. 等待Pod启动: kubectl get pods -l app=postgres,等待Pod变为Running

  2. 连接到数据库并创建数据:

    # 获取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
    
  3. 模拟Pod故障:删除Pod:

    kubectl delete pod $POD_NAME
    
  4. 观察Pod重建: Deployment控制器会立即发现Pod数量不符合期望,并创建一个新的PostgreSQL Pod
    kubectl get pods -l app=postgres -w (等待新的Pod变为Running)

  5. 连接到新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章目录,点击章节标题即可跳转阅读:

更多推荐