资讯动态

Kubernetes Volume 完全指南:从 emptyDir 到 PV/PVC 持久化存储

发布时间:2026/10/13 16:47:09 来源:尧图企业网站定制
先别急着写 YAML我强烈建议你先把 Kubernetes 里 Volume 这条线从头到尾捋一遍。很多新手学了几个月kubectl apply敲得飞快但一遇到“数据哪去了”“为什么 Pod 重建后文件消失了”就抓瞎。这一篇我会尽量讲透 Volume 的原理同时给出一套能直接抄走的实操方案覆盖emptyDir、hostPath、PV/PVC、StorageClass 动态供给这些日常最常见的场景。适合刚接触 Kubernetes 存储的人从头看也适合用过一阵子但对底层链路还模糊的同学查漏补缺。1. 容器文件系统与 Volume 的核心脉络1.1 容器一重启文件怎么就没影了要理解 Volume先得清楚容器默认的文件系统是怎么构成的。容器镜像其实是一组只读层叠起来的结果运行时会在最上面加一个可写层。你往容器里写入文件写的是这个可写层。只要容器还在这些文件就在容器一旦被删除这个可写层就跟着没了。这里有个容易混淆的细节Kubernetes 里 Pod 内的容器如果因为 OOM 或者 crash 触发了重启虽然容器进程重新拉起但旧容器的可写层在绝大多数实现里会被清掉所以之前写进容器本地路径的数据就丢了。这就好比住酒店时你穿酒店的一次性拖鞋退房后这双拖鞋会被扔掉再入住时给你换一双新的。想要跨房间保留自己的物品你得把东西放进自己带的行李箱而这个行李箱在 K8s 里就是 Volume。Volume 正是为了解决“数据不能跟着容器消亡”而出现的抽象。它被定义在 Pod 级别可以挂载到 Pod 内的一个或多个容器。这样即使容器重启卷里的数据依然存在因为卷的生命周期跟容器解耦转而跟着 Pod 走。1.2 Volume 生命周期与 Pod 的绑定关系Kubernetes 的 Volume 模型核心是一句话Volume 是 Pod 的一部分不是容器的一部分。在 Pod 的 spec 里你通过volumes字段声明卷然后在各个容器的volumeMounts里把卷挂到容器的某个目录。两者的生命周期关系可以用几句话概括Pod 创建时卷跟随 Pod 一起创建。Pod 运行期间卷里写入的数据对挂载它的所有容器可见。Pod 被删除时卷是否销毁取决于卷类型。emptyDir、configMap这类临时卷会随之清理而 PV/PVC 这类持久化卷只是解除绑定底层存储数据仍然保留。另外还要注意同一个卷可以同时挂载到多个容器挂载路径可以不同。这非常有用比如主容器写日志Sidecar 容器读日志做转发两者通过共享一个 Volume 就能配合起来。2. Kubernetes Volume 类型全景选型比写 YAML 更重要K8s 的 Volume 类型非常多但绝大多数场景下你只需要分清三类临时卷、节点本地卷、持久化声明卷。至于configMap和secret这类特殊卷很多人把它们当成“配置注入”来看但它们的挂载机制本质上也是卷。2.1 emptyDir最轻量的临时卷emptyDir是默认的临时目录卷Pod 一旦被删除目录内容也会被清空。它适合放缓存、临时文件、以及主容器与 Sidecar 之间的中间数据。下面是标准用法apiVersion: v1 kind: Pod metadata: name: emptydir-demo spec: containers: - name: app image: nginx volumeMounts: - name: cache mountPath: /cache - name: sidecar image: busybox command: [/bin/sh, -c, sleep 3600] volumeMounts: - name: cache mountPath: /shared volumes: - name: cache emptyDir: {}emptyDir默认使用节点上的磁盘空间你也可以通过medium: Memory把它挂成内存盘本质是 tmpfs。用内存盘时如果 Pod 写入的数据量超过内存限制会触发系统压力甚至导致节点问题生产环境要慎重。实操心得我会在后端服务里用emptyDir放进程产生的临时缓存因为一旦 Pod 崩了重启这些缓存本来也不该留。如果跨 Pod 重启还需要那就不属于临时卷的职责范围应该用 PVC。2.2 hostPath把节点磁盘直接暴露给 PodhostPath是把宿主机上的某个目录挂载进 Pod。它跟节点强绑定Pod 如果被调度到另一台节点数据就是另一台节点的内容了。所以它不适合做“共享持久存储”但非常适合 DaemonSet比如采集宿主机日志、读取节点系统信息。看一个例子apiVersion: v1 kind: Pod metadata: name: hostpath-demo spec: nodeSelector: kubernetes.io/hostname: node-01 containers: - name: logger image: busybox command: [/bin/sh, -c, tail -f /host/var/log/messages] volumeMounts: - name: log mountPath: /host/var/log volumes: - name: log hostPath: path: /var/log type: Directorytype的取值有Directory、DirectoryOrCreate、File、FileOrCreate等表示如果路径不存在要不要自动创建。如果不写默认不会创建路径不存在时挂载会失败。注意使用hostPath时要特别留意安全边界。让 Pod 挂载宿主机的系统目录意味着 Pod 能读写节点文件做一个有恶意行为的容器就能突破隔离。所以在生产环境一定要配合 PodSecurityPolicy 或 admission policy 限制这类高危卷的使用。2.3 PV/PVC持久化存储的两层抽象持久化存储不能只看 Pod 这一层K8s 引入了 PVPersistentVolume和 PVCPersistentVolumeClaim两个概念。PV 是集群里的存储资源由管理员预先创建或者由 StorageClass 按需动态创建。PVC 是用户发出的“存储申请单”上面写清楚我需要多大容量、什么访问模式。Kubernetes 会把 PVC 和满足条件的 PV 做绑定Pod 挂载时通过 PVC 引用 PV底层的数据就持久化下来了。概念定位谁来创建生命周期PV集群存储资源管理员或 StorageClass provisioner集群级独立于 PodPVC存储申请用户或工作负载命名空间级绑定某个 PVStorageClass动态供给模板管理员决定 PV 如何创建和回收访问模式也是一定要懂的名词ReadWriteOnce单个节点可以读写挂载。ReadOnlyMany多个节点可以同时只读挂载。ReadWriteMany多个节点可以同时读写挂载。块存储通常只支持 ReadWriteOnce文件存储如 NFS 才支持多节点读写。选型错了PVC 就一直绑不上 PV这个坑后面会把排查方法写出来。2.4 动态供给StorageClass 的作用没有 StorageClass 的时代管理员要先手动建一批 PV用户再逐个申请。动态供给的好处是用户只要建 PVCprovisioner 会自动创建一个 PV 并绑定。这个过程的幕后关键字段是storageClassName。apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-csi provisioner: nfs.csi.k8s.io parameters: server: 192.0.2.10 share: /exports/data reclaimPolicy: DeletePVC 里写上storageClassName: nfs-csi就会走动态供给。如果 PVC 里写的是storageClassName: 表示一定要手动绑定一个预创建的静态 PV不接受动态供给。这里很多人会踩坑后面会说。2.5 configMap 与 secret配置注入也是卷的一部分严格来说configMap和secret挂载进 Pod 以后也是 Volume不过内容来自 API 对象而不是真实存储介质。它们最常见的用途是把配置文件挂进容器指定目录。apiVersion: v1 kind: ConfigMap metadata: name: app-config data: app.properties: | server.port8080 log.levelINFOapiVersion: v1 kind: Pod metadata: name: configmap-via-volume spec: containers: - name: app image: nginx volumeMounts: - name: config mountPath: /etc/config readOnly: true volumes: - name: config configMap: name: app-config挂载后Pod 里看/etc/config/app.properties就能读到配置。需要注意一点挂载整个 configMap 卷时Kubelet 会定期同步同步后文件可能不会实时更新特别是用了subPath的单个文件挂载这是非常经典的坑。3. 挂载链路解析从 PVC 到 Pod 里看到文件到底发生了什么3.1 控制面决定调度Kubelet 负责挂载我们通常会以为“我写了 PVCPod 就能用”其实中间隔了好几步。Pod 被调度到某台节点后Kubelet 会启动一个 Volume Manager它才是真正负责“把卷挂进容器”的角色。整体链路大致是这样的用户创建 PVC如果带StorageClass控制器会调用 provisioner 创建底层存储资源并生成对应的 PV。用户创建 PodPod 引用 PVC。调度器在调度时会把满足数据位置的节点作为候选。调度完成后Kubelet 开始执行 volume 准备如果是需要 attach 的块设备卷先由 AttachDetachController 或 Kubelet 控制执行 attach。CSI 插件会调用存储后端接口把卷挂载到节点上的一个全局挂载目录。最后再把全局挂载目录绑定到容器内指定的mountPath。对于 NFS、CephFS 这类网络文件系统很多 CSI 驱动不需要 attach只要 mount 就行。因为你本来就通过网络访问共享目录不需要把物理磁盘挂到节点上。3.2 CSI 驱动的关键动作拆解CSIContainer Storage Interface是 K8s 与存储厂商之间的标准接口。它的核心是几个 gRPC 方法每个方法都对应卷生命周期的一个环节CSI 方法作用类比CreateVolume / DeleteVolume创建或删除存储卷买硬盘/砸硬盘ControllerPublishVolume / ControllerUnpublishVolume把卷关联到节点类似云盘 attach把硬盘插到服务器上NodeStageVolume / NodeUnstageVolume把卷挂到节点全局目录准备设备把硬盘放进服务器卡槽并接线NodePublishVolume / NodeUnpublishVolume把节点全局目录 bind mount 到容器把目录映射到容器里所以你在kubectl describe pod里看到的NodePublishVolume失败本质上是 CSI 驱动在最后一个环节没能完成 bind mount。3.3 fsGroup 与挂载传播很多存储卷需要设置属主和权限。K8s 给 Pod 设置了securityContext.fsGroup后Kubelet 会将卷的属组设为该 GID并开启 supplemental groups 权限。这在 NFS 上尤其重要因为 NFS 的访问权限依赖服务端配置容器内进程的 UID/GID 决定了以什么身份访问。挂载传播指的是宿主机挂载点与容器挂载点之间的可见性关系。默认是None也就是容器里新创建的挂载点宿主机看不见HostToContainer能让宿主机的新挂载在容器内可见Bidirectional则双向传播。日常业务用得不多但在做特权系统、需要容器内再挂载文件系统时会遇到。4. 四个可直接复制的 Volume 实战场景这一部分会比较长但我保证每个 YAML 和命令都是校验过的你可以直接复制到测试集群里跑。4.1 场景一emptyDir 让两个容器共享数据这个场景模拟的是主容器生成数据、Sidecar 消费数据的经典结构。我会用 nginx 作为主容器用一个 busybox sidecar 往共享目录里写静态页面让 nginx 直接对外提供服务。先创建 Podkubectl apply -f - EOF apiVersion: v1 kind: Pod metadata: name: shared-files spec: containers: - name: nginx image: nginx ports: - containerPort: 80 volumeMounts: - name: data mountPath: /usr/share/nginx/html - name: writer image: busybox command: [/bin/sh, -c, echo Hello from emptyDir /data/index.html sleep 3600] volumeMounts: - name: data mountPath: /data volumes: - name: data emptyDir: {} EOF验证一看两个容器是否能看见同一个文件。kubectl exec shared-files -c nginx -- cat /usr/share/nginx/html/index.html kubectl exec shared-files -c writer -- cat /data/index.html两条命令都会输出Hello from emptyDir说明共享卷生效。验证二让 nginx 容器原地重启再看数据是否还在。kubectl exec shared-files -c nginx -- kill 1过几秒再看kubectl exec shared-files -c nginx -- cat /usr/share/nginx/html/index.html文件还在因为emptyDir的生命周期是 Pod 级容器重启不影响卷内容。但如果kubectl delete pod shared-files这个目录就没了。4.2 场景二hostPath 做单节点持久化如果你就是想让 Pod 在同一个节点重建之后数据还在而且不要求跨节点hostPath可以省掉不少 PV/PVC 的绕路。kubectl apply -f - EOF apiVersion: v1 kind: Pod metadata: name: hostpath-data spec: nodeName: node-01 containers: - name: writer image: busybox command: [/bin/sh, -c, echo node local data /mnt/persist/data.txt sleep 3600] volumeMounts: - name: local mountPath: /mnt/persist volumes: - name: local hostPath: path: /opt/k8s-data type: DirectoryOrCreate EOF这里我用nodeName固定调度到 node-01因为hostPath本身不提供跨节点迁移能力这是演示必须注意的。验证过程分三步进入容器确认文件写入成功。到 node-01 上检查/opt/k8s-data/data.txt能看到node local data。删除 Pod 再重新创建挂载同一个宿主机路径数据依然存在。如果你不小心把 Pod 调度到了 node-02那么 Pod 看到的会是 node-02 的/opt/k8s-data大概率是空目录这也是hostPath明明用了却觉得“数据丢了”的常见原因。4.3 场景三NFS 静态 PV/PVC 实现跨节点共享持久存储现在做正儿八经的持久化。假设你已经有一台 NFS 服务端IP 是192.0.2.10共享目录是/exports/data允许 K8s 节点访问。为了讲解干净我用静态供给的方式先创建 PV再创建 PVC最后创建 Pod。第一步创建 PVapiVersion: v1 kind: PersistentVolume metadata: name: nfs-pv spec: capacity: storage: 5Gi accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain nfs: server: 192.0.2.10 path: /exports/data第二步创建 PVCapiVersion: v1 kind: PersistentVolumeClaim metadata: name: nfs-pvc spec: accessModes: - ReadWriteMany resources: requests: storage: 5Gi volumeName: nfs-pv这里有个关键点PVC 写不写storageClassName会影响 PV 绑定。上面的 PVC 没有写storageClassName它在默认 StorageClass 存在时会尝试动态供给如果集群有默认 StorageClass静态 PV 可能绑定不上。为了让静态 PVC 稳当绑定最好明确写storageClassName: spec: storageClassName: 这样 K8s 不会走动态供给只会去找匹配的静态 PV。第三步创建 Pod 并挂载 PVCapiVersion: v1 kind: Pod metadata: name: nfs-pvc-pod spec: containers: - name: app image: busybox command: [/bin/sh, -c, echo persistent from NFS /data/hello.txt sleep 3600] volumeMounts: - name: nfs mountPath: /data volumes: - name: nfs persistentVolumeClaim: claimName: nfs-pvc验证绑定状态kubectl get pv kubectl get pvc kubectl describe pod nfs-pvc-podPV和PVC都应该是BoundPod 也进入 Running。然后在任意一个可以访问 NFS 的地方都能看到/exports/data/hello.txt里的内容。删除 Pod 不影响 NFS 数据重新创建后数据还在。这个场景就是生产环境最常见的持久化姿势。4.4 场景四用 StorageClass 动态申请持久卷动态供给省去了管理员手动创建 PV 的工作量。以 NFS CSI 驱动为例首先确保你已经部署好对应的 CSI 插件和驱动然后创建 StorageClass。apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-dynamic provisioner: nfs.csi.k8s.io parameters: server: 192.0.2.10 share: /exports/dynamic reclaimPolicy: Delete接着只需要创建一个 PVC不用手动创建 PVapiVersion: v1 kind: PersistentVolumeClaim metadata: name: dynamic-pvc spec: accessModes: - ReadWriteMany storageClassName: nfs-dynamic resources: requests: storage: 2Gi执行后观察自动创建的 PVkubectl get pv kubectl get pvc dynamic-pvc你会发现系统自动生成了一个名字类似pvc-xxxxxxxx的 PV并且状态变成Bound。这就是动态供给的完整链路provisioner 收到 PVC 请求后在存储后端创建卷再生成 PV 对象最后与 PVC 绑定。注意动态供给下删除 PVC 后如果reclaimPolicy是Delete对应的 PV 和底层存储卷会被自动删除。万一里面还有重要数据这波操作就真的“删库跑路”了。建议对重要存储改用Retain这样即使 PVC 被删PV 也会保留需要人工确认后清理。5. 排查实录Volume 相关高频故障与解决思路5.1 Pod 卡在 ContainerCreating这是最常见的问题。执行kubectl describe pod pod-name看 Events 里最后一行的报错。典型的报错包括PersistentVolumeClaim is not boundPVC 没有 Bound先检查 PV 和 PVC 的容量、accessModes、storageClassName 是否匹配。MountVolume.MountDevice failed for volume ...CSI 驱动挂载失败检查 NFS 服务端路径是否存在、网络是否通、NAS 设备的白名单是否包含当前节点。Failed to create directory ... permission denied权限问题检查目录属主或 fsGroup。排查顺序应该固定先看 PVC 状态再看事件中的存储错误最后看节点本地目录权限。实操心得我见过一个很隐蔽的坑PVC 声明ReadWriteMany但底层使用的 CSI 驱动其实只支持ReadWriteOnce导致 PV 无法绑定。这种问题通过kubectl get pv看 PV 的 accessModes 一眼就能发现。静态 PV 与 PVC 的 accessModes 必须完全匹配。5.2 数据一删 Pod 就“丢”了如果你确认用了 PVC但删除 Pod 后数据还是没了先从这几点入手确认 PVC 是持久化类型还是临时卷。有人把 PV 挂载路径写错了实际写入的目录是emptyDir。确认 PVC 是否真的 Bound而不是用了hostPath或者emptyDir兜底。确认reclaimPolicy。如果 PVC 被删除策略为Delete时底层数据会被清理策略为Retain时 PV 还在但也不能直接复用要人工处理后才能再次绑定。还有一个经常被忽略的问题Pod 被驱逐后调度到新节点时如果底层是hostPath数据只在原节点新节点看不到。这种情况“看起来像丢了”实际上只有换回原节点才能恢复。生产环境必须用 PVC 或共享文件系统来解决。5.3 NFS 挂载后容器内写权限报错NFS 的权限不完全看 Linux 本地权限还要看服务端导出配置和客户端用户 ID。如果容器内的进程以nobody身份运行写文件时会受 NFS 服务端的 anonuid/anongid 限制。解决方式有几种在服务端把导出目录的权限调成允许匿名用户读写。给 Pod 的securityContext设置fsGroup让卷被挂进容器后有一个指定的 GID。或者在 NFS 服务端配置squash映射将客户端 root 映射到特定 UID。示例securityContext: fsGroup: 2000 runAsUser: 1000这一般能解决大多数权限问题。5.4 ConfigMap 更新后容器里的文件没有变configMap卷默认由 kubelet 周期同步但更新不是秒级生效。如果你通过kubectl edit configmap修改数据Pod 内挂载的文件通常要几十秒甚至更久才会刷新。还有一个更隐晦的坑如果使用subPath挂载单个文件更新不会写入已经挂载的文件因为 bind mount 的是文件本身而不是目录。规避手段是尽量避免用subPath挂载 configMap。需要实时更新的配置考虑用控制平面主动rollout restart让 Pod 重建重新挂载。或者使用支持热更新的配置文件方案比如让应用去监听 API Server 的变更。5.5 PV 删除不掉或者一直 TerminatingPV 有 Finalizer底层 CSI 驱动没执行成功时PV 会卡在Terminating。这时候先看 describedkubectl describe pv pv-nameEvents 里通常有Unable to remove volume from node或者Volume is attached这类信息。检查 Pod 是否还在使用这个 PV如果有先删除 Pod再确认 CSI 驱动 Pod 是否健康最后如果确认底层卷已经不存在可以手动清理 Finalizer但这是高危操作一定先确认数据已经不需要。实操心得清理 Finalizer 是我处理异常 PV 的兜底方案但我不建议新手上来就kubectl patch pv ... -p {metadata:{finalizers:[]}}。一旦误删对象释放但底层存储还挂着之后想恢复非常麻烦。先让事件说话多数问题在删除关联 Pod 后会自动恢复。最后再说一个我踩过好几次的坑不要不加思考就给 PVC 写storageClassName: 然后又没准备好静态 PV。这个写法等于告诉 K8s“我要手动绑定不接受动态创建”如果 PV 没提前建好Pod 就会一直卡住。反过来如果你写了自定义 StorageClass但 provisioner 没部署好同样会卡在 PVC Pending。先想清楚你想用哪种方式再决定字段怎么写。围绕存储的 K8s 故障绝大多数不是网络问题也不是驱动问题而是概念没对齐导致的配置矛盾。把 PV/PVC/StorageClass 的关系吃透你后面会省下大量排查时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑