- 示例工程
【免费下载链接】examples
Kubernetes application example tutorials
导读
本文以 Kubernetes 官方示例仓库(gh_mirrors/examp/examples)中的 nfs-data 文档 为骨架,深入剖析一个"自带数据文件"的 NFS 服务器容器:它把包含index.html的/exports目录通过 NFS 导出给集群内的其他 Pod 使用,并刻意只启用 NFSv3 以规避部分 Linux 内核在容器中运行 NFSv4 守护进程的兼容性问题。读完本文,你将掌握该镜像的 Dockerfile 与启动脚本细节、协议版本选择背后的原因,以及如何把它部署为 Kubernetes 中的 NFS 持久卷后端,并串联 PV/PVC、busybox 写入端与 nginx 读取端完成一次完整的读写验证。
上图为仓库中 NFS 持久卷示例的架构示意:Web 前端 Pod 使用基于 NFS 服务器的持久卷,而 NFS 服务器自身又使用由 GCE PD 或 Azure Disk 自动供应(auto-provision)的持久卷。
一、nfs-data 容器:定位与镜像形态
按 nfs-data 的 README 的描述,这个容器做的事情非常聚焦:
- 导出挂载目录
/exports,并在其中放入一个index.html文件; - 数据来源基于同级目录
_archived/volumes/nfs下的整套 NFS 示例(即父级 NFS 示例目录 所描述的 Web 前端 + NFS 持久卷场景); - 由于部分 Linux 内核在容器内运行 NFSv4 守护进程存在问题,该容器只开放 NFSv3;
- 镜像以
gcr.io/google-samples/nfs-server提供。
值得注意的是,当前仓库中的 nfs-server-deployment.yaml 实际使用的镜像是registry.k8s.io/volume-nfs:0.8,即该容器镜像随 Kubernetes 版本演进已迁移到registry.k8s.io镜像域,读者在部署时以部署清单中的实际镜像为准即可。
二、Dockerfile 逐层拆解:从 CentOS 到 NFS 服务进程
完整内容见 _archived/volumes/nfs/nfs-data/Dockerfile,核心指令如下:
FROM centos RUN yum -y install /usr/bin/ps nfs-utils && yum clean all RUN mkdir -p /exports ADD run_nfs.sh /usr/local/bin/ ADD index.html /tmp/index.html RUN chmod 644 /tmp/index.html # expose mountd 20048/tcp and nfsd 2049/tcp and rpcbind 111/tcp EXPOSE 2049/tcp 20048/tcp 111/tcp 111/udp ENTRYPOINT ["/usr/local/bin/run_nfs.sh", "/exports"]每一层的关键点:
- 基础镜像
centos+nfs-utils:nfs-utils提供rpcbind、rpc.mountd、rpc.nfsd、exportfs、rpcinfo、rpc.statd等完整 NFS 服务端工具链;同时顺带安装了ps命令(/usr/bin/ps),供运行期诊断进程使用。安装后立即yum clean all清理缓存,控制镜像体积。 - 目录与文件布局:创建导出根目录
/exports;把启动脚本放到/usr/local/bin/run_nfs.sh;把种子文件放到/tmp/index.html,并赋予644权限,由启动脚本在运行时拷贝到导出目录中。 - 端口暴露:
EXPOSE明确了 NFS 服务涉及的三个端口——2049/tcp(nfsd,NFS 协议本体)、20048/tcp(mountd,挂载协议)、111/tcp与111/udp(rpcbind,RPC 端口映射)。这三个端口必须与后续 Service 暴露的端口一一对应。 - 入口:容器启动即执行
run_nfs.sh /exports,把/exports作为待导出的共享目录传给脚本。
三、run_nfs.sh 启动脚本:导出、协议裁剪与优雅退出
镜像的核心逻辑集中在 _archived/volumes/nfs/nfs-data/run_nfs.sh 中,它依次完成四件事:准备导出清单、启动 RPC 基础设施、启动 mountd 与 nfsd、进入等待信号的空循环。
3.1 生成 /etc/exports 并投放种子文件
function start() { # prepare /etc/exports for i in "$@"; do # fsid=0: needed for NFSv4 echo "$i *(rw,fsid=0,insecure,no_root_squash)" >> /etc/exports # move index.html to here /bin/cp /tmp/index.html $i/ chmod 644 $i/index.html echo "Serving $i" done- 脚本接受一个或多个导出目录参数(本例为
/exports),逐个写入/etc/exports; - 导出选项
*(rw,fsid=0,insecure,no_root_squash)的含义:rw:允许客户端读写;fsid=0:为导出目录设置文件系统标识,是 NFSv4 所需的特性(脚本注释也明确说明这一点,即便本容器禁用 NFSv4,保留它也无害);insecure:允许客户端使用大于 1024 的源端口发起挂载(Kubernetes 中 NodePort 等场景常需要);no_root_squash:不把客户端 root 映射为匿名用户,便于在演示环境中的写入操作;
- 随后把
/tmp/index.html拷贝进每个导出目录并置为644,这是"带一个文件"的关键:客户端挂载后立刻能看到index.html。
3.2 启动 rpcbind 与挂载协议守护进程
# start rpcbind if it is not started yet /usr/sbin/rpcinfo 127.0.0.1 > /dev/null; s=$? if [ $s -ne 0 ]; then echo "Starting rpcbind" /usr/sbin/rpcbind -w fi mount -t nfsd nfds /proc/fs/nfsd # -N 4.x: disable NFSv4 # -V 3: enable NFSv3 /usr/sbin/rpc.mountd -N 2 -V 3 -N 4 -N 4.1 /usr/sbin/exportfs -r # -G 10 to reduce grace time to 10 seconds (the lowest allowed) /usr/sbin/rpc.nfsd -G 10 -N 2 -V 3 -N 4 -N 4.1 2 /usr/sbin/rpc.statd --no-notify echo "NFS started"这一段的工程细节值得逐条展开:
- rpcbind 幂等启动:先用
rpcinfo 127.0.0.1探测 RPC 服务是否已就绪,未就绪才执行rpcbind -w(-w表示热重启,保留已注册的 RPC 服务),避免重复启动冲突。 - 挂载内核 nfsd 模块:
mount -t nfsd nfds /proc/fs/nfsd把内核的 NFS 服务端文件系统挂到/proc/fs/nfsd,这是 nfsd 守护进程运行的前提。 - NFS 版本裁剪是核心:
rpc.mountd -N 2 -V 3 -N 4 -N 4.1与rpc.nfsd -G 10 -N 2 -V 3 -N 4 -N 4.1 2明确表示——禁用 NFSv2、NFSv4、NFSv4.1,只启用 NFSv3(-N为 disable,-V为 enable)。这与 README 中"由于部分 Linux 内核在容器内运行 NFSv4 守护进程有问题,只开放 NFSv3"的说明完全对应,是规避容器内核兼容性问题的务实取舍。 - NFSv3 流程补齐:
exportfs -r重新导出/etc/exports中的目录;rpc.statd --no-notify启动状态监控守护进程(NFSv3 锁与恢复所需),--no-notify抑制重启时的通知风暴。 - 缩短宽限期:
rpc.nfsd -G 10把 NFS 重启后的宽限期(grace time)压到允许的最低值 10 秒,加快服务端故障切换后的客户端恢复。 2参数:rpc.nfsd ... 2中的2指定 nfsd 内核线程数量,演示环境保持轻量。
3.3 优雅停止与等待信号
function stop() { echo "Stopping NFS" /usr/sbin/rpc.nfsd 0 /usr/sbin/exportfs -au /usr/sbin/exportfs -f kill $( pidof rpc.mountd ) umount /proc/fs/nfsd echo > /etc/exports exit 0 } trap stop TERM start "$@" # Ugly hack to do nothing and wait for SIGTERM while true; do sleep 5 donetrap stop TERM让容器收到SIGTERM(如kubectl delete或节点驱逐时)后按顺序收尾:rpc.nfsd 0停止全部 nfsd 线程、exportfs -au取消全部导出、清空/etc/exports、卸载/proc/fs/nfsd;- 主进程随后进入一个
while true; sleep 5的空转循环,等待SIGTERM触发 trap。脚本作者也自嘲这是 "Ugly hack",但对"以 bash 脚本为 PID 1"的容器来说,这是让信号能被正确处理的最简方案——没有它,SIGTERM会直接终止进程而不执行任何清理。
四、为什么只开 NFSv3:容器内核兼容性的务实选择
README 中"Since some Linux kernels have issues running NFSv4 daemons in containers"是一句简短但关键的工程结论。结合 run_nfs.sh 的启动参数可以确认:
- NFSv4 依赖内核态的 nfsd 与复杂的 RPC 状态,在部分容器运行时/内核组合下,
/proc/fs/nfsd挂载或守护进程初始化可能失败,导致服务无法启动; - NFSv3 的协议路径更简单,服务端只需 rpcbind + mountd + nfsd 即可工作,跨内核/容器运行时兼容性更好;
- 因此本容器显式
-N 2 -V 3 -N 4 -N 4.1,把能力收敛到 NFSv3,换取在容器环境中的稳定可用。
这也提醒读者:挂载端的选项必须与服务端能力对齐。当前仓库的 nfs-pv.yaml 中写有mountOptions: nfsvers=4.2,而父级 NFS 示例 README 同时注明 "this example uses an NFS container that doesn't support NFSv4"。两者存在版本不一致的风险,实际使用时请根据你部署的服务器镜像能力(NFSv3 容器 vs 支持 v4 的镜像)把mountOptions调整为对应的nfsvers=3或nfsvers=4.2,否则客户端可能因协议版本不匹配而挂载失败。
五、容器之外的完整链路:从镜像到 PV/PVC 的 Kubernetes 实战
单看 nfs-data 只是"一个带 index.html 的 NFS 服务器容器",它真正的价值是在父级 NFS 持久卷示例 中扮演存储后端。下面把完整链路串起来,说明该容器产出的共享目录如何被集群消费。
5.1 为 NFS 服务器准备底层存储
NFS 服务器自身需要一个持久化底座。在 GCE 上创建 200Gi 的 PVC(provisioner/nfs-server-gce-pv.yaml):
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nfs-pv-provisioning-demo labels: demo: nfs-pv-provisioning spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 200Gi在 Azure 上则通过注解指定managed-premium存储类(provisioner/nfs-server-azure-pv.yaml):
metadata: annotations: volume.beta.kubernetes.io/storage-class: managed-premium5.2 部署 NFS 服务器 Deployment 与 Service
nfs-server-deployment.yaml 把上述 PVC 挂到容器的/exports——这正是 nfs-data/volume-nfs镜像导出共享目录的位置,实现"NFS 服务器导出一个自动供应的持久卷":
containers: - name: nfs-server image: registry.k8s.io/volume-nfs:0.8 ports: - name: nfs # 2049 - name: mountd # 20048 - name: rpcbind # 111 securityContext: privileged: true # 需要特权以操作内核 nfsd volumeMounts: - mountPath: /exports name: mypvc volumes: - name: mypvc persistentVolumeClaim: claimName: nfs-pv-provisioning-demo注意两个关键点:
privileged: true:脚本需要挂载/proc/fs/nfsd、绑定内核端口,因此容器必须运行在特权模式,这是该镜像在 Kubernetes 中运行的前提条件;- 三个容器端口与 Dockerfile 的
EXPOSE一一对应,也即与 nfs-server-service.yaml 暴露的2049、20048、111三个 Service 端口一致。
5.3 用 PV/PVC 把 NFS 共享暴露为集群存储
NFS 服务器就绪后,用 nfs-pv.yaml 定义指向该服务器的持久卷(注意按 4 节说明核对mountOptions的协议版本),再用 nfs-pvc.yaml 发起 1Mi 的ReadWriteMany声明:
# PV spec: capacity: storage: 1Mi accessModes: - ReadWriteMany nfs: server: nfs-server.default.svc.cluster.local path: "/" mountOptions: - nfsvers=4.2 # PVC spec: accessModes: - ReadWriteMany storageClassName: "" resources: requests: storage: 1Mi volumeName: nfs这里正是 nfs-data 容器价值放大的地方:多个 Pod 不必关心 NFS 服务器的具体地址,只需通过 PVC 名字nfs引用共享存储,实现"用符号名代替硬编码服务器地址"的间接层。
5.4 写入端与读取端:验证共享文件确实可用
nfs-busybox-deployment.yaml扮演写入端:两个 busybox Pod 把 PVC 挂到/mnt,并以一个 shell 循环持续覆写index.html,写入当前时间和自身 hostname:
while true; do date > /mnt/index.html; hostname >> /mnt/index.html; sleep $(($RANDOM % 5 + 5)); donenfs-web-deployment.yaml扮演读取端:两个 nginx Pod 把同一个 PVC 挂到/usr/share/nginx/html,直接对外提供该文件;nfs-web-service.yaml 以80端口把两个前端 Pod 聚合成一个访问入口。
由此构成闭环:busybox 写入 → NFS 服务器(nfs-data 容器导出)→ nginx 读取,单份数据被多个 Pod 并发读写,充分体现 NFS 的ReadWriteMany特性。
六、端到端验证步骤
父级 NFS 示例 README 给出了完整命令序列,结合本文主题整理如下:
# 1. 按云平台创建底层 PVC(GCE 或 Azure 二选一) $ kubectl create -f _archived/volumes/nfs/provisioner/nfs-server-gce-pv.yaml # 2. 创建 NFS 服务器及其 Service $ kubectl create -f _archived/volumes/nfs/nfs-server-deployment.yaml $ kubectl create -f _archived/volumes/nfs/nfs-server-service.yaml $ kubectl describe services nfs-server # 确认 Endpoints 已关联运行中的 Pod # 3. 按 Service 地址更新 nfs-pv.yaml 的 server 字段后,创建 PV/PVC $ kubectl create -f _archived/volumes/nfs/nfs-pv.yaml $ kubectl create -f _archived/volumes/nfs/nfs-pvc.yaml # 4. 启动写入端 busybox $ kubectl create -f _archived/volumes/nfs/nfs-busybox-deployment.yaml $ kubectl get pod -l name=nfs-busybox # 5. 检查写入结果(应看到时间和 pod 名) $ kubectl exec nfs-busybox-jdhf3 -- cat /mnt/index.html # 6. 启动读取端 nginx 并验证 $ kubectl create -f _archived/volumes/nfs/nfs-web-deployment.yaml $ kubectl create -f _archived/volumes/nfs/nfs-web-service.yaml $ kubectl exec nfs-busybox-jdhf3 -- wget -qO- http://<nfs-web-service-IP>原文的验证输出示例为:cat /mnt/index.html返回一行时间戳加一行nfs-busybox-w3s4t之类的 Pod 名,wgetnginx 服务返回同样内容,即证明写入端的数据经 NFS 共享被读取端成功读出。若失败,优先检查两点:PV 中的服务器 IP 是否已替换为真实 Service 地址,以及kubectl describe services nfs-server是否列出了 Endpoints。
七、小结与使用注意事项
围绕_archived/volumes/nfs/nfs-data这个"带文件的 NFS 导出容器",可以提炼出几条可直接复用的结论:
- 镜像职责单一:Dockerfile 与启动脚本的全部目的就是把一个含
index.html的目录以 NFS 导出,作为 Kubernetes 存储示例的共享后端,产物即gcr.io/google-samples/nfs-server(仓库内部署清单使用registry.k8s.io/volume-nfs:0.8)。 - 协议裁剪是设计核心:
-N 2 -V 3 -N 4 -N 4.1的参数组合把服务收敛到 NFSv3,规避容器内核 NFSv4 兼容性问题;挂载端mountOptions必须与服务端能力对齐。 - 特权模式是硬前提:脚本要挂载
/proc/fs/nfsd,Kubernetes 中必须以privileged: true运行。 - 信号处理值得借鉴:
trap stop TERM+ 空转循环的模式,是 bash 脚本作为容器 PID 1 时实现优雅退出的经典写法。 - 完整的读写闭环:nfs-data 容器 + PV/PVC + busybox 写入端 + nginx 读取端,共同构成一个可复现的 NFS
ReadWriteMany共享存储演示,相关清单均在 _archived/volumes/nfs 目录下可查。
对于要在自己的集群里搭建 NFS 共享存储的读者,直接复用本文涉及的 Dockerfile、run_nfs.sh 与配套 YAML,即可在 GCE/Azure 等云平台或本地集群中快速起一套可读写的 NFS 后端。
- 示例工程
【免费下载链接】examples
Kubernetes application example tutorials
相关推荐
Kubernetes Goat 镜像构建实战:users-repo 脆弱容器与私有镜像仓库泄露场景源码解析
Kubernetes Goat 镜像构建实战:users repo 脆弱容器与私有镜像仓库泄露场景源码解析 users repo 是 Kubernetes Go
云原生应用安全教程容器训练项目:使用Kaniko在Kubernetes中构建容器镜像
容器训练项目:使用Kaniko在Kubernetes中构建容器镜像 什么是Kaniko Kaniko是一款开源工具,专门设计用于在Kubernetes环境中构建
Woodpecker CI 中使用 Buildah 构建容器镜像:Podman 后端的镜像构建实战
Woodpecker CI 中使用 Buildah 构建容器镜像:Podman 后端的镜像构建实战 本文基于 Woodpecker 社区博客《Podman in
CI/CDDevOps
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考