(十八)NFS 卷
在之前我们学过卷,emptyDir 会随着 Pod 删除,hostPath 又依赖某一个节点。这就存在一个问题,假设一个 Pod 当前运行在 worker-1,数据保存在这个节点
worker-1
└── /data/index.html
如果 Pod 后来被重新调度到 worker-2
worker-2
└── 没有 /data/index.html
这时就会出现问题
如果多个节点上的 Pod 都需要访问同一批文件,就需要一块独立于 Kubernetes 节点之外的共享存储。NFS 可以解决这个问题
worker-1 ─┐
├── NFS Server
worker-2 ─┘
NFS 全称是 Network File System ,它允许一台服务器把某个目录通过网络共享出去,其他主机再把这个远程目录挂载到本地,在 Kubernetes 中,可以理解成 Pod 通过所在节点的网络,访问 NFS 服务器,最终读写 NFS 上的共享目录
Pod
↓
Pod 所在节点
↓
网络
↓
NFS Server
↓
共享目录
Pod 删除以后,只是挂载关系消失,真正的数据仍然保存在 NFS Server 上。Pod 被调度到其他节点后,只要新的节点也可以访问 NFS Server,就可以继续挂载原来的目录
普通单机 NFS Server 本身仍然可能是,这一点和 Kubernetes 没有直接关系
搭建并验证 NFS
我这里就直接使用 master 主机作为 NFS Server ,使用 slave 作为 NFS Client 。如果有多台主机,可以替换成网段,实际操作时需要替换成自己的 IP 和网段
NFS Server: 192.168.64.2
NFS Client(slave): 192.168.64.3
Kubernetes 节点网段: 192.168.64.0/24
共享目录: /srv/k8s-share
配置 NFS Server
NFS Server 在 master 上安装
sudo apt-get update
sudo apt-get install -y nfs-kernel-server
# 创建共享目录
sudo mkdir -p /srv/k8s-share
sudo chown nobody:nogroup /srv/k8s-share
sudo chmod 0770 /srv/k8s-share
# 创建导出配置
mkdir -p /etc/exports.d
# 允许访问 192.168.64.3、权限 允许读写
cat > /etc/exports.d/k8s-share.exports <<'EOF'
/srv/k8s-share 192.168.64.3(rw,sync,root_squash,no_subtree_check)
EOF
# 让配置生效
exportfs -rav
exportfs -v
systemctl enable --now nfs-kernel-server
- rw 允许客户端读写
- sync 服务端写入数据后再返回成功
- root_squash 把客户端 root 映射成低权限用户
- nosubtreecheck 关闭子目录检查 ## 在 Kubernetes 节点测试挂载 每一个可能运行 NFS Pod 的 Kubernetes 节点,都需要安装 NFS Client: ```bash sudo apt-get update
sudo apt-get install -y nfs-common
先不创建 Pod,直接在 slave 写点东西然后测试 NFS 是否能正常访问
bash sudo mkdir -p /mnt/nfs-test
sudo mount -t nfs -o nfsvers=4.1 192.168.64.2:/srv/k8s-share /mnt/nfs-test
写入文件
echo 'hello nfs' | sudo tee /mnt/nfs-test/hello.txt
查看
cat /mnt/nfs-test/hello.txt
最后卸载
sudo umount /mnt/nfs-test
在 master 查看
cat /srv/k8s-share/hello.txt


可以看到,也是成功了。如果 Kubernetes 节点本身都无法挂载 NFS,那么后面的 Pod 也无法正常使用
# Pod 怎么使用 NFS
现在 NFS Server 已经共享了 `/srv/k8s-share`,Kubernetes 节点也确认能够正常访问,接下来就可以让 Pod 使用这块存储
## 挂载 NFS
创建一个 Pod,把 NFS Server 的 `/srv/k8s-share` 挂载到 Nginx 容器的 `/usr/share/nginx/html`
bash cat > nfs-pod.yaml <<'EOF' apiVersion: v1 kind: Pod metadata: name: nfs-demo spec: containers:
name: nginx image: nginx:latest
volumeMounts:
- name: web-data mountPath: /usr/share/nginx/html # 把 web-data 这个 Volume 挂载到这个目录
volumes:
name: web-data nfs: server: 192.168.64.2 # NFS Server 地址 path: /srv/k8s-share # NFS Server 上的共享目录 EOF
kubectl apply -f nfs-pod.yaml
kubectl get pod nfs-demo -o wide
这里仍然是之前学习过的 Volume 结构,volumeMounts 表示把 web-data 这个 Volume 挂载到容器的 `/usr/share/nginx/html`,而下面的 volumes.nfs 则表示 web-data 这个 Volume 的数据来自 `192.168.64.2:/srv/k8s-share`
现在向 Pod 中写入一个文件,然后分别从 Pod 和 NFS Server 查看
bash
在 Pod 中写入
kubectl exec nfs-demo -- sh -c 'echo "hello from pod" > /usr/share/nginx/html/index.html'
在 Pod 中读取
kubectl exec nfs-demo -- cat /usr/share/nginx/html/index.html
在 NFS Server 中读取
cat /srv/k8s-share/index.html
可以看到两边都有 hello from pod,说明 Pod 中的 `/usr/share/nginx/html/index.html` 实际保存在 NFS Server 的 `/srv/k8s-share/index.html`

现在把 Pod 删除,然后重建
bash kubectl delete pod nfs-demo
cat /srv/k8s-share/index.html
重新创建
kubectl apply -f nfs-pod.yaml
kubectl exec nfs-demo -- cat /usr/share/nginx/html/index.html
虽然 Pod 已经删除,但 index.html 仍然存在,因为这个文件真正保存在 NFS Server 中,
所以 NFS 和前面 emptyDir 最大的区别就在这里,Pod 的生命周期不会决定 NFS 中数据的生命周期

## 两个 Pod 共享文件
NFS 还有一个很重要的能力,就是多个 Pod 可以同时访问同一个共享目录。前面的 nfs-demo 已经向 `/srv/k8s-share/index.html` 写入了数据,现在再创建一个 Pod,把同一个 NFS 目录挂载到 `/data`
bash cat > nfs-reader.yaml <<'EOF' apiVersion: v1 kind: Pod metadata: name: nfs-reader spec: containers:
name: reader image: busybox:1.36 command: ["sh", "-c"] args: ["cat /data/index.html && sleep 3600"]
volumeMounts:
- name: shared-data mountPath: /data readOnly: true
volumes:
name: shared-data nfs: server: 192.168.64.2 path: /srv/k8s-share readOnly: true EOF
kubectl apply -f nfs-reader.yaml
kubectl get pod nfs-demo nfs-reader -o wide
kubectl logs nfs-reader
可以看到输出也是 hello from pod ,

虽然两个 Pod 中的挂载目录不同,一个是 `/usr/share/nginx/html`,另一个是 `/data`,但它们最终都访问 NFS Server 上的 `/srv/k8s-share`
text
/srv/k8s-share
/ \
/ \
↓ ↓
nfs-demo nfs-reader
/usr/share/nginx/html /data
所以两个 Pod 可以看到同一个 index.html ,即使这两个 Pod 分别运行在不同 Kubernetes 节点,只要对应节点都能够访问 192.168.64.2 ,依然可以共享同一份数据
# NFS 与 PV、PVC
现在 Pod 的 YAML 中直接写了:
yaml nfs: server: 192.168.64.3 path: /srv/k8s-share
这意味着 Pod 自己知道 NFS Server 地址和共享目录,当前的关系是:
text Pod ↓ NFS
这种写法很适合现在学习 NFS,因为可以直接看到 Volume 的数据到底来自哪里。但如果以后有很多 Pod 都需要使用 NFS,每一个 Pod 都要重复写 NFS Server 地址和目录,存储信息就会和 Pod 配置耦合在一起,因此 Kubernetes 还提供了 PV 和 PVC:
text Pod ↓ PVC ↓ PV ↓ NFS
可以先简单理解成
- PV 用来描述实际存储,例如 `192.168.64.2:/srv/k8s-share`
- PVC 表示 Pod 希望使用一块存储
这样 Pod 后面只需要引用 PVC,NFS Server 地址和目录等信息可以交给 PV 管理。我们后面会学到
## ReadWriteMany
在学习 PV 和 PVC 时还会看到存储访问模式,其中一个叫 ReadWriteMany,简称 RWX,它表示存储可以被多个节点同时以读写方式挂载
NFS 就是比较常见的 ReadWriteMany 存储,例如 worker-1 上的 Pod 和 worker-2 上的 Pod,可以同时挂载同一个 NFS 共享目录
但需要注意,ReadWriteMany 只表示这块存储允许多个客户端同时读写,如果两个 Pod 同时修改同一个文件,文件锁、覆盖和并发一致性依然需要应用自己处理
# NFS 扩展
这里再额外讲一下 NFS 本身的一些问题
## 单点故障
单点故障就是 整个系统里有一个部件,它一挂,依赖它的东西全挂
虽然 NFS 解决了 hostPath 数据和节点绑定的问题,但普通单机 NFS Server 本身仍然可能成为单点故障。如果所有 Pod 都依赖同一台 NFS Server,那么 NFS Server 故障以后,这些 Pod 都可能无法继续访问存储。
这个问题属于 NFS 存储本身的可靠性问题,和 Kubernetes 调度没有直接关系
## 权限
配置 NFS 时,不建议为了方便直接长期使用:
text *(rw,norootsquash)
也不建议简单把共享目录设置成:
bash chmod 777 /srv/k8s-share
`*` 会允许任意能够访问 NFS Server 的客户端尝试挂载,no_root_squash 会让客户端 root 在服务端仍然保留 root 权限,而 777 又会进一步扩大目录的读写权限
实验时可以适当简化权限,生产环境应该根据节点网段和应用需要限制访问范围
# 总结
这篇主要学习的是 NFS 怎么解决多节点 Kubernetes 中共享存储的问题
前面学习的 hostPath 会把数据绑定到某一个 Kubernetes 节点,而 NFS 把数据放到了独立的 NFS Server 上,所以 Pod 即使删除、重建或者被调度到其他节点,只要新的节点仍然能够访问 NFS Server,就可以重新挂载并继续使用原来的数据
当前记住 NFS 在 Kubernetes 中的关系就差不多了
- Pod → Volume → NFS Server → 共享目录
愿诸君顺遂