ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Kubernetes持久化存储实战:从PV/PVC到StorageClass与NFS动态供给

Kubernetes持久化存储实战:从PV/PVC到StorageClass与NFS动态供给 1. 为什么Kubernetes需要一套独立的存储抽象1.1 先聊聊容器世界里的数据到底有多脆弱熟悉Kubernetes的朋友应该都对这句话不陌生Pod是牲畜而不是宠物。翻译成人话就是Pod随时可能被销毁、被重建、被调度到另一台节点上你千万不能对它产生感情。但这句话背后藏着一个让很多人一开始都没意识到的问题——如果Pod本身是随时会消失的那Pod里跑的业务数据怎么办我在几年前第一次用Kubernetes部署一个有状态的MySQL实例时对有状态这三个字完全没有概念。那个Pod在运行第七天的时候因为节点内存耗尽被驱逐了数据全部丢失恢复耗时整整一个通宵。后来我才明白容器默认的存储机制就是随生随灭镜像层是只读的容器层临时写入的数据会随着容器删除而消失甚至不需要删除一次OOM重启就可能让关键数据从你的世界里彻底蒸发。这就像你在酒店房间里用便利贴记了一堆重要信息退房的时候要是忘了带走前台打扫完房间你的信息就没了。Kubernetes当然比酒店前台贴心它早就设计了Volume机制来把数据从Pod生命周期中剥离出来。但真正的问题在于Volume的类型太多了有emptyDir、hostPath、configMap、secret、云厂商的云盘、NFS、Ceph……每一种的创建方式、使用场景、生命周期都完全不一样。如果把这些差异全部暴露给业务开发者那Kubernetes的声明式理念可以说连一半都没做到。持久化存储的核心价值其实不是能存数据这么简单而是让应用开发者根本不需要关心数据到底存放在哪台机器上、用的是哪种存储介质。这句话在我刚接触Kubernetes的时候听起来像废话直到我被生产环境的存储问题折磨了半年才真正理解这套抽象的价值所在。1.2 hostPath不是一劳永逸的答案很多初学Kubernetes存储的人第一感觉是直接挂载宿主机目录不就行了吗hostPath确实能做到数据持久化——Pod重启后数据还在Pod删除重建后只要调度到同一节点数据依然还在。但你一旦开始认真思考调度到同一节点这个前提就会发现hostPath在集群场景里根本站不住脚。举个例子你的应用Pod配了hostPath挂载指向节点A的/data目录。有一天节点A坏了或者你跑了kubectl drain做节点维护Pod被自动调度到节点B。节点B上那个/data目录跟节点A上的完全没有任何关系之前的数据就像被扔进了一个平行时空。你可能会想那我用nodeSelector把Pod强制钉在节点A上不就行了这确实能解决数据一致性问题但又回到了最原始的宠物模式——你的Pod和某台具体的机器绑定在一起了机器出问题业务直接瘫痪。更麻烦的是hostPath遇到多副本场景几乎是无解的。Deployment里配置了三个副本每个副本有独立的存储用户请求打到不同副本上看到的数据还不一样这就是典型的分布式系统数据一致性灾难。我在一个生产项目里见到过这种情况业务方把文件上传到了某个Pod的hostPath目录但因为负载均衡路由到了另一个Pod文件死活看不到排查了一整天愣是没找到原因。所以在集群环境里持久化存储必须是一个独立的、与节点无关的数据服务。这个数据服务可以是外部的NFS服务器、Ceph集群也可以是云厂商提供的云盘。而Kubernetes要做的事情就是屏蔽这些底层存储的差异给使用者提供一个统一的、稳定的数据访问接口。这正是PV、PVC、StorageClass这套机制存在的根本原因。1.3 你需要的不是一种存储而是一套存储抽象体系我见过不少从单体架构迁移到Kubernetes的团队一开始都抱着选一个主流存储方案用到老的心态。但真实的生产环境远比想象的复杂不同业务对存储的要求差异极大。有的业务只需要几GB的共享目录对性能完全不敏感NFS就够用有的业务要求几TB的空间和低延迟必须用云厂商的SSD云盘还有日志系统需要海量流式写入性能和成本都不能放弃Ceph的RBD模式才适合。如果每个业务都直接对接各自的存储API那运维团队会疯掉——每次存储扩容、迁移、更换底层设备都要去改业务的部署配置。而Kubernetes的PV/PVC抽象体系恰恰就是来解决这件事的应用开发者只声明我要多大空间、什么读写模式至于背后用的是什么存储由管理员来配置。底层存储从NFS换到Ceph业务方只需要重新绑定一下PVCDeployment里的挂载配置可以完全不动。这也是为什么很多人学Kubernetes存储时觉得一开始被PV和PVC绕得头晕学会之后才发现真香的原因。这两个概念就像数据库的视图层和物理存储层的分离复杂度被封装得严严实实暴露给使用者的只是一个清爽的SQL接口。2. PV与PVC先理解绑定逻辑再谈使用2.1 PV是资源PVC是需求单先给出一个我在培训中常用到的类比PV是停车位PVC是停车需求单。What——管理员划分了一批停车位就是PV某个应用需要停一辆车需要申请一个车位于是提交一份PVC需求单声明我需要一个带顶棚、能停SUV的车位。调度器看到需求单后会去现有车位里找最匹配的一个绑定到一起。这个绑定关系一旦建立这台车就固定停在这个车位上了不会再变。在Kubernetes里PVPersistentVolume是集群级别的资源由管理员预先创建它描述了存储的容量、访问模式、回收策略以及底层存储的具体配置。比如一个NFS类型的PV会在nfs配置块里写明server地址和导出的路径一个云盘类型的PV会写明云厂商、区域、磁盘ID。PVCPersistentVolumeClaim是名字空间级别的资源由业务方创建它只描述需求我要5Gi的空间我要能多个节点同时读写我需要ReadWriteMany。Kubernetes的控制循环会扫描集群里所有满足条件的PV和这个PVC完成绑定。绑定之后PVC就变成了Pod配置里可以直接引用的存储对象。这里有个初学者经常踩的坑PVC申请的资源是一个请求值但并不等于最终的绑定值。比如你申请了5Gi的PVC但集群里只有一个10Gi的PV满足条件Kubernetes照样会把它绑定给你这个10Gi的PV就只服务于这5Gi的PVC了。这在容量规划时一定要格外注意浪费掉的那部分空间通常不会再被别的PVC使用。2.2 从创建到绑定的完整流程静态供给模式下持久化存储的完整使用流程接上刚才的类比就是管理员买车位用户提交需求单系统自动匹配绑定Pod引用绑定后的PVC挂载数据卷。实际操作中三个对象的配置大概长这样。首先管理员创建一个NFS类型的PVapiVersion: v1 kind: PersistentVolume metadata: name: pv-nfs-data spec: capacity: storage: 10Gi accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain storageClassName: nfs: server: 192.168.1.100 path: /data/k8s业务方创建对应的PVCapiVersion: v1 kind: PersistentVolumeClaim metadata: name:>spec: containers: - name: app image: nginx volumeMounts: - name: data mountPath: /usr/share/nginx/html volumes: - name: data persistentVolumeClaim: claimName:>/data/k8s 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)这里有几个参数需要重点说明rw表示读写权限sync表示事务落盘后再返回成功no_subtree_check能提升性能但会降低一点安全检查强度no_root_squash允许Pod里以root身份写的文件保持root属主——后续挂载权限问题的排查这个参数很关键设置成no_root_squash可以省掉很多无谓的权限纠结。保存配置后执行两条命令生效exportfs -av systemctl enable nfs-server systemctl start nfs-server可以顺手用showmount -e 192.168.1.100验证一下目录是否成功导出。这一步就算完成了NFS Server的部署暂时告一段落。接下来是Kubernetes工作节点上的客户端安装。这一步是最容易被遗漏的地方。很多人部署完NFS StorageClass之后PVC状态死活就是ContainerCreating一看kubelet日志发现挂在mount failed: exit status 32原因就是Kubernetes节点上没有安装NFS客户端包kubelet明明调用了mount命令但内核没有NFS客户端模块。CentOS节点上执行yum install -y nfs-utilsDebian系节点上执行apt install -y nfs-common然后不要跳过任何一台节点全部都要装。这一步之后节点上可以通过mount -t nfs 192.168.1.100:/data/k8s /mnt手动测试一下能否挂载成功。测完记着umount /mnt解挂不然接下来验证的时候会多出一道目录已被占用的困惑。4.2 部署NFS动态供给插件NFS静态供给的手工流程上一章已经讲过了这里重点演示动态供给。我在生产环境里最常使用的开源项目是 nfs-subdir-external-provisioner 它部署简单、社区活跃几乎不需要额外改动就能跑起来。在它的仓库里提供了一套完整的部署YAML主要包括三部分Namespace与RBAC权限、Deployment控制器、StorageClass定义。我习惯把它们拆分到单独的文件里管理方便后续排查。核心的Deployment部分长这样apiVersion: apps/v1 kind: Deployment metadata: name: nfs-client-provisioner namespace: kube-system spec: replicas: 1 selector: matchLabels: app: nfs-client-provisioner template: metadata: labels: app: nfs-client-provisioner spec: serviceAccountName: nfs-client-provisioner containers: - name: nfs-client-provisioner image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: k8s-sigs.io/nfs-subdir-external-provisioner - name: NFS_SERVER value: 192.168.1.100 - name: NFS_PATH value: /data/k8s volumes: - name: nfs-client-root nfs: server: 192.168.1.100 path: /data/k8s这段配置里有两个坑需要注意。第一PROVISIONER_NAME这个环境变量的值必须跟StorageClass里定义的provisioner字段完全一致这是Kubernetes控制平面识别谁来处理这个存储类的关键。很多教程喜欢直接复制一段YAML结果把provisioner名字写成了example.com/nfs跟StorageClass对不上一运行就发现PVC永远Pending。第二这个Deployment只会运行一个副本因为多个副本同时去操作同一个NFS目录创建子目录时并发写会产生竞争问题官方也是默认只部署一个副本。RBAC权限部分这个插件需要能读取和操作PV、PVC、StorageClass、Event等资源在namespace和cluster级别都要给足权限。我一般直接复用仓库里的deploy/目录下现成的RBAC文件改一下namespace即可不建议自己手搓权限漏一项都会导致插件运行时报错。最后是StorageClass的定义apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-storage provisioner: k8s-sigs.io/nfs-subdir-external-provisioner reclaimPolicy: Delete volumeBindingMode: Immediate这里的一个关键设计是每个PVC都会在NFS服务器上自动创建一个独立子目录目录名通常是${namespace}-${pvcName}-${pvName}对应到NFS Server的/data/k8s下面。这么做的好处是PVC和物理目录一一对应数据隔离性极好删PVC时也方便定位。你在NFS服务器上执行一下ls /data/k8s就能看到各个应用对应的子目录。创建完三份YAML后执行kubectl apply -f rbac.yaml kubectl apply -f deployment.yaml kubectl apply -f storageclass.yaml验证插件是否就绪可以用kubectl get pods -n kube-system | grep nfs查看Pod是否Running再kubectl logs看有没有异常输出。正常情况下插件日志里只会出现几条启动信息不会有WARN或者ERROR级别的记录。4.3 使用Nginx验证数据落盘和恢复部署完成后怎么验证这套存储真正的可用性我建议用一个最简单的Nginx来做全链路验证这也是我在网上搜到kubernetes 部署nginx时自己正在做的事情。首先创建一个请求NFS存储的PVCapiVersion: v1 kind: PersistentVolumeClaim metadata: name: nginx-pvc spec: accessModes: - ReadWriteMany resources: requests: storage: 1Gi storageClassName: nfs-storage创建之后立刻查看状态kubectl get pvc nginx-pvc正常情况下几秒内PVC就会从Pending变成Bound。如果一直Pending优先检查插件的Pod日志、NFS Server的共享目录权限、以及节点上的NFS客户端是否安装成功这几个环节的排查思路后面会有专门小节。变成一个Bound状态后kubectl get pv可以确认动态创建出来的PV已经自动生成。然后写一个Nginx Deployment把PVC挂载到Nginx的web目录上apiVersion: apps/v1 kind: Deployment metadata: name: nginx-pv-test spec: replicas: 1 selector: matchLabels: app: nginx-pv-test template: metadata: labels: app: nginx-pv-test spec: containers: - name: nginx image: nginx:1.24 ports: - containerPort: 80 volumeMounts: - name: html mountPath: /usr/share/nginx/html volumes: - name: html persistentVolumeClaim: claimName: nginx-pvcPod起来后在Nginx容器里写入一个测试文件kubectl exec -it nginx-pv-test-xxx -- sh -c echo hello-persistent-storage /usr/share/nginx/html/index.html然后干一件验证持久化的关键操作kubectl delete pod nginx-pv-test-xxx让Deployment自动重建这个Pod。重建完成后再次进入新的Pod执行cat /usr/share/nginx/html/index.html如果能看到hello-persistent-storage就说明数据在Pod销毁重建后依然保留持久化验证通过。再验证一下这个目录确实存在NFS共享服务器上到NFS Server上执行ls /data/k8s/default-nginx-pvc-*能看到一个真实目录和里面的index.html文件。这个时候你就是真真正正把Kubernetes持久化存储从概念应用落地了。5. 常见问题排查速查与避坑清单5.1 PVC一直Pending先查静态再查动态PVC长时间处于Pending状态这在Kubernetes存储相关的故障里大概占了一半以上。排查路径其实有固定的套路按下面这个顺序挨个验证就好。第一步看事件。kubectl describe pvc pvc-name事件信息里通常会直接写着原因。比如no persistent volumes available for this claim and no storage class is set说明走的是静态供给但集群里的PV没有匹配条件。如果是动态供给的PVC可能写着storageclass.storage.k8s.io nfs-storage not found说明StorageClass名字拼写错了或者根本不存在。第二步如果是动态供给看provisioner Pod的日志。kubectl logs -n kube-system provisioner-pod。常见日志有error getting handle for claim: invalid claim或者failed to provision volume with StorageClass之类。这里九成是环境变量或者StorageClass配置不对比如PROVISIONER_NAME和StorageClass的provisioner字段不一致或者NFS Server地址、路径填错了。第三步如果是静态供给检查PV的状态和字段。kubectl get pv看PV是不是Availablekubectl describe pv pv-name看accessModes、capacity、storageClassName是否匹配。这里特别常见的是PV设置了storageClassName: 但PVC没写这个字段结果PVC走了默认StorageClass的动态供给逻辑完全不去匹配你的PV。第四步要留意volumeBindingMode。如果StorageClass设置的是WaitForFirstConsumer那PVC在Pod创建之前会一直Pending这是正常的不是故障。我第一次用这种模式时还以为存储坏了排查了半天最后才发现是绑定模式的问题。5.2 挂载报错与节点权限的典型问题挂载报错的案例更多这里列出几种我实际遇到过的情况。第一种Pod事件里出现MountVolume.SetUp failed for volume ... mount failed: exit status 32。这个几乎可以断定是节点上缺少NFS客户端。去对应节点上执行mount -t nfs 192.168.1.100:/data/k8s /mnt如果也报错说明mount命令本身就有问题。注意这个问题的排查核心是哪个节点报错了就去哪个节点装nfs-utils因为kubelet是每台节点一个。第二种挂载成功但业务内部写文件报Permission denied。这种问题八成是NFS导出的目录权限不对或者root_squash的配置把容器的root用户映射成了nobody。我的一般处理是在/etc/exports把目录设成no_root_squash然后把NFS共享的目录属主改成nfsnobody:nfsnobodyDebian系或者nobody:nobodyCentOS系。容器的UID和GID不需要跟宿主机一致只要NFS层面允许访问即可。第三种PVC绑定成功但原有数据看不到。这个常见于NFS的路径复用问题。插件默认创建的子目录是${namespace}-${pvcName}-${pvName}如果你手动把数据放到了NFS根目录/data/k8s下而忘记创建那个子目录PVC挂载上去后看到的自然就是空目录。简单说是路径没对应上的问题一查目录树就知道了。5.3 动态供给删除了PVC会有什么后果这个教训我愿称之为存储运维的手滑刹车片动态供给场景下删PVC会直接触发底层数据卷删除。在默认使用reclaimPolicy: Delete时PVC被删除后对应的PV也会被删除provisioner会调用底层存储API把NFS子目录整个清理掉。如果你的应用数据只有一份容器里没有做任何备份删PVC就意味着物理删除没有后悔药。所以我在生产环境的规范是凡是配置了Delete的StorageClass都必须配套可靠的备份流程。至少做到数据定期拷贝到其他位置或者为关键业务的PVC单独配置一个reclaimPolicy: Retain的StorageClass。测试环境则可以反其道而行之尽量用Delete免得测试数据堆积在你的NFS Server上铺满磁盘。5.4 NFS的性能与高可用真相写到这里顺带提一嘴NFS这层方案的实际性能边界。NFS本质上是一个网络文件系统每一个IO操作都要经过网络协议栈所以它和本地盘的差距是客观存在的。我在一个日志分析的应用上做过粗略压测NFS顺序读的吞吐量大概只有本地磁盘的六到七成随机写的IOPS下降更明显这在高并发小文件读写的场景里会成为一个真正的瓶颈。解决方案不见得一定要上Ceph。我见过一个项目把NFS服务器部署在SSD物理机上网络走万兆交换机压测数据提升非常明显。NFS Server的硬件水平、网络质量以及挂载时是否加了rsize1048576,wsize1048576,hard,timeo600这些优化参数都会直接影响实际性能。挂载优化参数可以在StorageClass的mountOptions里配置生产环境建议开启hard模式和较大的timeo至少要让业务在存储短暂不可用的时候阻塞住而不是默默失败。高可用方面NFS最常见的单点问题可以通过keepalived这样的方案解决把NFS Server做成主备模式。更为稳妥的方式是一些团队直接选用云厂商的NAS服务底层已经帮你做了多副本都不用自己操心节点故障。大方向还是那句话先明确业务诉求再选存储规模别一上来就奔着最复杂的去。6. 写在后面个人实操中的一点心得真要我总结训Kubernetes持久化存储这件事我的体会是它的入门曲线确实有点陡但一旦把PV、PVC、StorageClass这套抽象逻辑给理顺后续的每一次存储调整都会体会到这层抽象带来的便利。我在自己的集群里几乎再也没手写过PV业务方要空间就提交PVC要性能就改用对应的StorageClass运维负担和业务效率都得到了明显的改善。最后再分享一个小技巧。当你觉得某条存储链路有点不可控、但又说不清问题在哪时最好的办法不是反复去看配置而是直接回到最底层验证在NFS Server上手动执行mount、手动写文件、手动读文件一步步拆解问题。你会的越底层碰到问题越有信心因为只要你验证过最基础的链路是通的那么剩下的就是上层配置的排查。这套习惯帮我节省了无数个在深夜排查存储故障的时间。
返回列表