一、为什么要在K8s里搞个NFS服务?
想象一下,你正在管理一个由很多个“小房间”(容器)组成的“大公寓楼”(Kubernetes集群)。每个小房间里的住户(应用)都很独立,但有时候,他们需要共享一些东西,比如家庭照片、公共文件或者一些需要大家一起读写的资料。如果每个房间都自己存一份,不仅浪费空间,而且很难保证大家看到的都是最新版本。
这时,一个公共的“储物间”就显得非常必要了。NFS(网络文件系统)就是这样一个经典的“储物间”,它可以让网络上的多台计算机像访问本地硬盘一样,去访问同一份文件。在传统的服务器环境里,我们可能会单独弄一台机器来当这个储物间。
但是,在云原生和K8s的世界里,一切都讲究“容器化”和“声明式管理”。我们能不能把这个“储物间”也变成K8s里的一个“住户”,让它也能被K8s统一管理、自动恢复、方便扩展呢?当然可以!这就是我们今天要聊的:把NFS服务也装进容器里,部署到你的K8s集群中,让它成为一个高可用、可弹性伸缩的共享存储服务。
这样做的好处显而易见:部署和运维变得极其简单,一个配置文件就能搞定;它能和K8s的调度、健康检查、故障转移等机制无缝集成;而且,你可以根据实际使用情况,动态调整这个“储物间”的大小和性能。
二、搭建前的准备工作与核心思路
在动手之前,我们得先理清思路,准备一些必要的“工具”和“材料”。
首先,你需要一个正在运行的Kubernetes集群。无论是用kubeadm自己搭建的,还是云服务商提供的托管集群,都可以。确保你的kubectl命令能够正常连接并管理这个集群。
其次,我们要理解在K8s里运行一个有状态服务(比如NFS这种存储服务)的常见模式。我们不会简单地去运行一个孤零零的NFS容器,而是会借助一些K8s的原生资源对象来让它更健壮:
- PersistentVolume (PV) 和 PersistentVolumeClaim (PVC):这是K8s管理存储的核心概念。PV好比是集群里的一块物理或网络硬盘,PVC则是应用向集群申请使用这块硬盘的“申请书”。对于NFS服务本身,它也需要一块“地盘”来存放自己的数据(比如用户上传的文件),这块地盘我们也会用PV/PVC来提供。
- StatefulSet:这是部署有状态应用的“黄金搭档”。与常用的Deployment不同,StatefulSet会为每个Pod维护一个唯一且持久的标识(如
nfs-server-0,nfs-server-1),并且能保证Pod的启动顺序和稳定的网络标识。这对于需要稳定存储和寻址的NFS服务来说非常合适。不过,为了简化初次理解,我们也可以先用Deployment来部署一个单实例的NFS服务。 - Service:这是K8s的服务发现和负载均衡机制。我们需要创建一个Service,为后端的NFS Server Pod提供一个稳定的访问入口(一个集群内部的域名或IP地址)。这样,其他应用就可以通过这个固定的地址来挂载NFS共享了。
我们的核心思路就是:创建一个NFS Server的容器镜像,然后通过K8s的编排能力(如StatefulSet/Deployment)将其运行起来,并配好存储(PV/PVC)和网络访问(Service)。 接下来,我们就用一个完整的例子来演示。
三、手把手实战:部署一个单节点NFS服务
下面,我们将使用一个具体的例子,在K8s中部署一个单节点的NFS服务。我们选择的技术栈是 Kubernetes 配合 Docker 镜像。
第一步:准备NFS Server的Docker镜像
我们可以基于一个轻量级的Linux镜像(如Alpine)来制作自己的NFS Server镜像,或者直接使用社区维护的成熟镜像。这里我们使用一个流行的开源镜像itsthenetwork/nfs-server-alpine作为基础。
但为了更清晰地理解原理,我们看看一个简化的Dockerfile内容:
# 技术栈:Docker
# 使用Alpine Linux作为基础镜像
FROM alpine:latest
# 安装NFS服务器所需的软件包
RUN apk add --no-cache nfs-utils
# 创建要共享的目录
RUN mkdir -p /exports
# 暴露NFS服务端口(2049是nfsd端口,111是rpcbind端口)
EXPOSE 2049 111
# 复制启动脚本到镜像中
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# 设置容器启动时执行的命令
ENTRYPOINT ["/entrypoint.sh"]
对应的entrypoint.sh启动脚本:
#!/bin/sh
# 技术栈:Shell Script
# 这是一个NFS服务器的启动脚本
# 1. 启动rpcbind服务(NFS依赖的远程过程调用服务)
rpcbind
# 2. 启动rpc.statd服务(用于状态监控和文件锁通知)
rpc.statd
# 3. 配置NFS共享
# 将本地的 /exports 目录共享给所有客户端(*),配置为读写(rw)、异步(async)等模式
echo "/exports *(rw,async,no_subtree_check,no_root_squash)" > /etc/exports
# 4. 应用exports配置
exportfs -a
# 5. 启动NFS守护进程,并保持在前台运行(这是容器不退出的关键)
rpc.nfsd
# 6. 启动mountd守护进程,处理客户端的挂载请求
rpc.mountd --foreground
你可以构建这个镜像并推送到自己的镜像仓库,或者直接使用现成的itsthenetwork/nfs-server-alpine:latest。
第二步:在K8s中创建存储(PV和PVC)
NFS服务器自己也需要地方存数据(即/exports目录里的内容)。我们在K8s集群里先为它准备一块“硬盘”。
# 技术栈:Kubernetes YAML
# 文件:nfs-storage.yaml
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-server-pv # PV的名称
spec:
capacity:
storage: 10Gi # 定义存储容量为10GB
accessModes:
- ReadWriteMany # 访问模式:多节点读写(NFS支持)
persistentVolumeReclaimPolicy: Retain # 回收策略:保留(删除PVC后,PV和数据保留)
storageClassName: manual # 存储类名称,这里我们手动管理
nfs: # 注意:这个NFS配置是“模拟”的,用于演示PV定义。
# 实际上,这个PV是给NFS Server Pod用的后端存储,可以是HostPath、云盘等。
# 这里为了概念完整而写,实际部署时,NFS Server Pod的后端存储可能需要用hostPath或云盘CSI。
# 更常见的做法是,NFS Server Pod直接使用宿主机的目录(hostPath)或一个云盘PVC作为后端。
server: 192.168.1.100 # 示例NFS服务器地址(实际不存在,仅作格式参考)
path: "/data"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-server-pvc # PVC的名称
spec:
storageClassName: manual
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi # 申请10GB的存储空间
重要说明:上面的PV定义中的nfs字段是用于消费NFS存储的Pod(即客户端)的配置。对于提供NFS服务的Pod(即NFS Server),它的后端存储通常不是另一个NFS,而是更直接的存储,比如:
- hostPath:直接将宿主机上的某个目录挂载给NFS Server容器。简单,但绑定了节点,Pod不能随意漂移。
- 云块存储(如AWSEBS, Azure Disk):通过CSI驱动挂载一块云硬盘。性能好,但通常只支持
ReadWriteOnce(单节点读写),不适合多副本NFS Server。 - 本地持久卷(Local Persistent Volume):K8s的一种PV类型,利用节点上的本地磁盘,性能最好,但也绑定了节点。
为了让示例更贴近“在K8s内部搭建”的主题且简化,我们下面采用hostPath方式为NFS Server提供后端存储。在生产环境请根据实际情况选择更可靠的存储方案。
第三步:部署NFS Server本身
我们使用Deployment来部署一个NFS Server Pod的副本。
# 技术栈:Kubernetes YAML
# 文件:nfs-server-deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-server # Deployment的名称
spec:
replicas: 1 # 副本数,这里先部署1个
selector:
matchLabels:
app: nfs-server # 选择器标签,用于匹配Pod
template:
metadata:
labels:
app: nfs-server # Pod的标签
spec:
containers:
- name: nfs-server
image: itsthenetwork/nfs-server-alpine:latest # 使用我们准备好的NFS镜像
ports:
- containerPort: 2049 # NFS协议端口
name: nfs
- containerPort: 111 # RPC绑定端口
name: rpcbind
securityContext:
privileged: true # NFS服务需要一些特权操作,这里设为true以简化
# 生产环境中,应尝试使用更细粒度的Capabilities(如SYS_ADMIN, DAC_READ_SEARCH等)
volumeMounts:
- name: exports-data
mountPath: /exports # 将存储挂载到容器内的共享目录
volumes:
- name: exports-data
hostPath: # 使用宿主机的目录作为NFS Server的后端存储
path: /data/nfs-exports # 请确保宿主机上存在此目录,并有适当权限
type: DirectoryOrCreate # 如果目录不存在则创建
执行命令部署它:kubectl apply -f nfs-server-deployment.yaml
第四步:创建Service,暴露NFS服务
现在NFS Server在运行了,但其他Pod还不知道怎么找到它。我们需要创建一个Service。
# 技术栈:Kubernetes YAML
# 文件:nfs-server-service.yaml
---
apiVersion: v1
kind: Service
metadata:
name: nfs-server-service # Service的名称
spec:
selector:
app: nfs-server # 选择标签为app=nfs-server的Pod作为后端
ports:
- name: nfs
port: 2049 # Service对外暴露的端口
targetPort: 2049 # 转发到Pod的哪个端口
- name: rpcbind
port: 111
targetPort: 111
clusterIP: None # 设置为None,表示这是一个Headless Service。
# 这对于StatefulSet很有用,Pod可以通过`pod-name.service-name`的DNS直接访问。
# 对于Deployment,我们通常用ClusterIP类型,这里用Headless是为了后续扩展考虑。
执行命令:kubectl apply -f nfs-server-service.yaml
至此,一个单节点的、容器化的NFS服务就在你的K8s集群内部搭建完成了!其他应用Pod可以通过nfs-server-service这个Service名称(或者其ClusterIP)来访问这个NFS共享。
四、进阶:如何实现高可用?
单节点服务有个致命问题:如果运行NFS Server的节点宕机了,所有依赖这个共享存储的应用都会受影响。所以,高可用(HA)是我们的终极目标。
在K8s里实现NFS服务的高可用,思路主要有两种:
主动-被动(Active-Passive)模式:这是比较经典的HA NFS方案,比如用
DRBD(分布式复制块设备)+Pacemaker/Corosync。但在容器化环境中部署和管理这类复杂集群软件,挑战很大,不是最佳云原生实践。利用StatefulSet的多副本与共享后端存储:这是更贴近K8s哲学的做法。
- 思路:我们部署一个
StatefulSet,设置replicas: 2。两个NFS Server Pod(比如nfs-0和nfs-1)同时运行。 - 核心挑战:两个Pod需要访问同一份数据。这就要求后端存储必须支持多节点读写(
ReadWriteMany),例如:- 云文件存储:如AWS EFS、Azure Files、Google Cloud Filestore。这是最推荐的方式,它们天生就是分布式、高可用的文件系统。
- 分布式文件系统:如CephFS、GlusterFS。你可以在K8s集群内部或外部部署一套。
- 工作流:两个Pod都挂载同一个
ReadWriteMany的PVC(其背后是上述存储)。它们同时对外提供NFS服务。 - 访问入口:创建一个普通的
ClusterIP类型的Service,它会把NFS请求负载均衡到两个Pod上。但是请注意:NFS协议本身是有状态的(文件锁、会话),简单的负载均衡可能导致客户端连接在不同Pod间跳跃,引发问题。因此,更稳妥的做法是:- 使用
Headless Service,让客户端直接通过Pod的DNS(如nfs-0.nfs-service.default.svc.cluster.local)连接到一个固定的Pod实例。 - 或者,在Service上配置
sessionAffinity: ClientIP,将来自同一客户端的请求粘滞到同一个后端Pod。
- 使用
- 思路:我们部署一个
下面是一个高可用NFS StatefulSet的简化示例框架,假设我们已经有一个支持ReadWriteMany的StorageClass(例如efs-sc):
# 技术栈:Kubernetes YAML
# 文件:nfs-ha-statefulset.yaml (概念示例)
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nfs-ha-server
spec:
serviceName: "nfs-ha-service" # 必须指定一个Headless Service
replicas: 2
selector:
matchLabels:
app: nfs-ha-server
template:
metadata:
labels:
app: nfs-ha-server
spec:
containers:
- name: nfs
image: itsthenetwork/nfs-server-alpine:latest
ports: [ ... ] # 同上
securityContext: { ... } # 同上
volumeMounts:
- name: shared-data
mountPath: /exports
volumeClaimTemplates: # StatefulSet的核心特性:为每个Pod自动创建独立的PVC
- metadata:
name: shared-data # 注意:这里每个Pod会有独立的PVC,要实现HA,我们需要的是所有Pod挂载同一个PVC。
# 因此,对于真正的共享存储场景,这个模板不适用。
# 正确做法是:预先创建一个ReadWriteMany的PVC,然后在Pod模板里直接引用这个PVC,而不是用volumeClaimTemplates。
spec:
accessModes: [ "ReadWriteMany" ]
storageClassName: "efs-sc" # 假设这是连接到EFS的存储类
resources:
requests:
storage: 100Gi
---
# 正确引用共享PVC的Pod模板部分应类似这样(伪代码,不能与volumeClaimTemplates同时存在):
# spec:
# containers: ...
# volumes:
# - name: shared-data
# persistentVolumeClaim:
# claimName: my-shared-efs-pvc # 预先创建好的、支持RWX的PVC
请注意:上面的volumeClaimTemplates是为每个Pod创建独立PVC的,适用于需要独立存储的多副本有状态应用(如ZooKeeper)。对于需要完全相同存储的多副本NFS Server,我们应该在Pod模板中直接引用同一个预先创建好的ReadWriteMany PVC。这暴露了当前K8s StatefulSet模型对“多Pod共享同一存储”场景支持的局限性,通常需要一些变通或Operator来管理。
五、应用场景、优缺点与注意事项
应用场景
- CI/CD流水线:多个构建任务(Pod)需要共享源代码、构建产物或依赖缓存。
- 内容管理系统(CMS):像WordPress这样的应用,多个Pod实例需要访问相同的插件、主题和上传的媒体文件。
- 机器学习训练:训练任务需要读取共享的大型数据集。
- 传统应用迁移:将一些依赖共享文件存储的传统应用迁移到K8s时,作为过渡方案。
技术优缺点
- 优点:
- 简化运维:部署、升级、回滚、监控都可以通过K8s统一管理。
- 资源弹性:可以方便地调整Pod的资源限制(CPU/内存)。
- 高可用潜力:结合K8s的调度、健康检查和多副本机制,可以构建高可用的NFS服务。
- 生态集成:与K8s的存储体系(PV/PVC/StorageClass)天然集成。
- 缺点:
- 性能开销:相比物理机或虚拟机直接部署,容器化增加了一些网络和文件系统抽象层的开销。
- 配置复杂度:实现真正的高可用(尤其是数据一致性)配置较为复杂。
- 协议局限性:NFS协议本身在容器网络环境下的延迟和稳定性可能不如专用存储协议。
- 安全考量:运行NFS服务通常需要较高的容器权限(privileged或特定Capabilities),需仔细评估安全边界。
注意事项
- 存储选择是关键:NFS Server的后端存储性能决定了整个共享存储的性能。根据IOPS、吞吐量、延迟需求选择合适的存储类型(本地SSD、云硬盘、分布式文件系统)。
- 网络性能:NFS对网络延迟敏感。确保NFS Server Pod与客户端Pod之间的网络延迟低且稳定。在云环境中,尽量让它们处于同一个可用区(AZ)。
- 权限与安全:
- 谨慎使用
privileged: true,尝试使用更细粒度的Linux Capabilities。 - 在
/etc/exports中严格限制可访问的客户端IP范围(在K8s内,可以使用网络策略NetworkPolicy来辅助)。 - 考虑在传输层启用Kerberos认证(配置复杂)。
- 谨慎使用
- 客户端配置:在客户端Pod挂载时,使用合适的NFS挂载选项,如
hard(硬挂载,推荐)、intr(允许中断)、timeo(超时时间)、retrans(重试次数)等,以增强稳定性。 - 监控与日志:为NFS Server Pod配置详细的日志输出,并集成到集群的日志收集系统(如EFK/ELK)。监控NFS的连接数、IO性能等指标。
六、总结
在Kubernetes集群内部署容器化的NFS服务,是将经典的基础设施服务融入云原生架构的一次有趣实践。它降低了共享文件存储的使用门槛,通过声明式配置让管理变得清晰简单。从单节点部署入手,理解PV/PVC、Service、Deployment等核心组件的协作方式是掌握这一技术的基础。
而迈向生产环境,高可用是无法回避的课题。这要求我们不仅熟悉K8s,还要深入了解存储技术(特别是支持ReadWriteMany的分布式存储),并精心设计部署架构以平衡性能、可用性与复杂性。
虽然NFS不是为云原生而生的最新技术,但在许多场景下,它仍然是一个可靠且实用的共享存储解决方案。通过容器化,我们让它焕发了新的活力。希望这篇内容能帮助你成功地在自己的K8s集群中搭建起这座“共享储物间”,为你的应用提供稳固的数据共享基石。
评论