0%

K8s存储实战:从Local-Path到NFS动态存储,一篇搞定

在 K8s 里搞存储,很多人一开始都是手动写 YAML 建 PV,后来才发现动态供给(Dynamic Provisioning) 才是王道。

本文不扯虚的,直接上手两套方案:先搞定轻量级的本地存储 local-path-provisioner,再搭建企业级共享存储 NFS。读完这篇,你就能根据业务需求,灵活搞定 K8s 的持久化存储了。

一、安装本地动态存储

如果你只是想快速测试,或者跑一些对 IO 要求极高、且不需要跨节点共享的组件(比如单机 Prometheus、临时计算任务),Rancher 开源的 local-path-provisioner 是最佳选择。它直接利用节点本地磁盘给 Pod 分配存储,性能几乎等同于裸盘。

1.一键部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 安装 local-path-provisioner
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.30/deploy/local-path-storage.yaml

# 修改配置把镜像名称 rancher/local-path-provisioner:v0.0.30 改为 docker.io/rancher/local-path-provisioner:v0.0.30

# 避坑提示:国内节点拉取 rancher/local-path-provisioner 镜像可能会超时。
# 如果 Pod 一直处于 ImagePullBackOff,建议提前将镜像导入,或修改 Deployment 替换为国内代理镜像。


# 将 local-path 设为默认 StorageClass
# 这样后续创建 PVC 时不指定 `storageClassName` 也会自动使用它
kubectl patch storageclass local-path -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'




# 清理相关
# 删除所有已有的 PV 和 PVC
kubectl delete pv --all 2>/dev/null || true
kubectl delete pvc --all --all-namespaces 2>/dev/null || true

# 删除旧的 local-path StorageClass
kubectl delete sc local-path --ignore-not-found=true

# 删除旧的 local-path-provisioner(Deployment + Namespace)
kubectl delete deployment local-path-provisioner -n local-path-storage --ignore-not-found=true
kubectl delete ns local-path-storage --ignore-not-found=true

2. 验证与使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 查看存储类
kubectl get sc

# 创建一个测试 PVC
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-local-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
EOF

# 查看状态,很快会 Bound
kubectl get pvc test-local-pvc

总结:local-path 部署极简,性能接近裸盘,但无法跨节点读写(仅 ReadWriteOnce),且 Pod 漂移后数据可能丢失(除非配合节点亲和性)。这就引出了我们下面要讲的 NFS。

二、方案对比:Local-Path vs NFS

在决定上 NFS 之前,我们先看一张核心对比图,理清两者的边界:

对比维度 local-path-provisioner NFS (网络文件系统)
存储位置 节点本地磁盘(SSD/HDD) 独立的远程 NFS 服务器
读写性能 ⭐⭐⭐⭐⭐ 极高(无网络开销) ⭐⭐⭐ 中等(受限于网络带宽和延迟)
访问模式 仅支持 ReadWriteOnce(单节点) 支持 ReadWriteMany(多节点同时读写)
数据持久性 节点故障则数据丢失(除非节点恢复) 集中存储,节点故障不影响数据安全
适用场景 高性能数据库、缓存、临时计算任务 共享配置文件、AI 模型仓库、日志收集、跨 Pod 共享文件
维护成本 极低(无需额外服务器) 需要独立维护 NFS 服务器及磁盘阵列

一句话总结:追求极致性能且数据不共享,选 local-path;需要多 Pod 共享数据或做高可用持久化,选 NFS

三、NFS 服务端安装与配置

  • 假设你有一台独立的节点(或 NAS 设备)作为 NFS 服务端,IP 为 192.168.32.131

1.安装NFS

Ubuntu / Debian 环境:

1
2
3
4
5
6
7
sudo apt update
# 服务端需要全套,客户端只装 nfs-common
# 1. NFS 服务端(主节点)安装命令
sudo apt install -y nfs-kernel-server nfs-common rpcbind

# 2. NFS 客户端(从节点)只需要装这个
sudo apt install -y nfs-common rpcbind

CentOS / RHEL 环境:

1
2
# 在每个机器。
sudo yum install -y nfs-utils rpcbind

2.服务端-创建共享目录 /nfs/data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
sudo mkdir -p /data/nfs-share

# 如果有权限问题-放开权限避免读写报错
# sudo chmod 777 /data/nfs-share

# 覆盖-写入共享配置 /etc/exports
# 配置 exports 文件(定义共享规则)
echo "/data/nfs-share *(insecure,rw,sync,no_root_squash)" | sudo tee /etc/exports

# 参数解释:
# * : 允许所有 IP 访问(生产环境务必改成具体网段,如 192.168.32.0/24)
# insecure : 允许客户端使用大于 1024 的随机端口(容器/虚拟机环境必加)
# rw : 读写权限
# sync : 同步写入,数据落盘更安全
# no_root_squash: 客户端 root 用户不压缩为 nobody,保留完整权限(避免容器内 root 写入报 Permission denied)




# 开机自启并立刻启动 rpcbind
sudo systemctl enable --now rpcbind
# 开机自启并立刻启动 nfs 服务
sudo systemctl enable --now nfs-server


# 重载 exports 共享配置
# -r 重载,-a 全部,-v 打印详情
sudo exportfs -arv


# 放行防火墙(如果开启了 UFW/firewalld)
sudo ufw allow nfs && sudo ufw allow rpcbind

3.NFS 从节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 查看服务端共享目录
showmount -e 192.168.32.131
# 正常输出:/data/nfs-share *


# 2. 本地挂载测试
sudo mkdir -p /data/nfs-share
sudo mount -t nfs 192.168.32.131:/data/nfs-share /data/nfs-share


# 3. 测试读写(同样注意 sudo 重定向问题)
echo "hello nfs server" | sudo tee /data/nfs-share/test.txt
# 回到服务端查看 /data/nfs-share 下是否有 test.txt,有则说明网络与权限全通。



# 4. 配置开机自动挂载
echo "192.168.32.131:/data/nfs-share /data/nfs-share nfs defaults,_netdev 0 0" | sudo tee -a /etc/fstab
# _netdev 参数很重要:告诉系统等网络就绪后再挂载,防止开机卡死

# 使上面写入生效
sudo mount -a

4. 常用运维命令(备忘)

1
2
3
4
# 重启服务
sudo systemctl restart nfs-server rpcbind
# 强制卸载(卡住时用)
sudo umount -lf /data/nfs-share

四、K8s接入 NFS 动态存储

手动建 PV 太原始了,我们使用官方推荐的 nfs-subdir-external-provisioner。它就像一个小管家,监听到 PVC 请求后,会自动在 NFS 目录下创建子目录并绑定 PV。

1. 通过 Helm 安装 Provisioner

1
2
3
4
5
6
7
8
9
10
# helm 安装nfs-subdir-external-provisioner

# 删除原有仓库(如有)
helm repo remove nfs-subdir-external-provisioner
# 使用国内镜像代理仓库
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm repo update

# 验证拉取
helm search repo nfs-subdir-external-provisioner

2. 准备核心配置文件 values.yaml

把默认配置导出并修改关键参数:

1
helm show values nfs-subdir-external-provisioner/nfs-subdir-external-provisioner > nfs-values.yaml

nfs-values.yaml默认字段的解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# 副本数量,建议保持为 1
replicaCount: 1
# 部署策略,Recreate 表示在更新时先销毁旧 Pod 再创建新 Pod
strategyType: Recreate

image:
# 镜像仓库地址
repository: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner
# 镜像标签版本
tag: v4.0.2
# 镜像拉取策略
pullPolicy: IfNotPresent
# 拉取私有镜像时使用的 Secret 列表
imagePullSecrets: []

nfs:
# 【重要】NFS 服务器的 IP 地址或域名,必须填写
server:
# 【重要】NFS 服务器上导出的共享目录路径
path: /nfs-storage
# NFS 挂载时的额外选项(例如:nolock,tcp,noresvport)
mountOptions:
# 主 NFS 卷的名称
volumeName: nfs-subdir-external-provisioner-root
# 主 NFS 卷的回收策略(Retain 表示删除 PVC 时保留底层数据)
reclaimPolicy: Retain

# 用于自动创建 StorageClass 的配置:
storageClass:
# 是否自动创建 StorageClass
create: true

# 设置 provisioner 的名称。如果未设置,将自动生成一个名称。
# provisionerName:

# 是否将此 StorageClass 设置为集群的默认 StorageClass
# 如果 storageClass.create 为 false,则忽略此设置
defaultClass: false

# 设置 StorageClass 的名称(后续创建 PVC 时会用到这个名字)
# 如果 storageClass.create 为 false,则忽略此设置
name: nfs-client

# 允许动态扩展卷的大小(需要底层存储支持)
allowVolumeExpansion: true

# 回收废弃卷时使用的策略(Delete 表示删除 PVC 时同时删除 NFS 上的数据)
reclaimPolicy: Delete

# 当设置为 true 时,在删除 PVC 时,provisioner 不会直接删除数据,而是将目录重命名归档(防止误删)
archiveOnDelete: true

# 如果配置了此参数且值为 'delete',则直接删除目录;如果值为 'retain',则保留目录。
# 此配置会覆盖 archiveOnDelete 的设置。
# 如果未设置值,则忽略。
onDelete:

# 指定一个模板,通过 PVC 的元数据(如 labels、annotations、name 或 namespace)来生成目录路径。
# 例如:`${namespace}-${pvc.name}`。如果未设置值,则忽略。
pathPattern:

# 设置访问模式 - ReadWriteOnce(单节点读写), ReadOnlyMany(多节点只读) 或 ReadWriteMany(多节点读写)
accessModes: ReadWriteOnce

# 设置卷绑定模式 - Immediate(立即绑定) 或 WaitForFirstConsumer(等待第一个 Pod 调度时再绑定)
volumeBindingMode: Immediate

# StorageClass 的注解 (annotations)
annotations: {}

leaderElection:
# 当设置为 false 时,将禁用 Leader 选举(多副本时用于防止脑裂,单副本时通常保持开启)
enabled: true

## RBAC 支持相关配置:
rbac:
# 指定是否创建 RBAC 资源(Role, RoleBinding 等)
create: true

# 如果为 true,则创建并使用 Pod Security Policy (PSP) 资源
# 注意:K8s 1.25+ 已废弃 PSP,新版集群通常不需要开启
podSecurityPolicy:
enabled: false

# Deployment 中 Pod 的注解 (annotations)
podAnnotations: {}

## 设置 Pod 的优先级类名称
# priorityClassName: ""

# Pod 级别的安全上下文配置
podSecurityContext: {}

# 容器级别的安全上下文配置
securityContext: {}

serviceAccount:
# 指定是否创建 ServiceAccount
create: true

# 添加到 ServiceAccount 的注解
annotations: {}

# 要使用的 ServiceAccount 的名称。
# 如果未设置且 create 为 true,则使用 fullname 模板自动生成一个名称
name:

# 资源限制与请求配置(建议在生产环境中开启以防 OOM)
resources: {}
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi

# 节点选择器,用于将 Pod 调度到特定标签的节点上
nodeSelector: {}

# 容忍度配置,允许 Pod 调度到带有特定污点 (Taint) 的节点上
tolerations: []

# 亲和性配置(节点亲和性或 Pod 亲和性)
affinity: {}

# 为创建的所有资源添加额外的标签 (labels)
labels: {}

# Pod 中断预算 (PDB) 配置
podDisruptionBudget:
enabled: false
maxUnavailable: 1

最终我的修改 nfs-values.yaml,如下(其他保持默认即可):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# nfs-values.yaml

# 副本数量,建议保持为 1
replicaCount: 1
# 部署策略,Recreate 表示在更新时先销毁旧 Pod 再创建新 Pod
strategyType: Recreate

# 镜像配置 (如果国内拉取困难,可替换为代理镜像)
image:
# repository: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner
# 国内拉取 k8s.gcr.io 困难,这里替换为华为云代理镜像
repository: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/registry.k8s.io/sig-storage/nfs-subdir-external-provisioner
tag: v4.0.2

nfs:
# 替换为你的 NFS 服务器 IP
server: 192.168.32.131
# 替换为你的 NFS 共享路径,必须与服务端 /etc/exports 中的路径完全一致
path: /data/nfs-share
# 优化 NFS 挂载参数,减少锁超时问题
mountOptions:
- nolock,tcp,noresvport

# 配置 StorageClass
storageClass:
create: true
# 是否将其设置为集群的默认 StorageClass
defaultClass: true
# StorageClass 的名称,后续创建 PVC 时会用到
name: nfs-client
# 回收策略:Delete (删除PVC时删除数据) 或 Retain (保留数据)
reclaimPolicy: Delete
# 当设置为 true 时,在删除 PVC 时,provisioner 不会直接删除数据,而是将目录重命名归档(防止误删)
archiveOnDelete: true
# 允许动态扩展卷的大小(需要底层存储支持)
allowVolumeExpansion: true
# 设置卷绑定模式 - Immediate(立即绑定) 或 WaitForFirstConsumer(等待第一个 Pod 调度时再绑定)
volumeBindingMode: Immediate
# 设置访问模式 - ReadWriteOnce(单节点读写), ReadOnlyMany(多节点只读) 或 ReadWriteMany(多节点读写)
# NFS 的精髓就是共享,这里必须设为 ReadWriteMany,否则跟本地存储没区别
accessModes: ReadWriteMany

# 资源限制
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi

注意archiveOnDelete: true 非常实用!删除 PVC 时不会直接删数据,而是把目录重命名归档,给运维留一条后路。

3. 部署到集群

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 安装
helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
-f nfs-values.yaml \
-n nfs-system \
--create-namespace


# 修改nfs-values.yaml 后可以执行升级
helm upgrade nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
-f nfs-values.yaml \
-n nfs-system


# 查看 Pod 状态,确保 Running
kubectl get pods -n nfs-system

# 卸载
# helm uninstall nfs-provisioner -n nfs-system

4. 验证动态存储是否生效

创建一个测试 PVC:

1
2
3
4
5
6
7
8
9
10
11
12
13
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
spec:
storageClassName: nfs-client # 与上面的name一致
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
EOF

查看状态和自动生成的 PV:

1
2
kubectl get pvc test-pvc
kubectl get pv

此时去 NFS 服务端的 /data/nfs-share 下查看,你会发现多了一个类似 default-test-pvc-pvc-xxxx 的子目录——大功告成

五、结语与最佳实践建议

  • 存储选型:在 K8s 中,local-path 和 NFS 并不是二选一,而是共存互补的关系。你可以将 NFS 作为默认存储,专门给无状态应用和共享存储用;将 local-path 留给需要高速读写的数据库(如 etcd、Prometheus)。

  • 性能调优:如果 NFS 传输大文件(比如 AI 模型),可以在 mountOptions 里加上 rsize=1048576,wsize=1048576 提高吞吐量。

  • 生产安全:务必把 /etc/exports 中的 * 换成具体的 CIDR(如 192.168.32.0/24),并考虑 NFS 结合 Kerberos 加密或内网隔离。

  • 故障排查三板斧

    • PVC 一直 Pending?执行 kubectl describe pvc <name> 看 Events。

    • 提示 mount failed?去节点上执行 showmount -e <server_ip> 检查网络通不通。

    • 容器内报 Permission denied?检查 NFS 服务端的目录属主,或者确认是否加了 no_root_squash

现在,你已经具备了在 K8s 中搭建完整存储体系的能力。下一期我们将基于这套 NFS 存储,实战部署 Harbor 私有镜像仓库KServe AI 模型推理平台,敬请期待!


如果觉得本文对你有帮助,欢迎 点赞、在看、转发 支持一下!有任何配置疑问,欢迎在评论区留言交流~

您的打赏,是我创作的动力!不给钱?那我只能靠想象力充饥了。