image

在前面几个章节,介绍了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中,方便使用。

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

在这里插入图片描述

2. 临时卷

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

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

  2. configMap、secret等一类将资源文件挂载为卷:正如我们前面所提到,我们需要将某些配置文件或者私密文件作为pod容器启动或者运行参数配置,而这些配置对于每个容器都是统一的,但是在生产的过程中有可能发生变更(例如nginx的config)。这时候我们就可以定义configMap,或者secret​[注](本质上,这两者都是资源文件,当我们定义它们的时候,相关的配置文件会保存到k8s的etcd中),然后在pod运行的时候,将configMap或者secret文件挂载到容器中(一般来说,都是ready only的)。具体的使用,可以参考5.2 secret 和 ConfigMap 卷 · Kubernetes - 痴者工良[注]。举个例子:

    # 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。
# 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
    # 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,因此会调度失败。因此我们需要进行延迟绑定。

    # 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经典网络文件系统,开源、轻量、广泛支持中小规模集群,自建存储
    CephFSCeph 提供的 POSIX 兼容文件系统大规模分布式存储,高可用
    GlusterFS开源分布式文件系统(Red Hat 支持)已逐渐被 Ceph 取代
    AWS EFSAmazon Elastic File SystemAWS 上的托管 RWX 文件存储
    Azure Files微软 Azure 的 SMB/NFS 文件服务Azure 云环境
    GCP FilestoreGoogle Cloud 的托管 NFS 服务GCP 云环境

    例如,定义一个NFS PV:

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

    # 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 内多个容器共享数据
  • 应用与存储解耦,实现可移植
    在这里插入图片描述

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 - 痴者工良

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐