在Kubernetes生产环境中,Pod本身是无状态的,一旦重启或漂移到其他节点,容器内的数据就会丢失。
如何为Pod提供持久化存储,并且做到多副本共享、动态扩容、安全可靠,是每个K8s运维人员必须掌握的技能。
本文将带你从零开始手把手实操,通过NFS + StorageClass + PV/PVC + Deployment的组合拳,搭建一套完整的K8s持久化存储方案。**从NFS服务配置、StorageClass策略定义,到PV/PVC绑定、Deployment挂载验证,再到扩容测试和故障排查,全程干货,每一行命令都有详解,每一个坑都帮你踩过。**无论你是K8s新手还是想深入理解存储机制的老兵,这篇实战都能让你对K8s存储体系有一个清晰、直观的认知。
了解内容:
可以帮助更好理解后面的配置
- NFS (Network File System): NFS 用于在网络上共享文件。它允许多个 Pod 访问同一个文件系统,适合需要共享存储的场景。
- StorageClass: StorageClass 是 Kubernetes 中用来定义存储类型的一种资源定义。它为 PersistentVolume (PV) 提供了动态配置的能力。通过 StorageClass,可以指定使用 NFS 作为存储后端,并定义存储的属性(如存储类别、访问模式、回收策略等)。
- PersistentVolume (PV): PV 是一个具体的存储资源,它是集群中的一个物理存储。管理员创建 PV 资源以定义可用的存储卷,这些卷不依赖于 Pod 的生命周期。
- PersistentVolumeClaim (PVC): PVC 是用户对 PV 的请求。它相当于一个租赁协议,用户可以声明所需的存储大小、访问模式等。Kubernetes 会根据 PVC 的要求,绑定一个合适的 PV。
- Deployment: Deployment 是用于管理应用程序副本的 Kubernetes 控制器。它可以保证在任何时候都运行所需数量的 Pod,确保应用的高可用性。Deployment 可以通过 PVC 来挂载所需的存储,使得应用可以利用管理的数据。
关系:
StorageClass 定义了一种存储策略。
PV 是具体的存储实现,依据 StorageClass 中的定义来创建。
PVC 用于请求所需的存储,Kubernetes 会查找匹配的 PV 并进行绑定。
Deployment 可通过 PVC 挂载所请求的存储,以供容器应用使用。
这种组合提供了一种灵活的存储管理方式,能够动态地供应和管理存储卷。
配置:
1.NFS
bash
[root@k8s-master01 20]# cat /etc/exports
/opt/nfs 192.168.64.0/24(rw,no_root_squash,sync,no_subtree_check)#之前的
/data/nfs 192.168.64.0/24(rw,sync,no_subtree_check)#这一行
[root@k8s-master01 20]# showmount -e
Export list for k8s-master01:
/data/nfs 192.168.64.0/24
/opt/nfs 192.168.64.0/24
[root@k8s-master01 20]# systemctl is-active nfs-server.service
active
2.StorageClass
bash
[root@k8s-master01 20]# cat 1.nfs-storageclass.yml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-storageclass
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
no-provisioner让PV必须手动创建,Retain保证数据安全不自动删除,WaitForFirstConsumer让PVC等到Pod使用时才绑定,三者配合实现"静态供给+延迟绑定+数据保护"的存储管理策略。
应用&验证
bash
[root@k8s-master01 20]# kubectl apply -f 1.nfs-storageclass.yml
storageclass.storage.k8s.io/nfs-storageclass created
[root@k8s-master01 20]# kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs-storageclass kubernetes.io/no-provisioner Retain WaitForFirstConsumer false 27s
3.PV
bash
[root@k8s-master01 20]# cat 2.nfs-pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv
spec:
accessModes: [ReadWriteMany]
capacity:
storage: 10Gi
volumeMode: Filesystem
storageClassName: nfs-storageclass
persistentVolumeReclaimPolicy: Retain
nfs:
path: /data/nfs
server: 192.168.64.11
应用&验证
bash
[root@k8s-master01 20]# kubectl apply -f 2.nfs-pv.yml
persistentvolume/nfs-pv created
[root@k8s-master01 20]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
nfs-pv 10Gi RWX Retain Available nfs-storageclass <unset>
4.PVC
bash
[root@k8s-master01 20]# cat 3.nfs-pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
spec:
accessModes: [ReadWriteMany]
resources:
requests:
storage: 4Gi
volumeMode: Filesystem
storageClassName: nfs-storageclass
storageClassName是 PV 和 PVC 的"接头暗号",用于将存储请求精准路由到特定类型的存储后端(如 NFS、SSD、Ceph),防止不同存储类型或不同租户的 PV 被错误绑定,同时支持动态供给和静态供给的区分。这个是针对生产环境针对该实验,这里就是指定了该 PV/PVC 属于
nfs-storageclass这个存储类别,只有相同类别的 PV 和 PVC 才能互相绑定,从而实现存储类型的精准匹配与隔离。如果不写一定要都不写,就是也应该可以匹配到。但是为了贴合现实还是写。
应用&验证
bash
[root@k8s-master01 20]# kubectl apply -f 3.nfs-pvc.yml
persistentvolumeclaim/nfs-pvc created
[root@k8s-master01 20]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
nfs-pvc Pending nfs-storageclass <unset> 8s
由于在声明 StorageClass 时,将 volumeBindingMode 设置为 WaitForFirstConsumer ,则需要等待
Pod 使用 PVC 时才绑定 PV,所以现在的PVC是Pending状态,没有绑定对应的Pod。
但如果 volumeBindingMode 设置为 Immediate ,则会立即绑定,显示为Bound
5.Deployment
bash
[root@k8s-master01 20]# cat 4.deploy.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-deploy
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx-app
image: hb.reg.com/k8s/nginx:1.27.4
volumeMounts:
- name: nfs-volume
mountPath: /usr/share/nginx/html
volumes:
- name: nfs-volume #要和挂载的卷名字一样
persistentVolumeClaim: # 固定关键字,表示"这个卷来自PVC"
claimName: nfs-pvc
应用&验证
bash
[root@k8s-master01 20]# kubectl apply -f 4.deploy.yml
deployment.apps/nfs-deploy created
[root@k8s-master01 20]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLAS
nfs-pv 10Gi RWX Retain Bound default/nfs-pvc nfs-storageclass <unset>
[root@k8s-master01 20]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nfs-deploy-f4d77f4b7-n62wb 0/1 ContainerCreating 0 14s
可以看到pv已经成功绑定,但是pod没有成功running
6.解决问题
最方便的是:kubectl describe 来查看,看events就行
bash
[root@k8s-master01 20]# kubectl describe pod nfs-deploy-f4d77f4b7-n62wb
。。。。。。
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 115s default-scheduler Successfully assigned default/nfs-deploy-f4d77f4b7-n62wb to k8s-node0
Warning FailedMount 115s kubelet MountVolume.SetUp failed for volume "nfs-pv" : mount failed: exit sta
Mounting command: mount
Mounting arguments: -t nfs 192.168.64.11:/data/nfs /var/lib/kubelet/pods/8fdfec88-c4c9-4bab-83cb-95451ba83537/volumes/
Output: Created symlink '/run/systemd/system/remote-fs.target.wants/rpc-statd.service' → '/usr/lib/systemd/system/rpc-
mount.nfs: mounting 192.168.64.11:/data/nfs failed, reason given by server: No such file or directory
Warning FailedMount 51s (x7 over 114s) kubelet MountVolume.SetUp failed for volume "nfs-pv" : mount failed: exit
Mounting command: mount
Mounting arguments: -t nfs 192.168.64.11:/data/nfs /var/lib/kubelet/pods/8fdfec88-c4c9-4bab-83cb-95451ba83537/volumes/
Output: mount.nfs: mounting 192.168.64.11:/data/nfs failed, reason given by server: No such file or directory
可以看到问题就是没有/data/nfs 这个目录导致挂载不上
bash
[root@k8s-master01 20]# mkdir -p /data/nfs
7.验证nfs,pod等资源
验证
bash
[root@k8s-master01 20]# kubectl delete -f 4.deploy.yml
deployment.apps "nfs-deploy" deleted from default namespace
[root@k8s-master01 20]# kubectl apply -f 4.deploy.yml
deployment.apps/nfs-deploy created
[root@k8s-master01 20]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nfs-deploy-f4d77f4b7-djvpq 1/1 Running 0 11s
现在就可行了。
curl验证:
bash
[root@k8s-master01 20]# kubectl get pod -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nfs-deploy-f4d77f4b7-djvpq 1/1 Running 0 7m48s 10.244.85.254 k8s-node01 <none> <none>
[root@k8s-master01 20]# curl 10.244.85.254
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.27.4</center>
</body>
</html>
验证nfs:
自定义首页在共享目录,看能不能自己加载到容器
bash
[root@k8s-master01 20]# echo welcome mynginx,nfs active > /data/nfs/index.html
[root@k8s-master01 20]# curl 10.244.85.254
welcome mynginx,nfs active
#从容器内部也可以验证
[root@k8s-master01 20]# kubectl exec -it nfs-deploy-f4d77f4b7-djvpq -- /bin/bash
root@nfs-deploy-f4d77f4b7-djvpq:/#
root@nfs-deploy-f4d77f4b7-djvpq:/# cat /usr/share/nginx/html/index.html
welcome mynginx,nfs active
root@nfs-deploy-f4d77f4b7-djvpq:/#
8.案例链路图(总结):

整个实验流程分为五个阶段:
首先在NFS服务器上创建共享目录/data/nfs并启动NFS服务,作为存储后端;
然后创建StorageClass定义存储策略(静态供给、保留数据、延迟绑定);
接着管理员手动创建PV,指定NFS服务器IP和共享路径,并声明storageClassName: nfs-storageclass;
之后用户创建PVC,通过相同的storageClassName和匹配的accessModes(RWX)+容量(4Gi≤10Gi)与PV绑定(但由于WaitForFirstConsumer,PVC会保持Pending直到Pod使用);
最后创建Deployment,Pod通过volumes.persistentVolumeClaim引用PVC,触发PV-PVC绑定,Pod成功挂载NFS存储到容器内/usr/share/nginx/html,验证通过自定义index.html页面确认共享存储生效
9.一些扩展:
不知道大家有没有印象刚刚设置deployment资源清单时,我没有设置有几个副本,那么会默认一个,如果在生产环境中一定不是一个。
在生产环境,可能因为改业务的浮动,我们会动态调整副本数量(eg.写一个shell脚本来监控管理,某个值在什么范围内副本增加/减少到几个)。
我们现在来模拟一下,但是因为这个只是简单的实验环境,没有具体业务,所以直接使用命令行来增加副本。
目的是了解真实业务&理解pod/Deplyment和PV之间的联系
bash
[root@k8s-master01 20]# kubectl scale deployment nfs-deploy --replicas=5
deployment.apps/nfs-deploy scaled
[root@k8s-master01 20]#
[root@k8s-master01 20]#
[root@k8s-master01 20]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nfs-deploy-f4d77f4b7-b7j92 1/1 Running 0 7s
nfs-deploy-f4d77f4b7-c9582 1/1 Running 0 7s
nfs-deploy-f4d77f4b7-djvpq 1/1 Running 0 20m
nfs-deploy-f4d77f4b7-plstj 1/1 Running 0 7s
nfs-deploy-f4d77f4b7-rhwbt 1/1 Running 0 7s
Deployment管Pod数量,PVC管存储申请,PV管实际存储,Pod通过PVC使用PV,多个Pod可共享一个PV(需RWX支持)。
Tips:
重点是Pod通过PVC使用PV,可以返回定义Pod的清单(Deployment清单),该清单会定义卷并挂载卷,这个卷会定义PVC,即按照哪个持久卷声明去选择PV(持久卷),而PVC资源清单就会定义accessModes是什么,一些可以共享PV,一些只能一个PV给一个Pod使用(比如RWO模式)。
而PVC会自己去匹配符合的PV,或者直接在PVC去声明要使用哪个PV(但是也要符合accessMode+PV实际容量大于等于PVC)。如下
text
方式一(自动匹配):
PVC(无volumeName)→ 系统自动筛选(容量+模式+class一致)→ 绑定最佳PV
方式二(手动指定):
PVC(有volumeName)→ 验证指定PV(容量+模式+class一致)→ 强制绑定该PV