多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

K8s NFS动态存储供给:nfs-subdir-external-provisioner部署与排障实战

K8s NFS动态存储供给:nfs-subdir-external-provisioner部署与排障实战 Kubernetes 集群里跑有状态应用第一道坎永远是数据怎么办。Pod 可以随时删、随时重建但数据不能跟着一起没了。NFS 应该是我见过最不挑环境的存储方案了——几乎所有 Linux 发行版都能搭服务端所有节点都能直接挂载不像 Ceph 需要单独部署 MON/OSD也不像 Longhorn 对内核特性有要求。剩下真正要解决的问题只有一个怎么让 PV 的创建和绑定过程自动化。这正是 nfs provisioner 干的事。这篇东西我会把 nfs-subdir-external-provisioner 从原理到部署、从验证到排障完整过一遍。你会看到三种部署方式、核心参数怎么选、删 PVC 不丢数据的坑在哪儿以及我踩过的几个典型问题。适合正在搭测试环境、准备上生产但还没想好存储方案的 K8s 运维同学也适合想搞明白 StorageClass 动态供给到底怎么运作的开发。1. 为什么需要动态存储供给先搞清楚 PV/PVC 那点事1.1 静态供给时代的痛点早期用 Kubernetes 给应用准备存储流程是这样的管理员先手动创建一个 PVPersistentVolume声明好容量、访问模式、路径然后开发者再写一个 PVCPersistentVolumeClaim去申请。申请能不能成功取决于 PV 和 PVC 是否匹配——容量够不够、访问模式对不对、有没有绑定到目标 StorageClass。一旦中间某个字段对不上PVC 就永远卡在 Pending 状态连报错信息都模棱两可。在存储券数量少的时候这套流程勉强能用但业务一多就非常痛苦。比如一个环境里同时跑几十个中间件每个都要独立的存储目录管理员就得一个个手工建 PV、写 YAML、绑定标签。更糟糕的是如果开发环境和测试环境的 PV 容量需求不一样你还得为每个环境单独准备一套 PV 清单。这套操作完全重复且容易出错本质上是人工翻译需求的过程。静态供给真正让人头疼的还不是创建而是回收。PV 删了数据怎么办谁来清谁负责重新分配很多时候答案就是没人管——一堆已释放的 PV 躺在那状态全是 Released目录里的数据永远没人清理。存储资源就这样白白浪费了。1.2 动态供给的工作原理StorageClass 出现之后这套流程发生了本质变化。核心思路是把创建 PV这件事从管理员手里交给一个自动化组件——Provisioner。开发者申请存储时只需要在 PVC 里声明 storageClassName集群里的 PV controller 看到 Pending 状态的 PVC 之后会去查找对应的 StorageClass调用注册在 StorageClass 上的 Provisioner 插件让插件去实际存储系统上创建资源然后自动生成 PV 并完成绑定。如果你啃过《深入理解Kubernetes源码》应该记得 controller-manager 里那一堆 volume 相关的 controller 是怎么协作的。PV controller 的工作循环本质上就是 watch PVC、watch PV、watch StorageClass然后根据状态机推动供需匹配。咱们平常感受不到它的存在是因为 provisioner 已经把创建底层存储资源并生成 PV这个脏活全部包掉了你只需要在 PVC 里写一句 storageClassName 就完事。动态供给的架构可以用一个很简单的链条描述PVC声明需求→ StorageClass指定 Provisioner 和参数→ Provisioner连接真实存储系统→ PV实际可挂载的存储对象。nfs provisioner 就是链条里那个 Provisioner 的角色它本质上是一个常驻集群的 Pod监听 PVC 事件然后在你自己准备的 NFS 服务器目录下为每个 PVC 创建独立子目录并以 PV 的形式暴露给集群使用。1.3 NFS 方案的适用边界NFS Provisioner 解决的是没有专业存储团队、也没有高端存储设备时的最朴素存储诉求。它最大的优势是简单NFS 服务端随便一台 Linux 就能跑不需要额外的控制器节点、不需要多副本同步K8s 节点只要能挂 NFS 就可以用。哪怕是租来的云服务器、没有共享磁盘的物理机环境只要内网 IP 能通就能拼出一个伪共享存储。但我也得给泼点冷水。NFS 说到底是一个网络文件系统单点故障是天然的——服务器挂了所有挂载它的 Pod 都会受影响性能瓶颈也同样明显大量小文件读写、随机 IO 场景下NFS 的表现远不如本地盘和分布式块存储。生产环境的数据库、中间件日志存储我建议还是优先考虑 Rook-Ceph 或云厂商的云盘NFS Provisioner 最适合的是日志归档、静态文件、测试环境的中间件数据这类对性能和可靠性要求没那么极端的场景。这个取舍要放到架构选型阶段就想清楚而不是等出问题再后悔。2. 部署前的架构设计与环境准备2.1 整体架构设计在动手敲命令之前先把你脑海中要搭出来的东西画出来思路会清晰很多。整套结构由三部分组成一台 NFS 服务器负责提供真实的磁盘空间一个在 K8s 集群内以 Deployment 方式运行的 provisioner Pod负责把 NFS 服务器的某个共享目录挂载进容器并监听 PVC 创建请求一个 StorageClass负责把 PVC 请求路由到 provisioner。更具体地说provisioner Pod 内部做的事情是把 NFS 服务器的共享目录比如 /data/nfs挂载到自己的容器里路径通常叫 /persistentvolumes。收到 PVC 创建请求后它会在 /persistentvolumes 下新建一个子目录默认命名格式是 {namespace}-{pvcName}-{pvName}然后把这个子目录封装成一个 NFS 类型的 PV 对象交给 K8s。Pod 里的应用挂载 PV 时K8s 会在节点上把 NFS 服务器上的对应子目录直接挂到应用 Pod 的容器里数据路径全程透明。这个设计有个特别的点需要注意provisioner Pod 负责的只是创建目录、生成 PV 对象数据读写并不经过它。应用 Pod 挂载 NFS 是直接与 NFS 服务器交互的所以 NFS 服务器的网络稳定性至关重要provisioner 所在节点的挂载状态反而没那么关键。2.2 组件版本选型为什么选 nfs-subdir-external-provisioner先交代一个容易踩的坑老牌项目 nfs-client-provisioner 已经归档停止维护了仓库地址放着不动没有人再修 bug、没有新的镜像发布。现在社区官方推荐的是 kubernetes-sigs 组织下的 nfs-subdir-external-provisioner最新的稳定版本是 v4.0.2镜像在 registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2。这两个项目的名字看着很像但生态地位完全不同。nfs-client-provisioner 是当初外部的个人/组织项目后来被官方吸收又演变成了 subdir 版本。subdir 版本的核心改进在于对 PV 目录的命名管理、对归档删除archiveOnDelete的支持以及对 K8s 新版本 API 的适配。我强烈建议新部署直接用 nfs-subdir-external-provisioner哪怕你以前看过老项目的文档也不要去复刻那套老 YAML——镜像拉不下来的问题都算轻的主要是里面用了不少已经废弃的 API 版本字段。2.3 NFS 服务器端准备工作先搞定 NFS 服务器否则集群里做啥都是白搭。以 Ubuntu/Debian 为例安装服务端apt-get update apt-get install -y nfs-kernel-server mkdir -p /data/nfs chmod 1777 /data/nfs编辑/etc/exports这一步是整个 NFS 配置里最关键的地方。我常用的配置是/data/nfs *(rw,sync,no_subtree_check,no_root_squash,fsid0)# 重新加载 exports 配置 exportfs -ra # 查看生效状态 showmount -e localhost这里的参数我得细讲一下因为它们直接影响后续 K8s 里挂载的权限表现很多人就是栽在这上面。rw允许读写。这个不用多解释只读的话什么都干不了。sync同步写入。数据先落盘再响应客户端写请求虽然性能比 async 差一点但稳定性好得多。存储类的东西我基本不碰 async。no_root_squash默认情况下 NFS 会把客户端的 root 用户映射成匿名用户nobody导致容器里以 root 身份写文件时落到 NFS 服务器上的 owner 变成 nobody后续其他进程可能没有权限读写。加了这个参数后 root 身份被保留权限问题少一大半。no_subtree_check取消了子树检查配合 fsid0 使用避免某些内核版本下导出子目录时的兼容性问题。fsid0显式告诉 NFS 服务器这个目录是导出根。某些系统上不写这个参数也能工作但写上能避免一堆奇奇怪怪的挂载报错。如果你不想用 no_root_squash 这么大刀阔斧的参数provisioner 也提供了 mapRootUser 这个配置参数可以把容器里的 root 映射成指定的 UID/GID等于把权限控制粒度再收一收。后面章节我会专门讲这个。2.4 RBAC 权限模型解析provisioner 要正常工作必须给它的 ServiceAccount 授予一批集群级权限。为什么一个存储插件需要这么多权限想清楚它的工作职责就知道了它要 watch 所有 namespace 下的 PVCpersistentvolumeclaims 的 get/list/watch要创建和删除 PVpersistentvolumes 的 create/delete要读取 StorageClass 的参数storageclasses 的 get/list/watch还要写事件供 kubectl describe 排查events 的 create/patch。这些资源都是集群级的所以需要 ClusterRole 而不是普通 Role。我在生产环境里看到的 RBAC 配置通常还额外加大了权限范围比如对 endpoints、services 也有读写权限。实际上 nfs-subdir-external-provisioner 官方部署文件要求的权限没那么杂保持最小权限原则就够了。如果你用的是 Helm 安装RBAC 相关的对象会自动生成不用手动去改。要手写部署文件的同学啃官方 deploy 目录下的 rbac.yaml 即可照着复制粘贴是最稳妥的。3. 核心实操三种方式部署 provisioner3.1 方式一Helm Chart速度最快我个人最推荐的方式还是 Helm省时省力参数直观。先把官方仓库加上helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/ helm repo update然后一次安装搞定helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \ --set nfs.server192.168.1.100 \ --set nfs.path/data/nfs \ --set storageClass.namenfs-client \ --set storageClass.defaultClasstrue \ --set storageClass.archiveOnDeletefalse \ --namespacekube-system这里我逐个解释关键参数避免你为了测试一下把生产配置搞乱nfs.server和nfs.path填你自己的 NFS 服务器 IP 和共享目录这两个是硬参数缺一不可。storageClass.name生成的 StorageClass 名字。这个名字会被 PVC 的 storageClassName 字段引用起个一眼能认出来的名字比如 nfs-client、nfs-standard。storageClass.defaultClass设为 true 会让集群里不带 storageClassName 的 PVC 自动路由到 NFS。这个选项省事但是会带来隐患——某些组件比如 Helm 安装的中间件如果没声明存储类会用默认 StorageClass 弹 PVC数据会被塞到 NFS 里。测试环境开着挺方便生产环境我建议关掉。storageClass.archiveOnDeletePVC 删除时是否归档数据。true 的话删除 PVC 不会真删 NFS 目录而是改名加个 archived- 前缀false 就直接删目录。这里非常关键后面我会专门分析。3.2 方式二原生 YAML 手工部署不想引 Helm 的团队不在少数尤其那种统一管控、不允许随手 helm install 的环境。这种场景直接手写 YAML 也可以。核心组件包括 ServiceAccount、ClusterRole、ClusterRoleBinding、Deployment、StorageClass。Deployment 的关键部分长这样apiVersion: apps/v1 kind: Deployment metadata: name: nfs-provisioner namespace: kube-system spec: replicas: 1 selector: matchLabels: app: nfs-provisioner template: metadata: labels: app: nfs-provisioner spec: serviceAccountName: nfs-provisioner containers: - name: nfs-provisioner image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 volumeMounts: - name: nfs-volume mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: k8s-sigs.io/nfs-subdir-provisioner - name: NFS_SERVER value: 192.168.1.100 - name: NFS_PATH value: /data/nfs volumes: - name: nfs-volume nfs: server: 192.168.1.100 path: /data/nfs三个环境变量各有讲究。PROVISIONER_NAME是一个唯一标识符它必须和 StorageClass 里的 provisioner 字段完全一致否则 PVC 请求到了 controller 手里却没有 provisioner 认领会一直 Pending。NFS_SERVER和NFS_PATH告诉 provisioner 去哪里连真实的存储。写这个 YAML 时我最常犯的错就是 PROVISIONER_NAME 写了一个值StorageClass 里又写了另一个结果怎么排查都对应不上。手工部署方式里还要单独创建一个 StorageClassapiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client provisioner: k8s-sigs.io/nfs-subdir-provisioner reclaimPolicy: Delete volumeBindingMode: Immediate allowVolumeExpansion: true parameters: archiveOnDelete: true3.3 StorageClass 核心参数详解StorageClass 是整个动态供给的入口配置几个关键字段必须吃透。provisioner字段决定谁来处理 PVC 请求上面已经强调过要和 Deployment 里的 PROVISIONER_NAME 一致。reclaimPolicy决定 PVC 被删除后 PV 怎么处理。Delete 表示 PV 对象跟着删除底层数据是否也删取决于 provisioner 的归档配置Retain 表示 PV 保留状态变成 Released数据原封不动留在 NFS 服务器上需要管理员手动处理。坦白讲我见过太多团队把 Delete 当成默认安全选项结果删除 PVC 后底层数据也被清了还浑然不知。allowVolumeExpansion控制 PVC 容量能否在线扩容。NFS 这种网络文件系统扩容本身不涉及底层分区操作所以设为 true 是安全的。如果这个字段不设以后想改 PVC 的 storage 大小会被 API server 直接拒绝。volumeBindingMode默认用 Immediate意思是 PVC 一旦创建就立刻触发 provisoning。如果你接的后端是拓扑敏感型存储比如 AWS EBS 绑可用区可能需要 WaitForFirstConsumerNFS 是全集群共享的用默认 Immediate 就好。parameters里最值得关注的是archiveOnDelete。这个参数只有 reclaimPolicy 为 Delete 时才有意义PVC 删除后PV 会被删除但 NFS 上的数据目录不会被直接清掉而是被改名为archived-{namespace}-{pvcName}-{pvName}。如果填 false目录就直接被 rmdir。我给大家的建议是测试环境用 false 清爽生产环境无论如何都要设 true——数据没了是真找不回来的目录多几个顶多后面人工清。还有pathPattern参数用来控制 PV 目录在 NFS 服务器上的命名格式。默认是${.PVC.namespace}/${.PVC.name}即按命名空间建子目录再按 PVC 名建子目录。用这个参数能实现租户隔离、权限边界清晰多业务共用一套 NFS 时非常实用。3.4 参数选型经验总结我在不同环境里配置过好几套 NFS Provisioner结合踩过的坑分享几条选型经验。默认 StorageClass 这个选项千万别拍脑袋就设 true。测试环境为了省事可以设但生产环境强烈建议关掉因为很多中间件的 Helm Chart 都会带 PVC一旦你把它当成默认存储类所有应用的无状态组件也可能悄悄挂到 NFS 上存储负载和故障域完全失控。让每个需要存储的应用显式声明 storageClassName反而能把存储依赖关系梳理清楚。archiveOnDelete我上面已经说了必须生产环境设 true。这个参数跟数据的最终归宿绑定不是你当时记得就能避免后续事故的——很多团队是后面要复用 PVC 名字一删一建老数据就被覆盖了。归档字段把它救回来。NFS 服务器上建议单独划分子目录给 K8s 专用别跟其他文件混在一起。因为 provisioner 会把每个 PVC 的目录命名和归档命名都放在共享根目录下如果根目录里还有其他业务目录清垃圾的时候很难分辨哪些是 PV 数据、哪些是业务文件。单独目录 定期巡检是最省心的组合。4. 验证与实战让应用真的用上动态存储4.1 创建 PVC 验证动态供给是否生效部署完 provisioner 和 StorageClass第一件事就是创建测试 PVC 验证动态供给链路。写一个最简单的 PVCapiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-pvc namespace: default spec: storageClassName: nfs-client accessModes: - ReadWriteOnce resources: requests: storage: 1Gi创建后观察状态kubectl apply -f test-pvc.yaml kubectl get pvc test-pvc正常情况下几秒钟内 PVC 状态会从 Pending 变成 Bound。同时能看到自动生成一个 PVkubectl get pv | grep test-pvc此时去 NFS 服务器上看一眼目录结构应该能看到类似于default-test-pvc-pvc-xxxx的子目录已经建好了。这个目录就是 PV 背后的真实数据位置里面哪怕一个文件都没写目录本身已经存在。这里有个小细节PVC 的 accessModes 可以写 ReadWriteOnce 也可以写 ReadWriteMany因为 NFS 天然支持多节点读写。若你在 PVC 中声明的是 ReadOnlyMany下面挂载的工作负载要以只读方式挂载否则会报权限错误。4.2 挂载到工作负载中验证读写PVC 验证通过后把它挂到一个 Deployment 里实际写入数据这才是完整的验证闭环。示例 DeploymentapiVersion: apps/v1 kind: Deployment metadata: name: nfs-test-writer spec: replicas: 1 selector: matchLabels: app: nfs-test-writer template: metadata: labels: app: nfs-test-writer spec: containers: - name: writer image: busybox:1.36 command: [/bin/sh, -c] args: - while true; do echo $(date) hello nfs /mnt/data/test.log; sleep 5; done volumeMounts: - name: data mountPath: /mnt/data volumes: - name: data persistentVolumeClaim: claimName: test-pvc等 Pod Running 之后进入容器检查文件kubectl exec -it deploy/nfs-test-writer -- tail -f /mnt/data/test.log再到 NFS 服务器上看同一个文件内容应该一致。这一步验证的是K8s 里的 PVC 挂载 - NFS 服务器目录这条链路是否真的通了而不只是 PV 状态显示 Bound。4.3 从源码角度理解删除、归档与保留策略很多人用 NFS Provisioner 时只知道删 PVC 数据会没却不知道为什么。这里从源码实现的角度帮大家把这个逻辑捋清楚。当 PVC 被删除时K8s 的 PV controller 会触发对应 PV 的回收流程。如果 StorageClass 的 reclaimPolicy 是 Deletecontroller 会调用 provisioner 的 Delete 方法。nfs-subdir-external-provisioner 在 Delete 阶段实现了一个关键分支如果 archiveOnDelete 参数为 true它会调用一个 rename 操作把 NFS 服务器上对应的目录从namespace-pvcname-pvname重命名为archived-namespace-pvcname-pvname如果为 false就直接删除目录。而如果 reclaimPolicy 是 RetainPV 被删除后 PV 对象本身保留在集群里状态变为 ReleasedNFS 数据原封不动需要管理员介入手动处理。这个逻辑细节正是《深入理解Kubernetes源码》里 PV 回收语义和外部 provisioner 实现之间的对应关系。理解了源码层的行为你就不会再犯明明配了 Delete 以为会自动清理的错——Delete 只是删除 PV 对象底层数据的生死完全由 params 和 StorageClass 联动决定。这里我补充一个恢复数据的技巧。如果不小心在 archiveOnDeletefalse 的情况下删了 PVC而 NFS 目录被删了那基本没救但如果目录还在比如 rename 后没清你可以在 NFS 服务器上把 archived- 前缀的目录改回正确名字然后手工重新创建同名的 PV 和 PVC 来恢复绑定。虽然麻烦但至少数据还有挽救的余地——前提是你的 NFS 服务器没开快照功能这又是一条劝告生产环境无论如何给 NFS 服务器配个快照或定期备份。4.4 多环境扩展与 namespace 隔离思路NFS Provisioner 天然适合多环境共用一个 NFS 服务器的场景只要在 StorageClass 参数里配好 pathPattern就能把不同 namespace 的数据物理隔离。示例配置parameters: pathPattern: ${.PVC.namespace}/${.PVC.name}这样 NFS 服务器上会自动按 namespace 建目录default namespace 的数据在default/pvcnameprod namespace 的数据在prod/pvcname。权限管理层面你甚至可以搭配 NFS 的 export 权限给不同 namespace 分配不同子目录的读写控制——当然实现的复杂度会高很多这里只是一提。日常情况下pathPattern 配合 archiveOnDelete 已经足够满足 90% 的规划和清理需求。5. 常见问题与故障排查实录5.1 Provisioning failedPVC 一直 Pending这是最典型的入门问题。创建 PVC 后卡在 Pending 几十分钟不动kubectl describe 里能看到 provision 相关的 Event通常写着类似 provisioning failed 或 storageclass not found。排查顺序我建议按下面来先看 StorageClass 的 provisioner 字段和 Deployment 里的 PROVISIONER_NAME 是否完全一致。两者不一致是最常见的低级错误比如少了个后缀、多了个斜杠。再看 provisioner Pod 日志。kubectl logs -n kube-system -l appnfs-provisioner --tail50日志里如果出现 NFS mount 失败、No such file or directory、Permission denied 之类基本就是 NFS 服务端的问题。这时候去 NFS 服务器上看 exports 配置是否正确、目录是否存在、showmount 能不能看到共享。还有一种隐蔽情况provisioner 所在的节点以前挂载过旧路径NFS 的挂载信息残留导致连接失败重启节点或者重新 mount 可以解决。5.2 权限问题文件 owner 是 nobody容器写不进去这个坑的典型表现是PV 创建成功、PVC 绑定成功、Pod 也能启动但应用日志里疯狂报 Permission deniedls -l 一看 NFS 目录的 owner 全是 nobody。问题根源几乎都出在 NFS 服务端的 root_squash 上。在没有 no_root_squash 的情况下容器里的进程以 root 或其他 UID 写 NFS 文件NFS 服务器会把请求映射成匿名用户nobody。如果容器里进程的 UID 在 NFS 服务器上没有对应账户写出来的文件 owner 就是 nobody。解决方向有两个第一在 /etc/exports 里给共享目录加上 no_root_squash 并重新 exportfs -ra适合容器里进程以 root 运行的场景第二用 provisioner 的 mapRootUser 参数把容器里的 root 映射成你指定的 UID/GID适合容器里进程以非 root 运行的场景。比如 Helm 安装时指定--set nfs.mapRootUser1000这会让 provisioner 在创建 PV 目录时把目录的属主设置为 UID 1000目标进程只要以 UID 1000 运行就能正常读写。这个参数在老版本里没有升级到 v4.x 后才有用之前确认一下版本。还有个别情况是 PV 目录已经有了但目录权限本身不对。此时不用删 PVC直接去 NFS 服务器上对对应目录 chmod/chown 即可修改立即生效。5.3 挂载报错 mount failedNFS 版本与协议问题应用 Pod 调度到某个节点后报挂载失败形如mount failed: exit status 32或mount: wrong fs type。常见原因包括节点上没装 nfs-common / nfs-utils以及 NFS 协议版本不匹配。Debian/Ubuntu 节点先安装客户端工具apt-get install -y nfs-commonCentOS/RHEL 装yum install -y nfs-utils安装后问题依旧就要考虑 NFS 版本差异。K8s 默认的 NFS 挂载可能使用 v4 或 v3跟服务端的配置不一定兼容。这时可以在 StorageClass 的 mountOptions 里显式指定挂载参数mountOptions: - nfsvers4.0如果服务端只支持 v3就把 nfsvers 改成 3。注意 nfsvers4.0 和 nfsvers4 的细微差别有些内核版本对 4.0 和 4.1 的支持不一样需要现场试验。遇到协议问题最快的方式是先手动在节点上 mount 一次验通之后再改 StorageClass比一遍遍重启 Pod 猜原因快得多。5.4 删除 PVC 后数据去向与恢复这个问题我在生产现场被问过很多次我删了 PVC数据还能找回来吗答案完全取决于 StorageClass 的 reclaimPolicy 和 archiveOnDelete 的配合。如果 reclaimPolicy 是 Delete 且 archiveOnDeletetrue删 PVC 后 NFS 服务器上会多一个 archived- 开头的目录数据还在。此时恢复方法先把 archived- 目录改名回正常业务目录名再手工创建 PV 和 PVC 把命名对上Pod 再挂载就能读到老数据。注意 PV 的 source 里 nfs.path 要精确到那个目录。如果 reclaimPolicy 是 Delete 且 archiveOnDeletefalse数据目录被直接删除随手 rm -rf 的后果没有任何机制能救。这也是我一直强调生产环境必须开 archiveOnDelete 的原因。如果 reclaimPolicy 是 RetainPVC 删除后 PV 还在但状态变成 Released。恢复流程是手动删除 PV不删底层数据然后重新创建 PV 指向现有 NFS 目录再创建 PVC 去绑定。这个方式不依赖 archiveOnDelete但要求 PV 和 PVC 的容量、访问模式精确匹配操作繁琐但安全。5.5 问题排查速查表现象可能的根因排查命令/手段解决方案PVC 一直 PendingStorageClass 的 provisioner 名字不匹配kubectl describe pvc查看事件统一 PROVISIONER_NAMEPVC Pending 且 provisioner 日志有 NFS 错误NFS 服务端 exports 配置错误、目录不存在showmount -e、ls /data/nfs修复 exports 和目录权限Pod 挂载失败 exit status 32节点未装 nfs-common/nfs-utilswhich mount.nfs安装 nfs 客户端工具挂载失败但节点可手动挂载StorageClass mountOptions 与 NFS 版本不匹配手动 mount 验证调整 nfsvers容器写文件 Permission deniedroot_squash 映射成 nobodyls -l看文件属主no_root_squash 或 mapRootUser删除 PVC 后数据目录消失archiveOnDeletefalse看 NFS 目录列表开 archiveOnDeletetruePV 状态 ReleasedreclaimPolicyRetainkubectl get pv手动重建 PV/PVC跨节点挂载部分失败NFS 服务端防火墙限制节点 curl/探测 NFS 端口2049放行 2049 端口同一目录多个 PVC 冲突pathPattern 命名不唯一看 NFS 目录命名使用 namespace/pvcname 模板我把这些年在 NFS Provisioner 上积累的经验浓缩成一句话动态供给的代码逻辑其实很薄真正的复杂度都藏在存储服务端和权限模型里。provisioner 装好只需要五分钟但你跟 NFS 服务器、跟 K8s 的 PV 回收语义之间的博弈才是真正有技术含量的部分。希望上面这些过程记录和踩坑笔记能让你在部署时少走几个弯路。
返回列表