别再手动建 PV 了!K8s 动态卷供应从原理到落地:StorageClass + Local Path + NFS 双 Provisioner 实战
摘要:告别手动 apply PV、删应用还得上节点清目录。K8s 动态卷供应(StorageClass + PVC + PV)让存储自动创建、自动绑定、自动回收。本文讲透原理与核心配置,并落地两套主流 Provisioner:Local Path 与 NFS,每步带验证。写完这条 PVC,水(PV)就自动来了。
一、为什么需要动态卷
以前创建存储卷(PV)都需要管理员手动操作,这类卷被称为静态卷。那么 Kubernetes 能不能"智能"管理卷呢?比如需要用卷时自动创建,不用时自动清理?
答案是可以的 ------ 这就是**动态卷供应(Dynamic Volume Provisioning)**的能力。它彻底省去了集群管理员提前手动配置存储的工作,用户只要提出存储需求,系统就会自动创建对应的存储卷。
二、动态卷供应流程
三步走,理解了这个流程,后面所有 YAML 都不用背:
- 管理员先创建"存储模板"(StorageClass ),指定用哪个"造卷工具"(Provisioner,制备器)来创建卷;
- 用户创建"存储申请单"(PVC)时,指定要用哪个"存储模板";
- 系统收到申请后,会让模板绑定的"造卷工具"自动创建一个 PV,并且把 PV 和用户的 PVC 绑定,用户直接用 PVC 就行。
三、StorageClass 存储类别
StorageClass 就像管理员定义的"存储套餐" ------ 不同套餐对应不同的存储类型、服务质量(比如读写速度)、备份策略等。Kubernetes 不关心套餐叫什么,只负责按套餐规则造卷。
两个必须先记住的规矩:
- 命名很重要:用户创建 PVC 时,要通过这个名字选择对应的存储套餐;
- 创建后不能改:StorageClass 一旦创建,名字和参数就没法修改了,要改只能删掉重建。
核心配置
每个"存储套餐"都包含以下核心信息,用来指导系统创建 PV:
1. provisioner(造卷工具,必填)
指定用哪个工具创建 PV。Kubernetes 自带一些 Provisioner,名字以 kubernetes.io 开头(比如 AWS EBS、Azure Disk);也可以使用第三方提供的 Provisioner(比如 NFS 没有内置,需要自己装),按 Kubernetes 规则运行即可。
| 存储类型 | 是否内置 | 说明 |
|---|---|---|
| AWSElasticBlockStore | ✅ 是 | 亚马逊云硬盘 |
| AzureFile/AzureDisk | ✅ 是 | 微软云文件 / 硬盘 |
| Ceph RBD | ❌ 否 | Ceph 块存储 |
| CephFS | ❌ 否 | 需外部制备器 |
| iSCSI | ❌ 否 | 需外部制备器 |
| Glusterfs | ❌ 否 | 分布式文件系统 |
| NFS | ❌ 否 | 网络文件系统,需装外部制备器 |
| Local | ❌ 否 | 本地文件系统,需装外部制备器 |
2. reclaimPolicy(PV 回收策略)
- Delete(默认):PVC 删除后,PV 也自动删,对应的后端存储(比如云硬盘、NFS 目录)也会清掉;
- Retain:PVC 删除后,PV 保留下来,后端存储的数据也不删,管理员可以手动处理。
3. volumeBindingMode(绑定时机)
- Immediate(立即绑定):创建 PVC 就造卷,不管 Pod 会不会用;适合存储能被所有节点访问的场景(比如 NFS、云硬盘);
- WaitForFirstConsumer(等使用者):先不造卷,等第一个用这个 PVC 的 Pod 创建后,根据 Pod 要跑的节点造卷;适合本地存储、只能被特定节点访问的存储。
4. mountOptions(挂载附加参数)
指定 PV 挂载时附加的"额外设置",比如权限、调试模式。
⚠️ 注意:如果存储不支持这些选项,造卷会失败;如果选项填错,PV 挂载会失败。
5. parameters
比如存储类型(SSD/HDD)、副本数等,不同工具参数不一样。
基础示例
bash
root@master30:~# cat > storageclass-demo.yaml <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard # 套餐名,用户PVC要填这个
provisioner: kubernetes.io/aws-ebs # 用AWS EBS的造卷工具
parameters:
type: gp2 # 存储类型为gp2(AWS的通用型SSD)
reclaimPolicy: Retain # PVC 不用了保留,不删除
allowVolumeExpansion: true # 允许扩容
mountOptions:
- debug # 挂载时开启调试模式
volumeBindingMode: Immediate # 立即创建并绑定PV
EOF
四、实战:部署 Local Path Provisioner
Local Path Provisioner 是 Rancher 开源、K8s 官方推荐的本地存储动态卷插件,适合需要使用节点本地磁盘的场景。
1. 部署制备器
bash
root@master30:~# wget https://ghfast.top/https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
官方模板 local-path-storage.yaml 核心由 5 类资源 组成,每部分各司其职:
| 资源类型 | 名称 | 核心作用 |
|---|---|---|
| Namespace | local-path-storage | 隔离 Local Path Provisioner 相关资源 |
| ConfigMap | local-path-config | 定义本地存储路径 规则(默认 /opt/local-path-provisioner) |
| ServiceAccount + RBAC | local-path-provisioner | 赋予 Provisioner 操作 PV/PVC 的权限 |
| Deployment | local-path-provisioner | 运行 Local Path Provisioner 核心程序(动态创建本地 PV) |
| StorageClass | local-path | 提供给 PVC 调用的「本地存储模板」 |
核心内容如下(部署前建议先 vim 修改镜像仓库):
bash
root@master30:~# vim local-path-storage.yaml
bash
apiVersion: v1 # K8s 核心 API 版本
kind: Namespace # 资源类型:命名空间(隔离用的大盒子)
metadata: # 元数据
name: local-path-storage # 命名空间名字,Provisioner 全家桶都住这
---
apiVersion: v1 # 核心 API 版本
kind: ServiceAccount # 资源类型:服务账号(Pod 在 K8s 里的"身份证")
metadata:
name: local-path-provisioner-service-account # SA 名字
namespace: local-path-storage # 放在上面那个命名空间里
---
apiVersion: rbac.authorization.k8s.io/v1 # RBAC 权限管理 API 版本
kind: Role # 资源类型:角色(只管一个命名空间的权限)
metadata:
name: local-path-provisioner-role # Role 名字
namespace: local-path-storage # 只在这个命名空间生效
rules: # 权限规则列表
- apiGroups: [""] # 空字符串 = 核心 API 组(pods/nodes/pv 这些基础资源)
resources: ["pods"] # 能操作的资源:Pod
verbs: ["get", "list", "watch", "create", "patch", "update", "delete"] # 增删改查全包
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole # 资源类型:集群角色(全集群范围的权限)
metadata:
name: local-path-provisioner-role # ClusterRole 名字(和上面 Role 同名,但级别不同)
rules:
- apiGroups: [""] # 核心 API 组
resources: ["nodes", "persistentvolumeclaims", "configmaps", "pods", "pods/log"] # 能看节点/PVC/配置表/Pod/Pod日志
verbs: ["get", "list", "watch"] # 只能查,不能改
- apiGroups: [""]
resources: ["persistentvolumes"] # 能操作 PV(动态卷的核心!自动建/删 PV)
verbs: ["get", "list", "watch", "create", "patch", "update", "delete"] # 增删改查全包
- apiGroups: [""]
resources: ["events"] # 事件(记录 PVC 绑定/解绑事件给用户看)
verbs: ["create", "patch"] # 只能写事件
- apiGroups: ["storage.k8s.io"] # 存储专属 API 组
resources: ["storageclasses"] # 能看 StorageClass
verbs: ["get", "list", "watch"] # 只读
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding # 资源类型:角色绑定(把 Role 的权限发给 SA)
metadata:
name: local-path-provisioner-bind
namespace: local-path-storage
roleRef: # ← 左边:从哪个 Role 拿权限
apiGroup: rbac.authorization.k8s.io
kind: Role
name: local-path-provisioner-role
subjects: # ← 右边:权限发给谁
- kind: ServiceAccount
name: local-path-provisioner-service-account
namespace: local-path-storage
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding # 资源类型:集群角色绑定(把 ClusterRole 发给 SA)
metadata:
name: local-path-provisioner-bind
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole # 绑定的是 ClusterRole(全集群级)
name: local-path-provisioner-role
subjects:
- kind: ServiceAccount
name: local-path-provisioner-service-account
namespace: local-path-storage
---
apiVersion: apps/v1 # 应用工作负载 API 版本
kind: Deployment # 资源类型:无状态工作负载(管 Pod 副本)
metadata:
name: local-path-provisioner
namespace: local-path-storage
spec:
replicas: 1 # 只跑 1 个副本(Provisioner 单点足够)
selector:
matchLabels:
app: local-path-provisioner # 管带这个标签的 Pod
template: # Pod 模板(照着这个样子建 Pod)
metadata:
labels:
app: local-path-provisioner
spec:
serviceAccountName: local-path-provisioner-service-account # Pod 用哪张"身份证"去调 API
containers:
- name: local-path-provisioner
image: hub.laoma.cloud/rancher/local-path-provisioner:v0.0.37 # 镜像(已换自己私有加速仓库)
imagePullPolicy: IfNotPresent # 本地有就不重新拉,没有才拉
command: # 容器启动命令
- local-path-provisioner
- --debug # 开 debug 日志,方便排错
- start
- --config
- /etc/config/config.json # 指定配置文件路径
volumeMounts: # 把卷挂载进容器里
- name: config-volume
mountPath: /etc/config/ # 配置文件挂到容器这个目录
env: # 环境变量
- name: POD_NAMESPACE # 把当前命名空间注入进 Pod
valueFrom:
fieldRef:
fieldPath: metadata.namespace # 自动取 Pod 自己的命名空间名
- name: CONFIG_MOUNT_PATH
value: /etc/config/ # 配置文件在容器里的路径
volumes: # 定义 Pod 能用的卷
- name: config-volume
configMap:
name: local-path-config # 从下面那个 ConfigMap 挂配置进来
---
apiVersion: storage.k8s.io/v1 # 存储 API 版本
kind: StorageClass # 资源类型:存储类(动态卷的"模板")
metadata:
name: local-path # SC 名字,PVC 里 storageClassName 就填这个
provisioner: rancher.io/local-path # 用哪个 Provisioner 来造 PV(和 Provisioner Pod 里的对应)
volumeBindingMode: WaitForFirstConsumer # 等 Pod 真正要挂载了再绑 PV(调度感知,Pod 调到哪 PV 就在哪)
reclaimPolicy: Delete # PVC 删了之后 PV 跟着删(数据也一起删)
---
kind: ConfigMap # 资源类型:配置字典(存配置文件用)
apiVersion: v1
metadata:
name: local-path-config
namespace: local-path-storage
data:
config.json: |- # 主配置 JSON(告诉 Provisioner 数据放哪)
{
"nodePathMap":[ # 节点路径映射表
{
"node":"DEFAULT_PATH_FOR_NON_LISTED_NODES", # 没单独列的节点都用默认路径
"paths":["/opt/local-path-provisioner"] # 数据存在节点的这个目录下
}
]
}
setup: |- # 【建卷前】执行的脚本(建目录)
#!/bin/sh
set -eu # 出错就退出
mkdir -m 0777 -p "$VOL_DIR" # 创建卷目录,权限 777(谁都能写)
teardown: |- # 【删卷后】执行的脚本(删目录)
#!/bin/sh
set -eu
rm -rf "$VOL_DIR" # 递归删掉整个卷目录
helperPod.yaml: |- # 辅助 Pod 模板(在目标节点上跑上面两个脚本的"干活小工")
apiVersion: v1
kind: Pod
metadata:
name: helper-pod
spec:
priorityClassName: system-node-critical # 高优先级,节点紧张时也不被驱逐
tolerations: # 容忍污点
- key: node.kubernetes.io/disk-pressure # 磁盘紧张节点也能调度
operator: Exists
effect: NoSchedule
containers:
- name: helper-pod
image: busybox # 用超轻量的 busybox 跑脚本
imagePullPolicy: IfNotPresent
一键部署:
bash
root@master30:~# kubectl apply -f local-path-storage.yaml
root@master30:~# kubectl get sc local-path
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path rancher.io/local-path Delete WaitForFirstConsumer false 31s
root@master30:~# kubectl get all -n local-path-storage
NAME READY STATUS RESTARTS AGE
pod/local-path-provisioner-74b5c4bf98-955sk 1/1 Running 0 63s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/local-path-provisioner 1/1 1 1 63s
NAME DESIRED CURRENT READY AGE
replicaset.apps/local-path-provisioner-74b5c4bf98 1 1 1 63s
2. 验证部署
bash
root@master30:~# kubectl config set-context --current --namespace default
步骤 1:创建 PVC
bash
root@master30:~# cat > local-path-pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: local-path-pvc
namespace: default
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
storageClassName: local-path # 指定 Local Path 的 StorageClass
EOF
root@master30:~# kubectl apply -f local-path-pvc.yaml
root@master30:~# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
local-path-pvc Pending local-path <unset> 6s
注意:此时 PVC 处于 Pending 状态 ------ 因为 StorageClass 是
WaitForFirstConsumer,要等 Pod 关联才会真正创建本地目录。
步骤 2:创建 Deployment
bash
root@master30:~# cat > local-path-deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: webapp
name: webapp
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- image: hub.laoma.cloud/library/nginx
name: nginx
volumeMounts:
- name: webapp-data
mountPath: /usr/share/nginx/html
volumes:
- name: webapp-data
persistentVolumeClaim:
claimName: local-path-pvc
EOF
root@master30:~# kubectl apply -f local-path-deployment.yaml
查看 Pod 与 PVC 状态:
bash
root@master30:~# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
webapp-6d46b54487-6nqmh 1/1 Running 0 114s 10.224.19.21 worker31.laoma.cloud <none> <none>
webapp-6d46b54487-kzkd5 1/1 Running 0 114s 10.224.19.22 worker31.laoma.cloud <none> <none>
root@master30:~# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
local-path-pvc Bound pvc-e992833c-af6a-4d4d-aff5-fc544394db6d 5Gi RWO local-path <unset> 4m41s
写入测试数据并验证:
bash
root@master30:~# kubectl exec webapp-6d46b54487-6nqmh -- bash -c 'echo Hello World From Nginx > /usr/share/nginx/html/index.html'
root@master30:~# ls /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/
index.html
root@master30:~# cat /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/index.html
Hello World From Nginx
步骤 3:清理资源
bash
root@master30:~# kubectl delete deployment webapp
root@master30:~# kubectl delete pvc local-path-pvc
root@master30:~# ls /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/
ls: cannot access '/opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/': No such file or directory
五、实战:部署 NFS Provisioner
Local Path 的缺点是数据绑死在某个节点 ,Pod 漂移到别的节点就读不到数据。生产/测试环境更常用的方案是 NFS 共享存储。
1. 部署 NFS 服务
bash
# master节点部署nfs服务
root@master30:~# apt install -y nfs-kernel-server
root@master30:~# mkdir -m 777 /shares/
root@master30:~# cat << EOF > /etc/exports
/shares *(rw,sync,no_root_squash,no_all_squash,insecure)
EOF
root@master30:~# systemctl restart nfs-server.service
# work节点安装nfs客户端
root@master31和32:~# apt install -y nfs-common
2. 部署制备器
NFS 没有内置制备器,这里我们使用社区经典的外部制备器 nfs-subdir-external-provisioner。
步骤 1:创建命名空间
bash
root@master30:~# kubectl create ns kube-storage
root@master30:~# kubectl config set-context --current --namespace kube-storage
步骤 2:创建 RBAC 权限
bash
root@master30:~# cat > nfs-rbac.yaml <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
namespace: kube-storage
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: nfs-client-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"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "update", "patch"]
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: run-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
namespace: kube-storage
roleRef:
kind: ClusterRole
name: nfs-client-provisioner-runner
apiGroup: rbac.authorization.k8s.io
EOF
root@master30:~# kubectl apply -f nfs-rbac.yaml
步骤 3:部署 NFS Provisioner 应用
bash
root@master30:~# cat > nfs-provisioner.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
namespace: kube-storage
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: nfs-client-provisioner
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
volumes:
- name: nfs-client-root
nfs:
server: 10.1.8.30 # NFS 服务器 IP
path: /shares # NFS 共享路径
containers:
- name: nfs-client-provisioner
image: hub.laoma.cloud/sig-storage/nfs-subdir-external-provisioner:v4.0.2
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: fuseim.pri/ifs
- name: NFS_SERVER
value: 10.1.8.30 # 替换为你的 NFS 服务器 IP
- name: NFS_PATH
value: /shares # 替换为你的共享路径
EOF
root@master30:~# kubectl apply -f nfs-provisioner.yaml
root@master30:~# kubectl get deployments.apps -n kube-storage
NAME READY UP-TO-DATE AVAILABLE AGE
nfs-client-provisioner 1/1 1 1 1m
步骤 4:创建 NFS StorageClass
bash
root@master30:~# cat > nfs-storageclass.yaml <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-storage
namespace: kube-storage
provisioner: fuseim.pri/ifs # 必须与上面 Provisioner 的 PROVISIONER_NAME 一致
parameters:
onDelete: retain # 删除 PVC 保留 NFS 数据
archiveOnDelete: "false"
#reclaimPolicy: Retain
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate
EOF
root@master30:~# kubectl apply -f nfs-storageclass.yaml
root@master30:~# kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs-storage fuseim.pri/ifs Delete Immediate true 2s
3. 验证部署
bash
root@master30:~# kubectl config set-context --current --namespace default
步骤 1:创建 PVC
bash
root@master30:~# cat > nfs-pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: webclaim
namespace: default
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: "nfs-storage"
EOF
root@master30:~# kubectl apply -f nfs-pvc.yaml
root@master30:~# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
webclaim Bound pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc 5Gi RWO nfs-storage <unset> 3s
root@master30:~# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS AGE
pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc 5Gi RWO Delete Bound default/webclaim nfs-storage 65s
对比 Local Path:NFS 由于是
Immediate模式,PVC 一创建就立刻造卷并 Bound,不需要等 Pod。
查看 NFS 共享目录并准备主页:
bash
root@master30:~# ls /shares/
default-webclaim-pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc
root@master30:~# echo Hello World From Nginx > /shares/default-webclaim-pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc/index.html
步骤 2:创建 Deployment 并暴露服务
bash
root@master30:~# cat > nfs-deploy.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: webapp
name: webapp
spec:
replicas: 2
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- image: hub.laoma.cloud/library/nginx
name: nginx
volumeMounts:
- name: webapp-data
mountPath: /usr/share/nginx/html
volumes:
- name: webapp-data
persistentVolumeClaim:
claimName: webclaim
EOF
root@master30:~# kubectl apply -f nfs-deploy.yaml
root@master30:~# kubectl expose deployment webapp --target-port 80 --port 80 --protocol TCP
root@master30:~# kubectl get svc webapp
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
webapp ClusterIP 10.109.140.205 <none> 80/TCP 5m11s
访问测试:
bash
curl 10.109.140.205
Hello World From Nginx
curl 10.109.140.205
Hello World From Nginx
步骤 3:清理资源
bash
root@master30:~# kubectl delete deployments.apps webapp
root@master30:~# kubectl delete pvc webclaim
persistentvolumeclaim "webclaim" deleted
root@master30:~# kubectl get pv
No resources found
root@master30:~# ls /shares/default-webclaim-pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc/
index.html
✅ 保留 NFS provisioner,下一章 StatefulSet 还要用。
Local Path 还是 NFS?怎么选
| 对比项 | Local Path Provisioner | NFS Provisioner |
|---|---|---|
| 存储位置 | 节点本地磁盘 | 独立 NFS 服务器共享目录 |
| 绑定模式 | WaitForFirstConsumer(必须) |
Immediate(推荐) |
| Pod 能否漂移 | ❌ 不能,重建后可能被调度到没数据的节点 | ✅ 能,任意节点都能挂载 |
| 性能 | ⭐⭐⭐⭐⭐ 本地磁盘 IO | ⭐⭐⭐ 受网络影响 |
| 可用性 | ❌ 节点宕机数据不可访问 | ⚠️ NFS 单点(生产需做高可用) |
| 适用场景 | 单节点测试、对 IO 要求极高的缓存类应用 | 通用测试环境、需要跨节点共享数据的场景 |
| 生产建议 | 仅限特定场景 | 用高可用 NFS 集群或换 Ceph / Rook |
一句话:玩单机/追求极致 IO 用 Local Path;要 Pod 自由漂移,用 NFS。
三个高频排错命令
bash
# PVC 一直 Pending,第一件事就是 describe
root@master30:~# kubectl describe pvc <pvc-name>
# 常见错误:storageclass 名不匹配 / provisioner pod 没起来 / WaitForFirstConsumer 还没被 Pod 引用
# 看 provisioner 是不是在正常工作
root@master30:~# kubectl logs -n kube-storage deployment/nfs-client-provisioner
root@master30:~# kubectl logs -n local-path-storage deployment/local-path-provisioner
# 看有没有生成对应的 PV
root@master30:~# kubectl get pv | grep <pvc-name>
六、总结
动态卷供应的本质,就是把"管理员手动创建 PV"这件事抽象成一个可复用的模板:
- StorageClass 定义套餐(用谁造、怎么造、怎么回收、什么时候绑);
- Provisioner 是干活的工人(内置或外部安装);
- PVC 是用户提的需求单;
- 需求单一交,工人按套餐自动把 PV 造出来并绑定。
记住 provisioner 名字要对得上、volumeBindingMode 别选错、reclaimPolicy 想清楚要不要留数据 ------ 这三点搞定,存储这块基本不会翻车。