K8s存储原理与实战:从PV、PVC到StorageClass和NFS动态供给

发布时间:2026/10/3 3:27:47
K8s存储原理与实战:从PV、PVC到StorageClass和NFS动态供给 1. 先把概念理清楚这四个东西各管什么做 K8s 的人迟早会被存储这套东西折腾一遍。刚开始接触到 PV、PVC、StorageClass 这仨缩写的时候大多数教程给出的解释都是那种“正确的废话”PV 是持久化卷PVC 是持久化卷声明。看完文档你依然不知道它们之间到底怎么配合工作更不知道 NFS 在其中扮演什么角色。我最初也是在这个地方栽了几个跟头后来在测试环境里把 NFS StorageClass 的链路完整跑了一遍顺带解决了若干次故障才算真正把这块吃透。先说结论PV、PVC、StorageClass、NFS 这四个东西并不是并列关系而是分工不同、处在不同层级的组件。你可以这样理解——NFS 负责提供真实的存储空间PV 是对这些存储空间的抽象描述PVC 是应用提出的存储需求StorageClass 则是用来让 PV 无需人工干预、按需自动生成的调度机制。1.1 PV集群里的“存储仓库”PVPersistentVolume是集群层面的资源由管理员预先准备好或者由 StorageClass 动态创建。它描述的是“集群里有这么一块存储可以用来挂载”这块存储的底层实现可以是 NFS、Ceph RBD、云厂商云盘、本地磁盘等。PV 听起来很抽象但你可以把它想做一间已经租好的仓库。仓库本身是客观存在的它有固定的面积有确定的出入口条件至于仓库将来放什么货是由使用方决定的。PV 在 K8s 里就承担这个角色。一个 PV 的 YAML 大概长这样apiVersion: v1 kind: PersistentVolume metadata: name: nfs-pv spec: capacity: storage: 10Gi accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain nfs: server: 192.168.1.100 path: /data/k8s-nfs字段不多但要注意几个细节。capacity.storage表示容量accessModes描述访问模式NFS 支持的ReadWriteMany多节点同时读写是其一大卖点。persistentVolumeReclaimPolicy决定 PV 被释放后的处理策略Retain表示保留数据Delete表示删除存储Recycle基本已被废弃。注意如果你在 YAML 里手动指定了nfs.server和nfs.path那这个 PV 的底层存储就是固定的。后期如果要迁移数据或扩容会有点麻烦。这一点恰恰是 StorageClass 动态供给要解决的痛点之一。1.2 PVC应用发起的“存储申请单”PVCPersistentVolumeClaim是应用层面提的请求。它不关心底层存储具体是什么只描述应用需要什么规格的存储比如容量多大、访问模式是什么、要不要某个 StorageClass 来动态分配。用仓库来类比PVC 就是“我要租 5 平方米要能让我随时搬货进去”的申请单。K8s 里的调度器会根据申请单上的条件去找合适的 PV 完成绑定这个过程是自动的前提是集群里有满足条件的 PV。PVC 最常见的 YAMLapiVersion: v1 kind: PersistentVolumeClaim metadata: name: app-data spec: accessModes: - ReadWriteMany storageClassName: nfs-storage resources: requests: storage: 5GiPVC 创建后K8s 会尝试找一个 PV 与它绑定。绑定条件包括存储容量是否足够、访问模式是否匹配、storageClassName 是否一致。如果找得到PVC 的状态会从Pending变成Bound如果找不到它会一直卡在Pending。这里有个新手容易踩的坑PVC 一旦绑定 PV就是一对一的关系一个 PVC 不能同时绑多个 PV一个 PV 通常也只服务一个 PVC。如果应用需要共享同一份数据应该让多个 Pod 挂载同一个 PVC而不是试图让 PVC 去“复用”多个 PV。1.3 StorageClass把“手动分配”变成“按需生成”StorageClass 的核心价值是存储的动态供给。在没有它之前管理员得预先手工创建好一批 PV用户创建 PVC 时从里面挑一个来绑定。这种做法的问题是PV 数量固定、规格有限PVC 提出 5Gi 的需求结果你给我预留的是 10Gi 的浪费且不灵活。有了 StorageClass 之后用户创建 PVC 时只需要声明storageClassName: nfs-storageK8s 就会调用对应的Provisioner 插件在 PVC 创建时自动生成一个 PV 并绑定。整个过程不需要管理员提前创建任何 PV。StorageClass 的 YAML 大致是apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-storage provisioner: k8s-sigs.io/nfs-subdir-external-provisioner parameters: archiveOnDelete: true onDelete: retain关键字段是provisioner它决定了 K8s 调用什么后端插件来创建 PV。常用的 Provisioner 非常多NFS 有 nfs-subdir-external-provisioner云盘有云厂商对应的插件Ceph 有 csi-cephfs / csi-rbd。用错了 ProvisionerPVC 就永远绑定不上 PV这是实际中最常见的问题之一。还有一个容易忽略的细节StorageClass 并不是创建了就能自动生效它依赖对应的 Provisioner 以 Pod 或 DaemonSet 的形式在集群中运行。所以写完 StorageClass 之后第一件事是确认 Provisioner 的 Pod 处于 Running 状态。1.4 NFS真正干脏活累活的后端NFSNetwork File System是最成熟、最通用的网络文件共享协议之一。它能把一台机器上的目录通过网络共享给其他机器挂载使用。K8s 的节点无论调度到哪一台机器都能通过 NFS 客户端挂载同一个远端目录实现跨节点的共享存储。NFS 在 K8s 存储方案里的地位有点像“万金油”它不像云厂商云盘那样受平台绑定的限制也不像 Ceph 那样需要复杂的部署和运维。只要网络通装一个 nfs-common 就能挂载。在私有化部署、机房自建集群的场景里NFS 几乎是零门槛的存储方案。不过 NFS 也有自己的短板性能上限有限单个 NFS 服务端容易成为瓶颈存在单点故障风险如果 NFS 服务端挂了所有挂载它的应用都会受影响。生产环境如果预算充足、业务压力大建议用 Ceph RBD 或者云盘替代但对中小规模集群、开发测试环境来说NFS 完全够用。我在实际项目里的选择逻辑很简单开发环境和压力不大的内部系统用 NFS核心业务业务数据库、需要高 IOPS 的场景才动用云盘或分布式存储。后者的运维复杂度一般团队撑不太住。2. 为什么要绕这么一圈直接挂 NFS 不就行了这是很多人刚接触这套组合时的第一反应。既然 NFS 能提供存储Pod 里直接写nfs卷不就行了为什么要额外引入 PV、PVC、StorageClass 这些概念确实如果你只是几个 Pod 临时使用一个固定目录直接挂 NFS 是最快的方案。K8s 原生支持这种用法apiVersion: v1 kind: Pod metadata: name: nginx-direct-nfs spec: containers: - name: nginx image: nginx:1.25 volumeMounts: - name: nfs-data mountPath: /usr/share/nginx/html volumes: - name: nfs-data nfs: server: 192.168.1.100 path: /data/k8s-nfs这样写确实可以直接跑但问题也随之而来。2.1 直接挂 NFS 卷的四个痛第一存储细节和 Pod 耦合。如果你有十个应用需要挂载 NFS就得在十个 YAML 里重复写 NFS 服务端地址和路径。哪天 NFS 服务器的 IP 变了或者迁移了目录你就要改所有涉及的 YAML逐个重新发布。第二容量没法管理。没有 PV 的容量抽象你想限制某个应用只能使用多少 G根本做不到。一个应用写满一个分区其他应用全部遭殃这是真实发生过的事故而不是理论上的担心。第三权限和回收策略失控。PV 层面的ReclaimPolicy、挂载参数、访问模式这些能力直接写 NFS 卷时都体现不出来。数据被删除后能不能保留应用被销毁后目录要不要清理这些需求统统没有统一入口。第四资源审计和 YAML 规范化无从谈起。集群规模一大谁在用哪个目录、用了多少容量没有 PV/PVC 这套抽象管理员根本没法从集群层面回答这些最基本的运维问题。2.2 PV PVC 把存储和应用解耦PV/PVC 这套抽象的核心价值是让应用开发者不需要关心存储到底在哪里、底层的实现是什么。应用只提 PVC 需求剩下的交给集群管理员和维护者去处理。这种“声明式”的设计正是 K8s 哲学的体现你描述想要的状态系统负责让实际状态向期望状态收敛。应用开发者写 YAML 时只需要volumes: - name: data persistentVolumeClaim: claimName: app-data剩下的什么 NFS 服务器地址、路径、容量规格都是集群管理员该操心的事。开发环境的 PV 可以接 NFS生产环境的 PV 可以换云盘应用侧代码和 YAML 完全不用动。这种解耦在微服务团队协作的场景里价值极大。2.3 StorageClass 把静态供给推进到动态供给如果没有 StorageClass管理员创建 PV 的过程通常是一次性完成一批之后很难根据实际需求灵活调整。用户提一个 3Gi 的 PVC管理员手头只有 5Gi 和 10Gi 的 PV只能将就用大的资源浪费是普适现象。StorageClass 出现后PVC 创建时就直接触发 Provisioner 创建匹配的 PV。这是一个“按需分配”的模式容量精确匹配免去了人工干预。你告诉 StorageClass 说“我要 5Gi、可多机读写”系统就真的创建一个恰好 5Gi 的 PV 给你。这个机制的额外好处是标准化的命名结构。以 nfs-subdir-external-provisioner 为例它可以按命名空间和 PVC 名自动生成 NFS 子目录例如/data/k8s-nfs/namespace/pvc-name/这样即使有几十个应用同时使用 NFS目录结构依然清晰可查不会出现几个应用写同一个目录互相覆盖的情况。2.4 存储后端选型NFS 的强项和边界NFS 不是唯一选择甚至不是性能最优的选择。但在 K8s 存储后端选型中它是最“零门槛”的一个。我用一个表格来对比常见后端方案都是实际中经常要做的权衡存储方案优点缺点适用场景NFS部署简单、跨节点共享、无平台依赖性能上限有限、有单点风险中小集群、开发测试、内部系统Ceph RBD分布式高可用、性能好运维复杂、需要专门团队维护生产环境、大规模集群云厂商云盘高可用、性能稳定、按量计费平台锁定、跨云迁移麻烦云上集群Local PV性能极高、IO 路径短数据绑定到节点、不能跨节点共享高性能本地存储场景结合实际经验我给新手的建议是如果集群规模在几十个节点以内内部系统居多上一套 NFS 存储方案完全够用。如果已经开始跑核心交易数据或需要大规模并发写再考虑 Ceph RBD 或云盘也不迟。方案选型从来不追求最先进而是追求在当前规模下代价最小、收益最大。3. 实操从零把 NFS StorageClass PVC 跑起来前面把概念掰开揉碎讲清楚了下面进入动手环节。我将用一套完整步骤从环境准备开始到应用验证数据持久化结束。这套流程我在测试环境和生产环境都验证过照着走基本不会出大问题。3.1 环境准备集群和存储机实际操作前先明确角色分工。我这里说的环境是一个常见的三节点 K8s 集群一个控制节点、两个工作节点外加一台独立的 NFS 存储服务器。存储服务器也可以复用集群里的某一台机器但生产环境建议独立出来把数据存储和控制面/应用节点分开风险更低。这边用到的系统是 Ubuntu 22.04 或 Rocky Linux 均可。K8s 版本是 1.28 以上。节点的初始化这里不展开讲重点说明存储链路。需要提前确认的两件事集群所有节点的kubelet和容器运行时都正常kubectl get nodes能看到所有节点处于Ready状态。NFS 服务器的 IP 要固定并且能通过内网与所有节点通信因为生产环境迁移 IP 是很痛苦的事情。3.2 在存储机上配置 NFS 服务端登录存储服务器安装 NFS 服务端软件# Ubuntu/Debian sudo apt update sudo apt install -y nfs-kernel-server # Rocky/CentOS # sudo dnf install -y nfs-utils安装完后创建一个共享目录我习惯用/data/k8s-nfs这种语义明确的路径sudo mkdir -p /data/k8s-nfs sudo chown nobody:nogroup /data/k8s-nfs sudo chmod 777 /data/k8s-nfs目录权限这一步非常关键。K8s 里的 Pod 是以容器内的用户身份访问挂载目录的容器的 root 用户映射到宿主机上往往不是真实 root涉及用户命名空间所以如果目录权限太严容器里写数据会直接报 Permission denied。测试环境图省事可以chmod 777生产环境建议按实际用户和权限模型来后面我会再说优化方案。然后编辑/etc/exports文件添加共享规则sudo vim /etc/exports写入/data/k8s-nfs *(rw,sync,no_subtree_check,no_root_squash)这行配置的含义rw允许读写。sync同步写入数据更可靠牺牲一点性能。no_subtree_check禁用子树检查避免某些场景下的文件访问问题。no_root_squash这个参数最需要留意。root_squash是 NFS 的默认行为它会把来自客户端的 root 用户映射成匿名用户通常是nobody从而限制 root 在 NFS 上的权限。K8s 容器里的进程默认以 root 用户运行时如果开启root_squash容器内对挂载目录的写入会被拒绝或出现权限错乱。测试阶段建议先用no_root_squash把链路跑通生产环境再结合专门的权限方案收紧。保存后重启 NFS 服务并验证导出sudo systemctl restart nfs-kernel-server sudo exportfs -rv此时在存储机上执行sudo exportfs -v应该能看到对应的导出记录。3.3 在节点上安装 NFS 客户端NFS 服务端配好了K8s 的每个节点还需要安装 NFS 客户端工具否则节点无法挂载 NFS 卷# Ubuntu/Debian sudo apt install -y nfs-common # Rocky/CentOS # sudo dnf install -y nfs-utils安装完可以用showmount命令验证连通性showmount -e 192.168.1.100如果能列出/data/k8s-nfs说明节点和 NFS 服务器之间的网络和 RPC 服务都正常。注意所有节点都要装客户端。如果漏了某一台调度到那台节点上的 Pod 会一直处于ContainerCreating看事件报错就是node has no NFS client之类的原因。3.4 部署外部 Provisioner动态供给的核心StorageClass 本身只是一个“声明”真正干活的是 Provisioner 插件。NFS 场景下我推荐使用nfs-subdir-external-provisioner这是 Kubernetes SIGs 维护的开源项目旧的nfs-client-provisioner已经移入归档新项目建议直接用 subdir 版本。用 Helm 安装是最省事的helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/ helm repo update helm install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \ --set nfs.server192.168.1.100 \ --set nfs.path/data/k8s-nfs \ --set storageClass.namenfs-storage \ --set storageClass.provisionerk8s-sigs.io/nfs-subdir-external-provisioner \ --set storageClass.archiveOnDeletetrue几个关键参数nfs.server是 NFS 服务器的 IP我这里演示用的是192.168.1.100实际替换成你的。nfs.path是 NFS 导出的目录。storageClass.name是创建的 StorageClass 名称后面 PVC 里要用它来引用。storageClass.provisioner是 Provisioner 标识必须和 StorageClass 里的provisioner字段一致。archiveOnDelete表示删除 PVC 时是否在 NFS 服务器上归档数据而不是直接删除目录。这是个很有用的保护开关建议开启。安装完成后验证kubectl get pod -l app.kubernetes.io/namenfs-subdir-external-provisioner kubectl get storageclassProvisioner Pod 是Running状态、StorageClass 列表里有nfs-storage说明动态供给中枢已经就绪。3.5 创建 StorageClass 并理解参数用 Helm 安装时其实已经自动创建了 StorageClass但为了说清楚细节我们看手动 YAML 版本apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-storage provisioner: k8s-sigs.io/nfs-subdir-external-provisioner parameters: archiveOnDelete: true onDelete: retain reclaimPolicy: Retain allowVolumeExpansion: true这里我要特别强调allowVolumeExpansion: true这个字段允许后续扩容 PVC 的容量。NFS 是文件系统扩容相对简单配了这个选项能省去以后不少麻烦。但要注意扩容操作不是所有 PV 类型都支持得看后端的实现能力。archiveOnDelete和onDelete的组合行为是删除 PVC 时对应的 NFS 子目录会被改名为带时间戳的归档目录而不是直接删除。这份数据保护策略在误删 PVC 后能救回全量数据我遇到过不止一次。3.6 用 PVC 验证动态供给存储侧配置完成后创建一个测试 PVC 来验证整条链路apiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-pvc spec: accessModes: - ReadWriteMany storageClassName: nfs-storage resources: requests: storage: 1Gi创建并查看状态kubectl apply -f test-pvc.yaml kubectl get pvc正常情况下几秒后 PVC 状态会从Pending变成Bound同时kubectl get pv会看到一个名字类似pvc-xxx的动态 PV 被创建出来这个 PV 的 StorageClass 就是nfs-storage。这一步遇到Pending的情况很常见具体排查方法我放在第 4 节。现在只要看到Bound说明 Provisioner 成功在 NFS 服务器上创建了对应的子目录并且 PV 已经就绪。到这一步你可以去 NFS 服务器上看一眼目录结构ls -l /data/k8s-nfs/正常情况下会看到一个以命名空间/PVC 名为结构的子目录比如default/test-pvc/。这个自动生成的目录结构就是动态供给最直观的体现。3.7 用真实应用验证数据持久化PVC 创建好之后用一个实际负载验证数据的持久性。我习惯用 Nginx 来测简单直观apiVersion: apps/v1 kind: Deployment metadata: name: nginx-storage-test spec: replicas: 1 selector: matchLabels: app: nginx-storage-test template: metadata: labels: app: nginx-storage-test spec: containers: - name: nginx image: nginx:1.25 volumeMounts: - name: data mountPath: /usr/share/nginx/html volumes: - name: data persistentVolumeClaim: claimName: test-pvc创建后进入容器写一个测试文件kubectl exec -it deploy/nginx-storage-test -- bash cd /usr/share/nginx/html echo hello from nfs hello.txt exit然后删除这个 Deploymentkubectl delete deploy nginx-storage-test再去 NFS 服务器上确认cat /data/k8s-nfs/default/test-pvc/hello.txt文件还在说明数据确实写到 NFS 服务器而不是容器本地磁盘。最后重新创建同一个 Deployment进入容器执行cat /usr/share/nginx/html/hello.txt文件依然存在这就验证了跨 Pod 生命周期持久化的核心能力。这一步验证的意义在于证明数据不依赖 Pod 的生命周期。很多新人以为“持久化”就是把数据放到了容器里其实那叫 John 一无所获。只有当 Pod 被删掉、重建后数据还能恢复到原来挂载点上这才叫真正的持久化。4. 常见故障与排查实录从 Pending 到 Permission denied跑存储链路的过程中我遇到过不少问题很多都是反复折腾才找到原因。这里整理几个出现频率高、排查路径清晰的故障其中有一部分正是大家经常在网上搜到的场景。4.1 集群初始化失败“the api server is not healthy”排查这是 K8s 运维中扔进搜索引擎频率最高的报错之一控制节点执行kubeadm init后卡住最后提示[kubelet-check] The HTTP call equal to https://master-ip:6443/healthz failed with error: Get https://master-ip:6443/healthz: dial tcp master-ip:6443: connect: connection refused ... the api server is not healthy after 4m0.00747357s这个问题发生的位置在集群搭建阶段虽然表面和存储无关但集群都起不来后面什么 PV、PVC 都无从谈起。我优先把这个高频故障放进来。这个报错最核心的原因通常是两类第一类kubelet 没启动起来或者启动后异常。排查方式很直接systemctl status kubelet journalctl -u kubelet -f如果 kubelet 挂了kubeadm init就没法完成健康检查。常见原因包括系统时间不同步、swap 没关闭、缺少依赖包。第二类容器运行时和 kubelet 的配置不一致。尤其是 cgroup 驱动。K8s 1.28 之后容器运行时用 containerd 居多如果 kubelet 的 cgroup 驱动是systemd而 containerd 配置的也是systemd那就没问题如果其中一个没配置就会出现 API server 容器起不来但 kubelet 日志又特别干净的情况。排查命令crictl ps -a | grep kube-apiserver crictl logs container-id如果 apiserver 容器是不断重启的状态先看它的日志kube-apiserver的日志会直接告诉你问题出在证书、etcd 还是网络层面。还有个隐藏问题如果你的机器重启过多次并且每次初始化都用了不同的--hostname参数或者更换过 IP旧的 etcd 数据和证书残留会导致新集群起不来。这种情况最干净的解法是sudo kubeadm reset -f sudo rm -rf /etc/kubernetes /var/lib/etcd然后重新kubeadm init。这里想提醒一点遇到重启集群报错第一反应先查日志拿事实不要盲目重置。但反复确认为环境残留问题时重置是最高效的方案。4.2 PVC 一直卡在 Pending 状态存储链路中PVC 卡 Pending 是上演频率最高的一出戏。创建 PVC 后kubectl get pvc一直显示Pending排查入口其实很标准kubectl describe pvc test-pvcdescribe输出里会带上事件信息常见原因有四类原因一StorageClass 不存在或名字拼错。检查 PVC 里的storageClassName是否和kubectl get storageclass输出一致。例子里是nfs-storage一旦拼错就找不到对应的 StorageClassPV 永远不会被动态创建。原因二Provisioner 没有运行。如果 StorageClass 写的是nfs-storage但对应的 Provisioner Pod 处于CrashLoopBackOff或压根没创建PVC 就会一直 Pending。此时看 Provisioner 的日志kubectl logs -n default provisioner-pod-name常见的失败日志是连接 NFS 服务器超时或者没有权限创建子目录。前者是网络不通后者是 NFS 服务端配置的权限问题。原因三NFS 服务器的路径不可写。动态供给需要在 NFS 共享目录下创建子目录如果共享目录权限不够Provisioner 创建目录失败PVC 就会死等。解决方式前面说过测试环境把/data/k8s-nfs权限调成 777或者理顺用户映射关系。原因四容量或访问模式不匹配。如果 PVC 请求 10Gi而动态供给无法创建这么大容量的 PV比如 NFS 磁盘空间只有 5Gi或者访问模式不在 StorageClass 支持的范围内也会一直 Pending。这些情况在describe事件里基本能看到明确的报错字段。4.3 挂载成功后容器里没有写权限PVC 状态已经BoundPod 也正常创建了但一进去写文件就报Permission denied。这个问题我遇到过不止一次这也是直接使用 NFS 卷最让人抓狂的地方。问题根源几乎都出在root_squash和容器用户映射上。默认情况下 NFS 开启root_squash来自客户端的 root 用户会被压制成匿名用户nobody:nogroup而挂载的目录如果属于nobody:nogroup权限够的话是可以写的但如果目录属于某个对nobody不可写的用户容器里的 root 用户就会栽跟头。基于我自己使用经验处理顺序是这样的先检查容器内是以什么用户运行进程。kubectl exec进容器执行id。如果是 root看 NFS 服务端的 exports 是否开了no_root_squash。如果不想开no_root_squash就得让 NFS 目录的属主和权限适配容器内用户。比如容器内进程以 uid 1000 运行那在 NFS 共享目录上也要创建 uid 1000 的属主并赋予相应用户写权限。测试环境最省事的做法是chmod 777 /data/k8s-nfs但生产环境不要这么干应该用规范的用户映射来配。还有一个隐藏坑K8s 的 Pod 默认开启了SecurityContext如果 YAML 里定义了runAsUser为非 root 用户但 NFS 目录权限是 700 且属主不是这个 uid同样会报权限错。这类问题核心是确认 UID 的映射关系。4.4 应用写入慢、文件消失NFS 调优与使用注意NFS 用久了常见的问题除了权限就是性能。一套从零搭建的 NFS 默认配置在并发写入稍大的场景下会有可见的延迟尤其是在网络环境一般、NFS 服务端磁盘是机械盘的时候。我在实际使用中做过的几个有效调优第一调整 PVC/PV 的 mountOptions。比如在 StorageClass 的parameters中设置parameters: archiveOnDelete: true onDelete: retain mountOptions: - nfsvers4 - actimeo2 - noatime - hardnfsvers4指定使用 NFSv4性能优于默认配置actimeo2能减少属性缓存的时延让文件可见性更实时noatime避免每次读文件都更新访问时间减少元数据开销。这些参数针对中小规模场景基本够用。第二避免大量小文件。NFS 对小文件频繁读写的性能很一般因为每次操作都涉及网络 RPC 和元数据更新。如果应用是大量小文件场景建议依赖内置对象的存储比如 Ceph而不是 NFS。第三建立目录归档和备份机制。NFS 目录一旦写满所有依赖它的应用都会出现 IO 卡死。建议定时监控 NFS 服务器的磁盘空间并做目录配额管理。4.5 常见问题速查表为了便于日常工作直接参考我把上述问题整理成一个速查表现象可能原因排查命令/手段常规解法kubeadm init报 api server not healthykubelet 异常 / cgroup 驱动不一致 / 环境残留journalctl -u kubelet -fcrictl logs修 kubelet 配置必要时kubeadm reset后重试PVC 一直 PendingStorageClass 未匹配 / Provisioner 故障 / NFS 不可达 / 权限不足kubectl describe pvc关注事件消息修正 storageClassName看 provisioner 日志Pod 创建卡在 ContainerCreatingNFS 客户端未装 / 网络不通showmount -e nfs-ip节点安装 nfs-common / nfs-utils容器写文件 Permission deniedroot_squash / 目录属主不匹配id查看容器用户及 NFS 目录属主配置 exports 加no_root_squash或调整属主权限写入慢、IO 卡顿NFS 默认挂载参数欠佳 / 后端磁盘弱mount查看实际挂载参数加 nfsvers4, actimeo2, noatime 等删除 PVC 后数据被清空ReclaimPolicy 配置不当kubectl get pv -o yaml设置Retain开启archiveOnDelete扩容 PVC 失败StorageClass 未开启 allowVolumeExpansionkubectl edit storageclass补上allowVolumeExpansion: trueProvisioner Pod CrashLoopBackOffNFS 目录无权限 / NFS 地址不对看 pod 日志检查 NFS 路径、目录权限和网络连通性上面这张表我建议直接保存到自己的运维笔记里。平时线上告警第一件事不是翻文档而是对照现象快速定位可能原因再用对应命令排出问题这能大幅缩短故障处理时间。5. 一些实际操作中的体会整套 PV/PVC/StorageClass/NFS 链路跑下来我最大的感受是K8s 的存储抽象设计得很漂亮但这份漂亮在国际上不少教程里被讲得像天书。真正理解这套结构动手从 NFS 开始搭是最快的路径因为 NFS 不像云盘那样有平台依赖也不像 Ceph 那样运维成本高你在一台普通服务器上就能把整条链路从头到尾摸一遍。再多说一个小技巧给应用做 PVC 时容量规格千万别拍脑袋写。最合理的做法是对应用的日志增长速率和历史数据量做一周的观测然后乘以 1.5 到 2 的冗余系数。这么做不是因为 K8s 不支持扩容而是因为 PVC 扩容虽然能配但 NFS 后端一旦子目录所在磁盘满了扩容照样失败预留足够的容量能省去很多不必要的运维烦恼。另外如果你在测试环境把这套 NFS 动态供给跑通了生产环境迁移时千万别把 NFS 的目录原封不动搬过去。生产环境的共享目录、归档策略、权限模型都要重新梳理一遍哪怕底层还是 NFS目录权限和回滚机制也得专门设计这是区分“能用”和“好用”的分水岭。按我现在的工作习惯任何新建的后端应用接入集群存储时优先推荐 PVC StorageClass 的动态供给模式只有在极少数不需要持久化、或者文件完全可以丢的场景才会退回去用 emptyDir。原因也很简单PV/PVC/StorageClass 这套抽象帮你把存储编排的复杂度转移到了平台层应用侧越简单长期维护的代价越低。底子打好了后面不管是扩容、迁移还是换存储后端都会从容很多。