現(xiàn)動(dòng)態(tài)存儲(chǔ)供應(yīng)的落地實(shí)踐)
1. 項(xiàng)目概述為什么在Kubernetes里用NFS做動(dòng)態(tài)存儲(chǔ)不是“湊合”而是務(wù)實(shí)選擇你剛在生產(chǎn)環(huán)境里部署完一套微服務(wù)Pod跑得挺穩(wěn)日志也進(jìn)了ELK結(jié)果第二天業(yè)務(wù)方甩來一條消息“用戶上傳的Excel模板不見了?!薄榱艘蝗Πl(fā)現(xiàn)某個(gè)處理文件的Job Pod重啟后掛載的臨時(shí)目錄清空了。這時(shí)候你才意識(shí)到容器天生無狀態(tài)但業(yè)務(wù)數(shù)據(jù)必須有落腳地。而Kubernetes原生的emptyDir和hostPath根本扛不住滾動(dòng)更新、跨節(jié)點(diǎn)調(diào)度、權(quán)限隔離這三座大山。于是你開始翻文檔看到PersistentVolumePV和PersistentVolumeClaimPVC這兩個(gè)詞再往下翻又撞上StorageClass——它像一道閘門把底層存儲(chǔ)細(xì)節(jié)和上層應(yīng)用隔開。而NFS就是這道閘門背后最常被選中的“水廠”。NFS不是新技術(shù)但它在Kubernetes生態(tài)里扮演的角色非常特殊它不追求極致IOPS也不標(biāo)榜超低延遲而是用極低的運(yùn)維門檻、天然的多讀多寫支持、以及對POSIX語義的完整兼容成為中小規(guī)模集群里最可靠的共享存儲(chǔ)底座。尤其當(dāng)你面對的是日志歸集、配置熱更新、CI/CD中間產(chǎn)物緩存、或者多個(gè)Pod需要協(xié)同讀寫同一份報(bào)表模板這類場景時(shí)NFS提供的“一個(gè)路徑處處可掛”能力比塊存儲(chǔ)如iSCSI或?qū)ο蟠鎯?chǔ)如S3更直接、更省心。我?guī)н^的幾個(gè)項(xiàng)目里某高校實(shí)驗(yàn)室用NFS支撐20個(gè)Jupyter Notebook實(shí)例共享課程數(shù)據(jù)集某電商中臺(tái)用NFS做灰度發(fā)布時(shí)的配置快照池都跑了一年半沒出過存儲(chǔ)層故障。這不是因?yàn)镹FS多先進(jìn)而是因?yàn)樗銐蚝唵?、足夠透明、足夠可控——你ssh進(jìn)NFS服務(wù)器ls -l一眼就能看清誰在讀、誰在寫、空間還剩多少你改一行/etc/exports加個(gè)sync參數(shù)就能立刻感知到性能變化。這種“所見即所得”的掌控感在云原生世界里反而成了稀缺品。所以“Kubernetes: 利用NFS動(dòng)態(tài)提供后端存儲(chǔ)”這個(gè)標(biāo)題說的不是一項(xiàng)炫技工程而是一套經(jīng)過千錘百煉的落地方法論它把NFS從傳統(tǒng)Linux服務(wù)器上的“配角”升級成Kubernetes調(diào)度體系里的“編排友好型存儲(chǔ)資源”。核心在于“動(dòng)態(tài)提供”四個(gè)字——你不再需要提前在NFS服務(wù)器上手工創(chuàng)建好每個(gè)目錄、再一個(gè)個(gè)定義PV對象而是通過StorageClass聲明“我要什么樣的NFS卷”由nfs-subdir-external-provisioner這個(gè)外部供應(yīng)器自動(dòng)完成目錄創(chuàng)建、PV綁定、權(quán)限設(shè)置一整套動(dòng)作。整個(gè)過程對開發(fā)者完全透明他們只管寫pvc.spec.resources.requests.storage: 5Gi剩下的交給系統(tǒng)。這種解耦讓存儲(chǔ)管理真正回歸到“按需申請、用完釋放”的云原生本意而不是變成運(yùn)維同學(xué)每周定時(shí)巡檢df -h的體力活。2. 整體架構(gòu)設(shè)計(jì)與方案選型邏輯為什么是nfs-subdir-external-provisioner而不是其他2.1 三種NFS集成路徑的硬核對比在Kubernetes里對接NFS業(yè)界其實(shí)有三條主流路徑每條背后都是不同的權(quán)衡取舍純靜態(tài)PV方式管理員手動(dòng)在NFS服務(wù)器上創(chuàng)建目錄如/data/app-logs再在K8s里定義對應(yīng)PVnfs.path: /data/app-logs最后由PVC去綁定。這種方式最原始但勝在絕對可控。我見過某金融后臺(tái)系統(tǒng)堅(jiān)持用這套方案理由很實(shí)在審計(jì)要求所有存儲(chǔ)路徑必須提前審批、留痕、備案自動(dòng)創(chuàng)建目錄等于繞過流程。但代價(jià)也很明顯——每次上線新服務(wù)都要走工單、等審批、人工執(zhí)行平均耗時(shí)2天。當(dāng)集群里PVC數(shù)量超過50個(gè)光是PV YAML文件的版本管理就讓人頭皮發(fā)麻。FlexVolume或CSI插件直連比如早期用nfs-clientFlexVolume或者現(xiàn)在更主流的nfs-csi-driver。它們讓Kubelet能直接和NFS服務(wù)器通信跳過PV/PVC抽象層。好處是性能略優(yōu)少一層轉(zhuǎn)發(fā)但問題更致命FlexVolume已被K8s官方棄用而CSI驅(qū)動(dòng)雖然功能全卻要求你在每個(gè)Node上安裝二進(jìn)制、配置RBAC、還要處理NFS客戶端內(nèi)核模塊兼容性比如CentOS 7默認(rèn)的nfs-utils版本太老掛載NFSv4.1會(huì)報(bào)錯(cuò)。去年幫某物流平臺(tái)排查一個(gè)“部分Node掛載失敗”的問題折騰三天才發(fā)現(xiàn)是某批新采購的服務(wù)器BIOS里禁用了CONFIG_NFS_V4_1選項(xiàng)。External Provisioner動(dòng)態(tài)供應(yīng)也就是本項(xiàng)目采用的nfs-subdir-external-provisioner方案。它本質(zhì)是個(gè)獨(dú)立的Deployment以Sidecar模式運(yùn)行在集群里監(jiān)聽PVC創(chuàng)建事件然后調(diào)用NFS服務(wù)器的mkdir和exportfs命令生成新目錄并反向創(chuàng)建PV對象。它的優(yōu)勢不是性能而是抽象干凈、依賴極簡、調(diào)試直觀。整個(gè)供應(yīng)器只依賴一個(gè)ServiceAccount權(quán)限、一個(gè)NFS Server地址、一個(gè)基礎(chǔ)路徑如/data/k8s-volumes沒有Node級組件沒有內(nèi)核模塊要求升級只需替換鏡像。更重要的是它的日志就是你的操作記錄——kubectl logs -f nfs-provisioner-xxx里能看到每一行msg:creating new volume后面跟著完整的mkdir命令和返回碼故障定位效率遠(yuǎn)超CSI驅(qū)動(dòng)那種“掛載失敗但日志只顯示rpc error”的黑盒體驗(yàn)。提示nfs-subdir-external-provisioner并非K8s官方維護(hù)而是由社區(qū)項(xiàng)目kubernetes-sigs/nfs-subdir-external-provisioner孵化。它之所以成為事實(shí)標(biāo)準(zhǔn)是因?yàn)樗珳?zhǔn)卡在了“夠用”和“不過度設(shè)計(jì)”的黃金分割點(diǎn)上——不支持快照、不支持在線擴(kuò)容、不支持多租戶配額但正因如此代碼只有不到2000行Go漏洞極少升級風(fēng)險(xiǎn)極低。如果你的業(yè)務(wù)不需要這些高級特性強(qiáng)行上CSI反而增加復(fù)雜度。2.2 Provisioner核心組件拆解它到底在做什么nfs-subdir-external-provisioner看似只是一個(gè)Pod但內(nèi)部邏輯非常精巧。我們把它拆成三個(gè)關(guān)鍵角色來看Controller Manager這是主循環(huán)線程持續(xù)調(diào)用K8s API Server的Watch接口監(jiān)聽PersistentVolumeClaim資源的ADDED事件。一旦發(fā)現(xiàn)新PVC就提取其spec.storageClassName匹配到對應(yīng)的StorageClass對象再讀取該StorageClass里定義的provisioner字段必須是k8s-sigs.io/nfs-subdir-external-provisioner和parameters如archiveOnDelete: false。這一步?jīng)Q定了“誰來供應(yīng)”和“按什么規(guī)則供應(yīng)”。Volume Provisioner拿到PVC元數(shù)據(jù)后它會(huì)生成一個(gè)唯一卷名格式為namespace-pvc-name-random-string然后拼接出NFS服務(wù)器上的完整路徑如/data/k8s-volumes/default-myapp-pvc-abc123。接著它通過exec.Command(ssh, ...)或exec.Command(mount, ...)方式取決于配置連接NFS服務(wù)器執(zhí)行mkdir -p創(chuàng)建目錄并調(diào)用exportfs -i -o rw,sync,no_root_squash nfs-server:/data/k8s-volumes/default-myapp-pvc-abc123導(dǎo)出該路徑。注意這里no_root_squash是關(guān)鍵——它允許K8s集群內(nèi)的root用戶即Pod以root身份運(yùn)行時(shí)在NFS目錄里擁有真實(shí)root權(quán)限否則會(huì)出現(xiàn)Permission denied錯(cuò)誤。很多初學(xué)者卡在這一步以為是K8s權(quán)限問題其實(shí)是NFS導(dǎo)出參數(shù)沒配對。PV Creator目錄創(chuàng)建成功后Provisioner會(huì)構(gòu)造一個(gè)PersistentVolume對象填入spec.nfs.serverNFS服務(wù)器IP、spec.nfs.path剛創(chuàng)建的路徑、spec.capacity.storage等于PVC申請的大小、spec.accessModes繼承自PVC最后調(diào)用API Server的Create接口提交。此時(shí)K8s Scheduler會(huì)自動(dòng)將這個(gè)PV和PVC綁定后續(xù)Pod掛載時(shí)Kubelet就按標(biāo)準(zhǔn)流程發(fā)起NFS掛載請求。這個(gè)流程里最值得玩味的設(shè)計(jì)是Provisioner本身不參與實(shí)際的數(shù)據(jù)讀寫它只負(fù)責(zé)“造房子”和“發(fā)鑰匙”。真正的掛載、卸載、IO轉(zhuǎn)發(fā)全部由Kubelet內(nèi)置的NFS客戶端完成。這就意味著你完全可以用kubectl delete pvc my-pvc刪除一個(gè)PVCProvisioner會(huì)自動(dòng)清理NFS服務(wù)器上的對應(yīng)目錄如果archiveOnDelete設(shè)為false而整個(gè)過程對正在運(yùn)行的Pod零影響——因?yàn)镻od掛載的是NFS路徑不是Provisioner的Pod。3. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)從NFS服務(wù)器配置到StorageClass定義3.1 NFS服務(wù)器端安全與性能的平衡術(shù)NFS服務(wù)器不是隨便裝個(gè)nfs-kernel-server就能用的它需要針對K8s場景做三處關(guān)鍵加固目錄結(jié)構(gòu)規(guī)劃不要把所有動(dòng)態(tài)卷都塞進(jìn)/export根目錄下。我建議分三級路徑/data/k8s-volumes/cluster-name/namespace。比如/data/k8s-volumes/prod-cluster/default。這樣做的好處是1exportfs導(dǎo)出時(shí)可以精確控制每個(gè)命名空間的權(quán)限/data/k8s-volumes/prod-cluster/default *(rw,sync,no_root_squash)2運(yùn)維巡檢時(shí)du -sh /data/k8s-volumes/prod-cluster/*一眼看出哪個(gè)命名空間占空間最多3未來要做存儲(chǔ)配額可以直接在namespace目錄上用xfs_quota限制。exports配置的魔鬼細(xì)節(jié)/etc/exports里這一行看似簡單實(shí)則暗藏玄機(jī)/data/k8s-volumes/prod-cluster *(rw,sync,no_root_squash,subtree_check,fsid0)sync強(qiáng)制同步寫入避免NFS服務(wù)器崩潰導(dǎo)致數(shù)據(jù)丟失。雖然比async慢10%~15%但對日志、配置類場景數(shù)據(jù)一致性遠(yuǎn)比吞吐量重要。no_root_squash必須開啟K8s里絕大多數(shù)容器以UID 0root運(yùn)行如果關(guān)閉此選項(xiàng)NFS會(huì)把root映射成nobody用戶導(dǎo)致Pod內(nèi)touch /mnt/test.txt失敗。但這也帶來風(fēng)險(xiǎn)——惡意Pod可能通過chown root:root提權(quán)。緩解方案是在Provisioner的parameters里加上mountOptions: [nfsvers4.1,hard,timeo600]讓掛載更健壯同時(shí)在NFS服務(wù)器上啟用rpcbind的防火墻白名單只允許K8s Node IP段訪問。subtree_check開啟子樹檢查防止客戶端繞過導(dǎo)出路徑訪問上級目錄。雖然會(huì)略微降低性能但安全收益遠(yuǎn)大于成本。fsid0為整個(gè)導(dǎo)出樹指定唯一文件系統(tǒng)ID避免NFS客戶端混淆多個(gè)導(dǎo)出路徑。內(nèi)核參數(shù)調(diào)優(yōu)在NFS服務(wù)器上執(zhí)行以下命令解決高并發(fā)掛載時(shí)的Stale file handle錯(cuò)誤# 增加NFS服務(wù)器的TCP連接隊(duì)列長度 echo net.core.somaxconn 65535 /etc/sysctl.conf # 提高NFS服務(wù)器處理RPC請求的線程數(shù)Ubuntu/Debian echo RPCBIND_OPTIONS--manage-gids /etc/default/rpcbind systemctl restart rpcbind # 對于RHEL/CentOS修改/etc/sysconfig/nfs echo RPCNFSDCOUNT32 /etc/sysconfig/nfs systemctl restart nfs-server這些參數(shù)不是憑空寫的。去年某視頻平臺(tái)在壓測時(shí)100個(gè)Pod同時(shí)申請PVC前20個(gè)成功后80個(gè)全卡在Pending狀態(tài)。抓包發(fā)現(xiàn)是rpcbind的連接隊(duì)列溢出ss -s | grep tcp:顯示orphan連接數(shù)爆表。調(diào)大somaxconn后問題消失。3.2 Kubernetes側(cè)StorageClass與Provisioner部署的黃金參數(shù)StorageClass是整個(gè)動(dòng)態(tài)供應(yīng)的“憲法”它的每一個(gè)字段都直接影響PVC的行為apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # 必須和Provisioner Deployment里spec.template.spec.containers[0].args[0]一致 parameters: archiveOnDelete: false # 關(guān)鍵設(shè)為true會(huì)在刪除PVC時(shí)重命名目錄為archived-name設(shè)為false則徹底刪除 onDelete: delete # 配合archiveOnDelete決定PV被回收時(shí)的動(dòng)作 pathPattern: ${.PVC.namespace}/${.PVC.name} # 動(dòng)態(tài)生成NFS路徑的模板${.PVC.namespace}會(huì)被替換成PVC所在命名空間 reclaimPolicy: Delete # PV回收策略必須是Delete或Retain不能是Recycle已廢棄 volumeBindingMode: Immediate # 立即綁定PVC創(chuàng)建后立刻觸發(fā)Provisioner也可設(shè)為WaitForFirstConsumer延遲到Pod創(chuàng)建時(shí)再綁定適合需要拓?fù)涓兄膱鼍捌渲衟athPattern參數(shù)最容易被忽略但它決定了你的NFS目錄結(jié)構(gòu)是否可管理。默認(rèn)值是${.PVC.namespace}/${.PVC.name}這意味著PVCdefault/my-app-logs會(huì)映射到NFS路徑/data/k8s-volumes/prod-cluster/default/my-app-logs。但如果你的業(yè)務(wù)要求所有日志卷必須放在/logs子目錄下可以改成logs/${.PVC.namespace}-${.PVC.name}。注意pathPattern里不能出現(xiàn)..或絕對路徑符號(hào)Provisioner會(huì)校驗(yàn)并拒絕非法模式。Provisioner Deployment的部署也有講究。官方Y(jié)AML里args部分通常長這樣args: - -provisionercluster.local/nfs-client這里的cluster.local是K8s集群的域名后綴必須和你集群的實(shí)際clusterDomain一致可通過kubectl get cm -n kube-system coredns -o yaml查看。如果填錯(cuò)Provisioner會(huì)一直報(bào)failed to get server version錯(cuò)誤因?yàn)檫B不上API Server。另外resources.requests.memory建議至少設(shè)為128Mi否則在高負(fù)載集群里Provisioner Pod可能被OOMKilled導(dǎo)致PVC創(chuàng)建失敗。注意Provisioner的ServiceAccount必須有足夠權(quán)限。最小化RBAC清單如下apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: nfs-provisioner-runner rules: - apiGroups: [] resources: [persistentvolumes] verbs: [get, list, watch, create, delete] - apiGroups: [] resources: [persistentvolumeclaims] verbs: [get, list, watch, update] - apiGroups: [storage.k8s.io] resources: [storageclasses] verbs: [get, list, watch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: run-nfs-provisioner subjects: - kind: ServiceAccount name: nfs-provisioner namespace: default roleRef: kind: ClusterRole name: nfs-provisioner-runner apiGroup: rbac.authorization.k8s.io3.3 PVC與Pod掛載那些文檔里不會(huì)寫的細(xì)節(jié)定義一個(gè)PVC看似簡單但有幾個(gè)坑必須避開apiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-app-logs namespace: default spec: accessModes: - ReadWriteMany # 關(guān)鍵NFS必須用RWX不能用ReadWriteOnceRWO resources: requests: storage: 10Gi storageClassName: nfs-client # 必須和上面定義的StorageClass name一致accessModes必須是ReadWriteManyRWX這是NFS區(qū)別于塊存儲(chǔ)的核心特征。ReadWriteOnceRWO意味著卷只能被一個(gè)Node掛載而NFS服務(wù)器允許多個(gè)客戶端同時(shí)掛載同一路徑所以K8s里NFS PV的accessModes永遠(yuǎn)是RWX。如果你誤寫了RWOPVC會(huì)一直處于Pending狀態(tài)kubectl describe pvc會(huì)顯示no persistent volumes available for this claim and no storage class is set——這不是沒PV而是PV的accessModes和PVC不匹配。存儲(chǔ)大小只是“軟限制”NFS本身不支持配額除非你額外配xfs_quota所以requests.storage: 10Gi只是告訴Provisioner“請給我建個(gè)目錄”并不阻止Pod往里面寫100Gi數(shù)據(jù)。真正的空間控制得靠NFS服務(wù)器端的quota或監(jiān)控告警。我習(xí)慣在NFS服務(wù)器上部署node_exporter用Prometheus采集node_filesystem_avail_bytes{mountpoint~/data/k8s-volumes/.*}指標(biāo)當(dāng)可用空間低于15%時(shí)觸發(fā)企業(yè)微信告警。Pod掛載PVC時(shí)還有一個(gè)隱藏技巧利用subPath實(shí)現(xiàn)單個(gè)PVC多用途。比如一個(gè)10Gi的PVC你想讓W(xué)eb容器寫日志到/var/log/nginx讓Sidecar容器收集日志到/logs可以這樣寫volumeMounts: - name: app-storage mountPath: /var/log/nginx subPath: nginx-logs - name: app-storage mountPath: /logs subPath: collected-logs volumes: - name: app-storage persistentVolumeClaim: claimName: my-app-logs這樣兩個(gè)容器實(shí)際共享同一個(gè)NFS目錄下的不同子目錄既節(jié)省PV數(shù)量又避免權(quán)限沖突。subPath的另一個(gè)妙用是當(dāng)PVC里已有歷史數(shù)據(jù)而你只想讓Pod讀取其中某個(gè)子目錄時(shí)用subPath可以精準(zhǔn)定位不用把整個(gè)卷掛載進(jìn)來污染容器根文件系統(tǒng)。4. 實(shí)操過程與核心環(huán)節(jié)實(shí)現(xiàn)從零部署到驗(yàn)證全流程4.1 環(huán)境準(zhǔn)備與前置檢查清單在動(dòng)手之前務(wù)必完成以下五項(xiàng)檢查否則90%的失敗都源于這里NFS服務(wù)器連通性驗(yàn)證從任意一個(gè)K8s Node上執(zhí)行# 檢查NFS服務(wù)是否響應(yīng) showmount -e nfs-server-ip # 嘗試手動(dòng)掛載模擬Kubelet行為 mkdir -p /mnt/test-nfs mount -t nfs4 -o vers4.1,hard,intr,rsize1048576,wsize1048576 nfs-server-ip:/data/k8s-volumes/prod-cluster /mnt/test-nfs echo test /mnt/test-nfs/hello.txt cat /mnt/test-nfs/hello.txt umount /mnt/test-nfs如果showmount超時(shí)檢查NFS服務(wù)器防火墻ufw status或firewall-cmd --list-all是否放行2049/tcp和111/tcp端口如果mount失敗且報(bào)Operation not permitted檢查Node內(nèi)核是否加載nfs模塊lsmod | grep nfs。K8s集群版本確認(rèn)nfs-subdir-external-provisionerv4.0.2要求K8s 1.20。執(zhí)行kubectl version --short如果Server版本低于1.20必須降級使用v3.x分支。Provisioner鏡像選擇官方鏡像registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2在國內(nèi)拉取極慢。我推薦用阿里云鏡像加速kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/nfs-subdir-external-provisioner/master/deploy/class.yaml # 修改deployment.yaml將image改為 # image: registry.cn-hangzhou.aliyuncs.com/google_containers/nfs-subdir-external-provisioner:v4.0.2Namespace隔離Provisioner默認(rèn)部署在default命名空間。生產(chǎn)環(huán)境強(qiáng)烈建議新建storage命名空間并修改所有YAML里的namespace: storage避免和業(yè)務(wù)Pod混在一起。NFS服務(wù)器磁盤健康執(zhí)行smartctl -a /dev/sdb假設(shè)/data在sdb上檢查Reallocated_Sector_Ct和Current_Pending_Sector是否為0。曾有個(gè)案例NFS服務(wù)器硬盤有壞道導(dǎo)致Provisioner創(chuàng)建目錄時(shí)偶爾失敗錯(cuò)誤日志里只顯示mkdir: cannot create directory排查三天才發(fā)現(xiàn)是硬件問題。4.2 分步部署手把手帶你走通全流程第一步部署Provisioner含RBAC保存以下內(nèi)容為provisioner-deploy.yaml--- apiVersion: v1 kind: ServiceAccount metadata: name: nfs-provisioner namespace: storage --- # RBAC內(nèi)容同3.2節(jié)最小化RBAC清單 --- apiVersion: apps/v1 kind: Deployment metadata: name: nfs-provisioner namespace: storage spec: replicas: 1 selector: matchLabels: app: nfs-provisioner strategy: type: Recreate template: metadata: labels: app: nfs-provisioner spec: serviceAccount: nfs-provisioner containers: - name: nfs-provisioner image: registry.cn-hangzhou.aliyuncs.com/google_containers/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 # 替換為你的NFS服務(wù)器IP - name: NFS_PATH value: /data/k8s-volumes/prod-cluster # 替換為你的基礎(chǔ)路徑 volumes: - name: nfs-client-root nfs: server: 192.168.1.100 path: /data/k8s-volumes/prod-cluster執(zhí)行kubectl apply -f provisioner-deploy.yaml。等待Pod就緒kubectl get pods -n storage -l appnfs-provisioner # 應(yīng)看到 STATUSRunningREADY1/1 kubectl logs -n storage deploy/nfs-provisioner | head -20 # 正常日志應(yīng)包含 Starting NFS Subdir External Provisioner第二步創(chuàng)建StorageClass保存為storageclass.yamlapiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client annotations: storageclass.kubernetes.io/is-default-class: true # 設(shè)為默認(rèn)省去PVC里寫storageClassName provisioner: k8s-sigs.io/nfs-subdir-external-provisioner parameters: archiveOnDelete: false pathPattern: ${.PVC.namespace}/${.PVC.name} reclaimPolicy: Delete volumeBindingMode: Immediate執(zhí)行kubectl apply -f storageclass.yaml。驗(yàn)證kubectl get sc # 輸出應(yīng)包含 nfs-client k8s-sigs.io/nfs-subdir-external-provisioner Delete Immediate false第三步創(chuàng)建測試PVC與Pod保存為test-app.yamlapiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-pvc namespace: default spec: accessModes: - ReadWriteMany resources: requests: storage: 1Gi --- apiVersion: v1 kind: Pod metadata: name: test-pod namespace: default spec: containers: - name: nginx image: nginx:alpine volumeMounts: - name: nfs-volume mountPath: /usr/share/nginx/html volumes: - name: nfs-volume persistentVolumeClaim: claimName: test-pvc執(zhí)行kubectl apply -f test-app.yaml。觀察狀態(tài)kubectl get pvc -n default # 應(yīng)看到 STATUSBoundVOLUMEpvc-xxxxxx自動(dòng)生成的PV名 kubectl get pv # 找到對應(yīng)PV檢查SPEC.NFS.PATH是否為/data/k8s-volumes/prod-cluster/default/test-pvc-xxxxxx kubectl exec -it test-pod -- sh -c echo Hello from NFS /usr/share/nginx/html/index.html # 在Node上檢查NFS服務(wù)器ls -l /data/k8s-volumes/prod-cluster/default/test-pvc-xxxxxx/ # 應(yīng)看到index.html文件且屬主為nobody:nogroupNFS默認(rèn)映射第四步壓力測試與邊界驗(yàn)證創(chuàng)建10個(gè)并發(fā)PVC驗(yàn)證Provisioner穩(wěn)定性for i in {1..10}; do cat EOF | kubectl apply -f - apiVersion: v1 kind: PersistentVolumeClaim metadata: name: stress-pvc-$i namespace: default spec: accessModes: - ReadWriteMany resources: requests: storage: 500Mi storageClassName: nfs-client EOF done等待2分鐘執(zhí)行kubectl get pvc -n default | grep stress-pvc | wc -l # 應(yīng)輸出10 kubectl get pv | grep stress-pvc | wc -l # 應(yīng)輸出10 # 登錄NFS服務(wù)器檢查目錄數(shù)量 ls -l /data/k8s-volumes/prod-cluster/default/ | grep stress-pvc | wc -l # 應(yīng)輸出10如果某個(gè)PVC卡在Pending立即看Provisioner日志kubectl logs -n storage deploy/nfs-provisioner --since1m | grep -A5 -B5 stress-pvc-5 # 常見錯(cuò)誤mkdir: cannot create directory ‘/data/k8s-volumes/prod-cluster/default/stress-pvc-5’: Permission denied # 解決檢查NFS服務(wù)器上/data/k8s-volumes/prod-cluster目錄權(quán)限應(yīng)為777或755且屬主為nfsnobody4.3 生產(chǎn)級增強(qiáng)監(jiān)控、備份與故障演練動(dòng)態(tài)供應(yīng)只是起點(diǎn)生產(chǎn)環(huán)境必須配套三件事監(jiān)控Provisioner健康度在Prometheus里添加以下指標(biāo)抓取- job_name: nfs-provisioner static_configs: - targets: [nfs-provisioner-service:8080] # Provisioner自帶/metrics端點(diǎn) metrics_path: /metrics關(guān)鍵告警規(guī)則nfs_provisioner_pvc_create_total{statuserror} 0PVC創(chuàng)建失敗立即告警rate(nfs_provisioner_pvc_create_duration_seconds_sum[5m]) / rate(nfs_provisioner_pvc_create_duration_seconds_count[5m]) 30平均創(chuàng)建耗時(shí)超30秒說明NFS服務(wù)器IO瓶頸NFS目錄自動(dòng)備份在NFS服務(wù)器上配置rsync定時(shí)任務(wù)每天凌晨2點(diǎn)同步/data/k8s-volumes/prod-cluster到異地備份機(jī)# /etc/cron.d/nfs-backup 0 2 * * * root rsync -avz --delete /data/k8s-volumes/prod-cluster/ backup-userbackup-server:/backup/nfs-prod/注意--delete參數(shù)確保備份目錄和源目錄完全一致但首次運(yùn)行前務(wù)必手動(dòng)驗(yàn)證rsync -ndry-run。故障演練腳本編寫一個(gè)failover-test.sh模擬NFS服務(wù)器宕機(jī)# 1. 記錄當(dāng)前PVC狀態(tài) kubectl get pvc -A before-failover.txt # 2. 在NFS服務(wù)器上停服務(wù) systemctl stop nfs-server # 3. 等待30秒檢查Pod是否受影響 kubectl get pods -A | grep Running | wc -l # 應(yīng)基本不變NFS客戶端有重試機(jī)制 # 4. 恢復(fù)服務(wù) systemctl start nfs-server # 5. 驗(yàn)證Pod能否繼續(xù)IO kubectl exec test-pod -- sh -c date /usr/share/nginx/html/timestamp.log實(shí)測結(jié)果NFS客戶端默認(rèn)重試1000次間隔從0.1秒指數(shù)增長總超時(shí)約3分鐘。這意味著只要NFS服務(wù)器在3分鐘內(nèi)恢復(fù)業(yè)務(wù)Pod完全無感。這個(gè)數(shù)據(jù)比任何文檔都管用。5. 常見問題與排查技巧實(shí)錄那些踩過的坑我都幫你記下來了5.1 PVC長期Pending不只是網(wǎng)絡(luò)問題這是最高頻的故障現(xiàn)象是kubectl get pvc顯示Pendingkubectl describe pvc里Events為空或只有waiting for a volume to be created。別急著查網(wǎng)絡(luò)先按順序排查排查步驟檢查命令預(yù)期結(jié)果常見原因1. Provisioner Pod是否Runningkubectl get pods -n storage -l appnfs-provisionerSTATUSRunningPod CrashLoopBackOff通常是RBAC權(quán)限不足或NFS服務(wù)器地址填錯(cuò)2. Provisioner日志是否有錯(cuò)誤kubectl logs -n storage deploy/nfs-provisioner --tail50無ERROR或panic日志里出現(xiàn)failed to create volume: mkdir: permission denied說明NFS服務(wù)器目錄權(quán)限不對3. StorageClass是否存在且名稱匹配kubectl get sc輸出包含nfs-clientPVC里storageClassName拼寫錯(cuò)誤如寫成nfs-client-14. PVC的accessModes是否為RWXkubectl get pvc test-pvc -o yaml | grep accessModesaccessModes: [ReadWriteMany]誤寫為ReadWriteOnceK8s無法匹配PV5. NFS服務(wù)器是否能被Provisioner Pod訪問kubectl exec -n storage deploy/nfs-provisioner -- ping -c 3 192.168.1.10064 bytes from ...Provisioner Pod所在Node的網(wǎng)絡(luò)策略NetworkPolicy阻止了出向流量我遇到過最詭異的一次所有檢查都通過但PVC就是Pending。最后發(fā)現(xiàn)是NFS服務(wù)器的/etc/exports里用了主機(jī)名nfs-server.local代替IP而Provisioner Pod的/etc/resolv.conf里DNS服務(wù)器配置錯(cuò)誤導(dǎo)致showmount -e nfs-server.local超時(shí)。解決方案很簡單/etc/exports里一律用IP永不寫主機(jī)名。5.2 Pod掛載后Permission DeniedNFS權(quán)限的迷宮Pod里執(zhí)行touch /mnt/test.txt報(bào)Permission denied90%的情況是NFS的root_squash惹的禍。但解決方案不止一種方案A推薦NFS服務(wù)器端關(guān)閉root_squash修改/etc/exports把*(rw,sync)改成*(rw,sync,no_root_squash)然后exportfs -ra重載。這是最直接的解法適用于內(nèi)網(wǎng)可信環(huán)境。方案BPod內(nèi)以非root用戶運(yùn)行在Pod spec里加securityContext: runAsUser: 1001 runAsGroup: 1001 fsGroup: 1001同時(shí)確保NFS服務(wù)器上/data/k8s-volumes/prod-cluster目錄屬主為1001:1001權(quán)限為775。這樣Pod以UID 1001運(yùn)行NFS會(huì)將其映射為真實(shí)UID 1001自然有寫權(quán)限。方案CProvisioner自動(dòng)修復(fù)權(quán)限高級在Provisioner的parameters里加parameters: archiveOnDelete: false pathPattern: ${.PVC.namespace}/${.PVC.name} onDemand: chmod 777 # Provisioner創(chuàng)建目錄后自動(dòng)執(zhí)行chmod注意onDemand參數(shù)需要Provisioner v4.0.0且必須確保NFS服務(wù)器上/data/k8s-volumes/prod-cluster目錄的父目錄有setgid位chmod gs /data/k8s-volumes/prod-cluster否則新目錄的屬組不會(huì)繼承。5.3 動(dòng)態(tài)卷被意外刪除如何找回“消失”的數(shù)據(jù)當(dāng)archiveOnDelete: true時(shí)刪除PVC會(huì)重命名目錄為archived-default-test-pvc-xxxxxx。但如果誤設(shè)為false數(shù)據(jù)就真沒了。這時(shí)候別慌NFS服務(wù)器的/data/k8s-volumes/prod-cluster目錄下可能還有線索**檢查.nfs*臨時(shí)