Kubernetes存储卷:保障有状态应用的数据持久化

在前面几个章节,介绍了k8s的pod以及网络相关的知识,在这一章,则开始对k8s的存储进行介绍。k8s卷(volumes)为container提供了文件系统访问以及数据共享的能力。

1. 引入

首先先引入一个问题,为什么k8s要提供文件系统访问以及数据共享的能力?

如果大家使用docker部署过数据库,就会发现,一般来说,我们都会将容器数据库中的data,log目录挂载到host的目录下,这样即使容器G了,至少我们的数据不会丢失。因此在k8s中,我们也需要某个存储方式,能够让数据脱离容器的生命周期而存在,然后也能够让数据在不同的pod、容器内进行共享存储。例如:

  1. 数据库pod:将数据库的数据进行持久化保存,就需要挂载持久化存储。
  2. 日志文件:例如一个pod中多个容器,进行共享日志文件。比如说一个pod中的应用容器产生日志,而sidecar容器收集日志,上传到统一的日志中心。
  3. 挂载配置文件(configmap):一般来说,我们的应用在启动的过程中,可能需要读取一些配置文件。我们当然可以在打包镜像的时候,将配置文件放进去,但是这样的话,就失去了灵活(配置进行更改的时候,还需要重新打包镜像,上传,部署)。如果我们将配置写入到某个统一的地方,然后挂载到应用容器中去,那么应用便可以实时读取配置(因为修改配置之后,挂载的文件也会自动更新)。
  4. 挂载敏感信息(Secret):比如说,我的应用需要使用数据库,我们可以将数据库账号密码设置为环境变量,但是环境变量存在泄露的风险(ps -ef打印环境变量),我们也可以将账号密码放在configmap里面,但是configmap谁都能看到,不符合最小化权限设计原则。因此,我们可以将相关敏感信息放在某中类型的存储卷(Secret)中,然后对其进行权限控制,甚至使用密钥进行加密。
  5. 使用云存储:这个就是云厂商的存储卷直接挂载到pod中,方便使用。

加下来将详细的对各个类型的卷进行介绍。

plantuml 复制代码
@startmindmap
* Kubernetes 存储 (Storage)
** 临时卷 (Ephemeral Volumes) <<Pod 内置>>
*** emptyDir
*** configMap
*** secret
*** 通用临时卷
** 持久卷 (Persistent Volumes) <<集群级资源>>
*** 持久卷声明 (PVC) <<用户接口>>
**** 通过 StorageClass 动态供应
**** 绑定静态 PV
*** 持久卷 (PV) <<管理员/系统创建>>
**** 本地存储 (Local Storage)
***** hostPath <<仅单节点>>
***** local (Local Persistent Volume) <<多节点需调度约束>>
**** 网络/云存储 (Network / Cloud Storage)
***** 文件存储 (File Storage) <<支持 RWX>>
****** nfs
****** cephfs
****** azureFile
****** glusterfs
****** AWS EFS / GCP Filestore
***** 块存储 (Block Storage) <<通常 RWO>>
****** awsElasticBlockStore (EBS)
****** gcePersistentDisk (GCE PD)
****** azureDisk
****** cinder (OpenStack)
****** rbd (Ceph RBD)
****** iscsi
****** fc (Fibre Channel)
***** 分布式/企业存储
****** portworxVolume
****** storageos
****** scaleIO
****** quobyte
****** vsphereVolume
****** photonPersistentDisk
@endmindmap

2. 临时卷

K8s中的临时卷(Ephemeral Volumes) 是一种生命周期与 Pod 绑定的存储卷,它在 Pod 创建时动态创建,在 Pod 删除时自动清理。临时卷主要用于提供临时、高性能或特定用途的本地存储,不适用于需要持久化数据的场景。常用的可以分为如下几种:

  1. emptyDir:Pod 启动时为空,存储介质可以是磁盘或内存(Node中的磁盘或内存),pod中所有容器共享该卷。在生产中,我们常用emptyDir来收集日志。例如,在一个pod中多个容器,应用容器生成日志在临时卷中,sidecar容器(日志收集容器)将临时卷中的日志上传到ELK。又或者说,CI/CD构建过程中,源码pull在emptyDir中,编译后的产物通过sidecar上传到制品库。

    plantuml 复制代码
    @startuml
    ' 启用中文支持(确保环境支持 UTF-8)
    skinparam defaultTextAlignment center
    skinparam wrapWidth 200
    skinparam backgroundColor #FFFFFF
    
    ' 自定义颜色
    skinparam component {
      BackgroundColor #E6F3FF
      BorderColor #1E88E5
      FontColor #0D47A1
    }
    
    skinparam package {
      BackgroundColor #F0F8E0
      BorderColor #7CB342
      FontColor #33691E
    }
    
    skinparam folder {
      BackgroundColor #FFF3E0
      BorderColor #FB8C00
      FontColor #E65100
    }
    
    package "Kubernetes 节点" <<Node>> {
      [Pod\n(my-app)] as pod #BBDEFB
    
      package "容器 1\n(主应用)" <<Container>> {
        [挂载点: /cache] as m1
      }
    
      package "容器 2\n(日志收集器)" <<Container>> {
        [挂载点: /shared] as m2
      }
    
      [临时卷 emptyDir\n(名称: temp-storage)] as emptydir #FFECB3
    
      pod --> m1
      pod --> m2
      m1 --> emptydir : 挂载\nmountPath: /cache
      m2 --> emptydir : 挂载\nmountPath: /shared
    }
    
    node "节点本地存储" <<Storage>> {
      folder "/var/lib/kubelet/pods/...\n/temp-storage" as nodeDir
    }
    
    emptydir --> nodeDir : 存储位置\n• 默认:节点磁盘\n• 可选:内存 (tmpfs)
    @enduml
  2. configMap、secret等一类将资源文件挂载为卷:正如我们前面所提到,我们需要将某些配置文件或者私密文件作为pod容器启动或者运行参数配置,而这些配置对于每个容器都是统一的,但是在生产的过程中有可能发生变更(例如nginx的config)。这时候我们就可以定义configMap,或者secret​[[注]](#[注])(本质上,这两者都是资源文件,当我们定义它们的时候,相关的配置文件会保存到k8s的etcd中),然后在pod运行的时候,将configMap或者secret文件挂载到容器中(一般来说,都是ready only的)。具体的使用,可以参考5.2 secret 和 ConfigMap 卷 · Kubernetes - 痴者工良[[注]](#[注])。举个例子:

    yaml 复制代码
    # nginx-configmap.yaml 定义一个configMap资源
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: nginx-config
    data:
      nginx.conf: |
        events {}
        http {
          server {
            listen 80;
            location / {
              return 200 "Hello from ConfigMap!\n";
              add_header Content-Type text/plain;
            }
          }
        }
    
    #----------另外一个pod定义文件---------------
    # nginx-pod.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx-with-config
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        volumeMounts:
    	# 表示这个挂载点引用的是下面 volumes 中定义的名为 config-volume 的卷
        - name: config-volume
    	  # 表示要把卷挂载到容器内的 这个具体路径
          mountPath: /etc/nginx/nginx.conf
    	  # 只挂载卷中的 nginx.conf 这一个文件,而不是整个卷目录。
          subPath: nginx.conf
      volumes:
    	# 定义一个名为 config-volume 的卷,供上面的 volumeMounts 引用。
      - name: config-volume
        configMap:
    	  # 数据来源是一个叫 nginx-config 的 ConfigMap。
          name: nginx-config
  3. 通用临时卷:Generic Ephemeral Volume(通用临时卷)的作用基本上和emptyDir很类似,都是k8s为pod提供的临时存储方案,数据的生命周期与pod进行绑定。但是通用临时卷相比于emptyDir,容量可控、性能更强。通用临时卷依靠CSI驱动(Container Storage Interface,容器存储接口, 将外部存储系统翻译为k8s存储系统的插件),可以将外部存储(云盘、本地SSD、网络存储)挂载为pod的临时卷,并支持指定容量大小,以及高级的存储特性(例如加密,快照)。emptyDir ​ 是"轻量级临时盘",通用临时卷是"带容量和性能保障的临时云盘"。简单场景用 emptyDir,高性能/大容量/需管控的场景用通用临时卷。 如果用一个形象的例子来理解,就是emptyDir是个人电脑上的临时文件夹,而通用临时卷就是NAS上面的临时文件夹(容量大,有快照,可配置容量限制......)。

3. 持久卷

3.1 PV & PVC

在生产中,我们当然不仅仅是使用临时卷,还需要使用持久卷(Persistent Volume,PV),以实现数据的持久化。这样及时pod被删除、重建也能够实现数据的保留以支持有状态应用(StatefulSets,例如数据库,redis,kafka、对象存储),或者实现跨节点共享数据。

  • PV 是集群中由管理员预先配置或由存储类(StorageClass,本质上是一个 "存储模板",告诉k8s如何动态创建持久卷)动态创建的一块存储资源(如 NFS、iSCSI、云盘等)。它是集群级别的资源,生命周期独立于使用它的 Pod。
  • Persistent Volume Claim(PVC) ,PVC 是用户对存储资源的"申请",类似于 Pod 对 CPU/内存的请求,定义了用户希望使用的存储大小、访问模式(如只读、读写、单节点或多节点访问)。k8s会根据 PVC 的要求,自动绑定一个合适的PV。
yaml 复制代码
# pv配置文件 pv.yaml
apiVersion: v1
# 资源类型:PersistentVolume(持久卷)
kind: PersistentVolume
metadata:
  # PV 的名称,在整个集群中必须唯一
  name: my-pv
spec:
  # 定义该 PV 的存储容量
  capacity:
    # 请求的存储大小,单位可以是 Gi(Gibibyte)、Mi 等
    storage: 5Gi
  # 访问模式:定义该卷如何被挂载
  # - ReadWriteOnce (RWO):只能被单个节点以读写方式挂载
  # - ReadOnlyMany (ROX):可被多个节点以只读方式挂载
  # - ReadWriteMany (RWX):可被多个节点以读写方式挂载
  accessModes:
    - ReadWriteOnce
  # 回收策略:当 PVC 被删除后,PV 如何处理
  # - Retain(保留):手动回收,数据不会被删除(适合重要数据)
  # - Delete(删除):自动删除底层存储(如云盘),仅适用于动态供应
  # - Recycle(已废弃):旧版本的自动清理方式,不推荐使用
  persistentVolumeReclaimPolicy: Retain

  # hostPath 是一种 将宿主机(Node)上的文件或目录挂载到 Pod 中 的方式。
  hostPath:
    # 宿主机上的实际路径,PV 的数据将存储在此目录
    path: /mnt/data


# -------pvc配置文件------ pvc.yaml
apiVersion: v1
# 资源类型:PersistentVolumeClaim(持久卷声明)
kind: PersistentVolumeClaim
metadata:
  # PVC 的名称,在命名空间内唯一 Pod 将通过此名称引用该 PVC
  name: my-pvc
  # namespace: my-namespace

# PVC 的规格
spec:
  # 期望的访问模式,必须与 PV 的 accessModes 兼容
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      # k8s会寻找 capacity.storage >= 3Gi 且未被绑定的 PV
      storage: 3Gi

# ----------pod.yaml 文件
apiVersion: v1
kind: Pod
metadata:
  # Pod 的名称
  name: nginx-pod

# Pod 的规格
spec:
  containers:
    - name: nginx                    # 容器名称
      image: nginx:alpine            # 使用的镜像
      volumeMounts:                  # 挂载卷到容器内
        - name: web-content          # 与下方 volumes.name 对应
          mountPath: "/usr/share/nginx/html"  # 容器内的挂载路径

  # 定义 Pod 使用的卷(volumes)
  volumes:
    - name: web-content             # 卷名称,需与 volumeMounts.name 一致
      persistentVolumeClaim:        # 表示该卷使用 PVC 提供的存储
        claimName: my-pvc           # 引用前面创建的 PVC 名称 必须在同一命名空间下

上面的代码,是我们手动创建了一个pv,pvc,然后pod去使用pvc。在k8s中,创建pv有两种方式,一种是上面的这种,用户手动创建一个pv,指定pv相关的配置,然后让pvc消费,这种称之为静态制备 。还有一种,是pvc进行制备,比如说pvc指定了一个不存在pv,则就根据pvc里面StorageClass的配置,制作出一块pv出来,称之为动态制备

方式 是否涉及StorageClass
静态制备 不涉及 管理员手动提前创建好 PV (比如用hostPath​、NFS 等),PVC 去匹配它。PV 里没有 storageClassName字段
动态制备 涉及 PVC 指定 StorageClass → 系统自动创建 PV →这个 PV 会自动带上 .spec.storageClassName字段,值等于 PVC 请求的 StorageClass 名称。

3.2 本地存储

本地存储分为hostPath和local PV:

  • hostPath:hostPath 卷能将Node工作节点文件系统上的文件或目录挂载到你的 Pod 中。也就是说,如果pod部署在A节点上,就会使用A节点的某个目录,如果pod被删除重新部署到B节点上,那么就会使用B节点的某个目录,之前的数据就丢失了(因为数据在A节点上)。因此多副本 Pod 无法共享数据(每个节点数据独立),只适合单节点应用测试,不适宜生产环境。

    • 没有 PVC,没有 PV。
    • Pod 被调度到哪个节点,就用哪个节点的 /mnt/data
    yaml 复制代码
    # pod.yaml
    apiVersion: v1
    kind: Pod
    spec:
      containers:
        - name: app
          volumeMounts:
            - name: data
              mountPath: /data
      volumes:
        - name: data
          hostPath:
            path: /mnt/data   # ← 直接指定节点路径!
  • local PV:在hostPath中,我们无法控制pod部署在哪个节点(即使我们通过nodeSelector​来进行控制,如果未来pod需要重新部署在其他节点,那么我们所有的pod配置都需要修改,也就是说pod和node进行了一个强耦合。)而localPV就是为了解决这个问题,localPV只支持静态制备。管理员预先在特定节点上准备磁盘或目录 ,然后创建local PV,显式的声明该存储位于哪个节点,然后pod使用pvc去挂载目录。在这种情况下,pod并没有与节点node形成一个强依赖,pod只是依赖于pvc。在下面的依赖配置中,pv-local指定为node-1节点,也就是说pv部署在node-1中。而pvc通过storageClassName: local-storage​,可以将pvc-local​与pv-local​进行绑定。而pod通过使用pvc-local就会将pod调度到node-1中。

    默认情况下,k8s在 PVC 创建后立即尝试绑定一个 PV(称为 Immediate Binding )。而 volumeBindingMode: WaitForFirstConsumer 表示:"不要急着绑定 PV!等第一个使用这个 PVC 的 Pod 被调度时,再根据 Pod 的调度结果来绑定合适的 PV。"

    这是因为如果我们有两个pv,pv-1绑定在node1中,pv-2绑定在node2中。如果创建pvc的时候,立即绑定pv(比如说随机选到了pv-1,绑定到了node1),但是创建pod的时候,node1对应的cpu或者内存资源又不足,调度器想把pod调度到node2中,那么肯定会调度失败,因为Pod 要去 node-2​,但存储在 node-1,因此会调度失败。因此我们需要进行延迟绑定。

    yaml 复制代码
    # storageclass-local.yaml
    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    meta
      name: local-storage
    provisioner: kubernetes.io/no-provisioner
    volumeBindingMode: WaitForFirstConsumer
    
    # pv-local.yaml
    apiVersion: v1
    # 资源类型:PersistentVolume(持久卷)
    kind: PersistentVolume
    metadata:
      name: pv-local
    spec:
      capacity:
        # 请求的存储大小,这只是声明值,Kubernetes 不会验证底层实际大小
        storage: 100Gi
      # 卷的模式:指定存储是作为文件系统还是原始块设备使用
      # - Filesystem(默认):挂载为目录,Pod 通过文件读写(绝大多数场景)
      # - Block:作为原始块设备暴露给容器(需容器内格式化,高级用法)
      volumeMode: Filesystem
      # 访问模式:定义该卷如何被节点挂载
      accessModes:
        - ReadWriteOnce
      # 回收策略:当绑定的 PVC 被删除后,PV 如何处理
      persistentVolumeReclaimPolicy: Delete
      storageClassName: local-storage
      local:
        # 节点上实际的目录或挂载点路径
        path: /mnt/disks/ssd1
    
      # 节点亲和性:强制指定该 PV 只能被调度到特定节点
      nodeAffinity:
        required:
          nodeSelectorTerms:
            - matchExpressions:
                # 匹配节点的标签
                - key: kubernetes.io/hostname
                  # In 表示节点 hostname 必须在 values 列表中
                  operator: In
                  # 允许使用该 PV 的节点主机名列表
                  # 通常只写一个节点(因为本地存储不共享)
                  values: ["node-1"]   # ← 明确绑定到 node-1
    
    ---
    # pvc.yaml
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: pvc-local
    spec:
      storageClassName: local-storage
      accessModes: [ReadWriteOnce]
      resources:
        requests:
          storage: 100Gi
    
    ---
    # pod.yaml
    spec:
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: pvc-local   # ← 通过 PVC 间接使用

3.3 网络存储

前面我们介绍的local pv,hostPath,都存在一个问题,那就是pv是跟node节点进行了一个强绑定,多节点多pod没法使用同一个pv。因此"网络存储"出来了,网络存储的数据不绑定在某一台物理节点上 ,因此更适合多节点集群中的持久化需求,还能够实现快照,副本等等高级存储特性,听起来是不是跟通用临时卷很像。在 Kubernetes(k8s)中,持久卷(PersistentVolume, PV)的"网络存储" 是指通过网络协议访问的、可跨节点共享或挂载的存储系统。

  1. 文件存储(File Storage) ​:多个 Pod(跨节点)可同时读写同一份数据,适合共享配置、上传目录等场景。

    存储类型 说明 适应场景
    NFS 经典网络文件系统,开源、轻量、广泛支持 中小规模集群,自建存储
    CephFS Ceph 提供的 POSIX 兼容文件系统 大规模分布式存储,高可用
    GlusterFS 开源分布式文件系统(Red Hat 支持) 已逐渐被 Ceph 取代
    AWS EFS Amazon Elastic File System AWS 上的托管 RWX 文件存储
    Azure Files 微软 Azure 的 SMB/NFS 文件服务 Azure 云环境
    GCP Filestore Google Cloud 的托管 NFS 服务 GCP 云环境

    例如,定义一个NFS PV:

    yaml 复制代码
    apiVersion: v1
    kind: PersistentVolume
    spec:
      capacity:
        storage: 100Gi
      accessModes: [ReadWriteMany]
      nfs:
        server: nfs.example.com
        path: "/shared/data"
  2. 块存储(Block Storage): 将远程块设备(如云硬盘)挂载到单个节点不能跨节点共享,但性能高。需要 Pod 自己格式化和管理文件系统

    yaml 复制代码
    # ebs-sc.yaml
    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: ebs-sc
    provisioner: ebs.csi.aws.com
    volumeBindingMode: WaitForFirstConsumer

4. 4. 总结

卷(Volume)是 Kubernetes 中用于解决容器临时性文件系统问题的机制,它允许:

  • 容器重启后数据不丢失(持久化)
  • 同一 Pod 内多个容器共享数据
  • 应用与存储解耦,实现可移植
plantuml 复制代码
@startuml
' 设置样式
skinparam defaultTextAlignment center
skinparam wrapWidth 200
skinparam shadowing false
skinparam component {
  backgroundColor<<Pod>> LightBlue
  borderColor<<Pod>> #336699
  backgroundColor<<Ephemeral>> LightGreen
  borderColor<<Ephemeral>> #2E8B57
  backgroundColor<<PVC>> LightYellow
  borderColor<<PVC>> #DAA520
  backgroundColor<<Storage>> LightPink
  borderColor<<Storage>> #FF6347
  backgroundColor<<CSI>> LightGray
  borderColor<<CSI>> #696969
  backgroundColor<<Backend>> Wheat
  borderColor<<Backend>> #8B4513
}

package "Kubernetes 集群" {
  [Pod\n(应用容器)] as pod <<Pod>>
  
  package "卷定义(Pod 内)" {
    [emptyDir\n(临时卷)] as emptyDir <<Ephemeral>>
    [configMap\n(配置注入)] as configMap <<Ephemeral>>
    [secret\n(密钥注入)] as secret <<Ephemeral>>
    [persistentVolumeClaim\n(持久卷声明引用)] as pvcRef <<PVC>>
  }

  [持久卷声明(PVC)\n• 存储大小:10Gi\n• 访问模式:RWO\n• 存储类:fast-ssd] as pvc <<PVC>>

  [存储类(StorageClass)\n• 名称:fast-ssd\n• 供应器:ebs.csi.aws.com\n• 参数:类型、加密等] as sc <<Storage>>

  [持久卷(PV)\n• 容量:10Gi\n• 后端:AWS EBS / NFS / 本地磁盘\n• 节点亲和性] as pv <<Storage>>

  [CSI 存储驱动\n• Controller 服务\n• Node 服务\n• Sidecar 容器:\n  - external-provisioner\n  - node-driver-registrar] as csi <<CSI>>
}

[底层存储系统\n(AWS EBS / NFS / Ceph / 本地 SSD)] as storage <<Backend>>

' 连接关系
pod --> pvcRef : 挂载卷
pod --> emptyDir : 挂载卷
pod --> configMap : 挂载卷
pod --> secret : 挂载卷

pvcRef --> pvc : 引用

pvc --> sc : 使用存储类
sc --> csi : 触发动态供应
csi --> pv : 创建 PV 和底层存储
pvc --> pv : 绑定关系

pv --> storage : 由...提供支持

' 布局优化(隐藏连线调整位置)
pod -[hidden]d-> emptyDir
emptyDir -[hidden]r-> configMap
configMap -[hidden]r-> secret
secret -[hidden]r-> pvcRef

pvcRef -[hidden]d-> pvc
pvc -[hidden]r-> sc
sc -[hidden]r-> csi
csi -[hidden]d-> pv
pv -[hidden]d-> storage

@enduml

StorageClass、PV、PVC关系如下:

PVC →(引用)→ StorageClass →(触发创建)→ PV

资源 角色 创建者 生命周期
StorageClass 存储模板 集群管理员 集群级,长期存在
PV 实际存储资源 管理员(静态)或系统(动态) 集群级,独立于 Pod
PVC 存储申请单 应用开发者 命名空间级,绑定 PV 后长期存在

前面我们在很多地方都定义了accessModes,以下是对Access Modes做的一个总结表格:

模式 含义 支持的存储 关键说明
ReadWriteOnce 卷可以被单个节点以读写模式挂载 绝大多数存储(包括本地存储、块存储如 AWS EBS、GCP PD) 这是最常用的模式。一个节点上可以运行多个 Pod 并同时访问该卷。
ReadOnlyMany 卷可以被多个节点以只读模式挂载 NFS、CephFS 等​文件存储/共享存储 常用于需要跨多个 Pod 分发只读配置、数据或代码的场景。
ReadWriteMany 卷可以被多个节点以读写模式挂载 主要限于文件存储(如 NFS, CephFS, Azure Files) 需要多个 Pod 同时写入同一存储的场景(如内容管理系统)。
ReadWriteOncePod 卷可以被单个 Pod 以读写模式挂载 仅支持 CSI 卷,且需要 Kubernetes v1.22+ 确保卷的独占性。这是有状态工作负载的理想选择,可防止其他 Pod 误挂载。

5. 脚注

[注]

卷 | Kubernetes

[注]

5.2 secret 和 ConfigMap 卷 · Kubernetes - 痴者工良