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