☰
用DRBD+Pacemaker打造高可用NFS,对接Kubernetes动态存储
2026/10/21 6:01:05 网站建设 项目流程

我见过太多团队在Kubernetes存储选型上翻车:有人上来就上分布式存储,结果维护成本直接拖垮了项目组;也有人随便挂个单机NFS,节点一挂整个业务全部写入失败。这两种极端我都在生产环境里踩过,所以今天想聊一个中间态但又非常实用的方案:用DRBD加Pacemaker把NFS做成高可用集群,再通过StorageClass对接Kubernetes动态存储。

这套组合放到现在依然是很多企业内部平台的标准配置,主要解决三个问题:NFS服务器单点故障导致业务中断、PVC需要管理员手动创建PV的低效流程、以及存储扩容和故障切换时没人敢动基础设施的尴尬。

这篇文章我会从架构选型、DRBD数据同步层、Pacemaker资源编排层、Kubernetes侧动态供给,再到生产环境里真正踩过的坑,全部撸一遍。适合正在搭建K8s集群存储、或者被NFS单点问题困扰的平台运维和架构师参考。

1. 架构设计:为什么K8s生产环境还要用NFS

1.1 NFS在云原生时代并没有过气

很多新同学一听到NFS,觉得这是上世纪的东西,Kubernetes里不是有CSI、有分布式存储吗?但从生产实际情况看,NFS在今天依然是使用率极高的Kubernetes存储后端。

原因很简单:Kubernetes需要ReadWriteMany(RWX)的场景太多了。多个Pod同时读写同一个共享目录,在业务上非常常见——比如日志聚合、AI训练数据集、内容管理系统上传目录、多副本应用共享配置。而大多数块存储只支持单节点读写,分布式文件系统像CephFS、GlusterFS配置复杂、运维门槛高,剩下的选择里,NFS的协议成熟度、稳定性、兼容性反而是最省心的。

另一个现实因素是成本。很多企业内部已经有大量存量NFS设备或基于通用服务器搭建的NFS服务,往上接存储系统的需求不是“换一个高端的”,而是在现有资产上做高可用、做自动化。

1.2 高可用选型对比:DRBD+Pacemaker为什么能打

“NFS高可用”听起来简单,实际上方案不少,我结合项目组的经验排个对比。

方案数据同步方式切换时间与数据丢失部署复杂度适用场景
NFS + Keepalived + rsync文件级定时同步可能丢最近一次同步窗口的数据;切换秒级低测试、非核心共享文件、可接受少量丢失
NFS + DRBD + Pacemaker块级实时主从复制RPO接近0;切换秒级到十几秒中生产环境共享存储,数据库以外的绝大多数文件场景
云厂商NAS / 托管文件存储厂商自研以厂商SLA为准低已上云且预算充足

Keepalived加rsync的方案我早期试过不少,优点是配置简单,几行脚本就能跑起来。但有个致命问题:rsync只能解决文件级复制,无法保证复制到一半的状态一致性。一个应用正在写文件,rsync同步到一半,主节点挂了,备节点拿到的目录可能是不完整的,对应用来说这是不可接受的。

DRBD是块设备级别的实时镜像,主节点写入任何一个块,备节点同步同样的块,数据在块层保持一致。配合Pacemaker做资源编排,主节点故障后,备节点自动把DRBD提升为主、挂载文件系统、拉起NFS、漂移VIP,整个过程应用侧感知到的就是一次短暂的NFS连接卡顿,然后自动恢复。

1.3 整体拓扑与数据流走向

这套架构包含三层:

  • 数据层:两台物理机或虚拟机,各自一块独立磁盘做DRBD实时镜像。
  • 集群层:Pacemaker + Corosync负责心跳、资源约束、故障转移。
  • 服务层:NFS导出目录,虚拟IP对外提供服务。

Kubernetes节点上运行的是动态供给组件(nfs-subdir-external-provisioner),它不直接对接某台NFS服务器,而是对接一个虚拟IP。这样NFS服务无论漂移到哪台物理节点,对K8s集群来说IP始终不变。

数据流走向:

  1. 用户创建PVC,指明StorageClass。
  2. Kubernetes控制平面看到PVC绑定请求,找到对应的动态供给组件。
  3. 供给组件通过VIP访问NFS服务,在导出目录下创建子目录。
  4. 控制平面创建PV并绑定PVC,Pod正常挂载。
  5. 实际写入时,Pod通过VIP访问NFS服务,数据落盘到主节点的DRBD块设备,同时实时复制到备节点。

这里要特别强调:这套方案适合文件共享类业务,不适合数据库。数据库有严格的锁语义、频繁的随机小IO,NFS协议和文件系统锁在这种负载下表现很差。数据库类应用请老实使用本地盘或专业的分布式块存储。

2. 数据同步层:用DRBD把两台机器变成一块盘

2.1 磁盘准备与裸设备初始化

DRBD的经典用法是把一块原始磁盘分区作为底层设备。我通常推荐给两台NFS服务器各配一块独立的数据盘,建议与系统盘分开,容量按业务实际需求规划,SSD优先,因为后面我们会遇到小文件性能问题。

登录两台节点,假设设备名为 /dev/vdb,先做分区:

# 节点 nfs01 和 nfs02 上分别执行 lsblk # 确认 /dev/vdb 是数据盘后 fdisk /dev/vdb <<EOF n p 1 w EOF # 装好基础工具 yum install -y drbd84-utils kmod-drbd84 # RHEL系 # 或者 apt install -y drbd-utils # Debian系 modprobe drbd

在RHEL系和Debian系上,DRBD包名和内核模块不太一样,我用的是通用写法。装完后建议把DRBD模块加入开机加载,否则节点重启后模块不加载,资源起不来。这一步很多人会漏。

2.2 DRBD资源配置与启动

初始化好分区后,配置 /etc/drbd.conf 和同目录下的 .d 文件。我这里给一个生产常用配置:

global { usage-count no; } common { protocol C; net { cram-hmac-alg sha1; shared-secret "nfs-ha-cluster-secret"; } syncer { rate 200M; } } resource r0 { device /dev/drbd0; disk /dev/vdb1; meta-disk internal; on nfs01 { address 172.16.10.11:7788; } on nfs02 { address 172.16.10.12:7788; } }

协议C表示主节点写入数据后,必须等备节点确认数据写入才算完成,这是保证切换时数据不丢的关键。同步速率可以根据网络带宽调整,内网万兆可以跑500M,千兆建议100M到200M。

接下来在两个节点分别执行:

drbdadm create-md r0 drbdadm up r0 drbdadm status

正常情况下两个节点都会显示 Connected,但是Primary只有一个。在 nfs01 上执行提升操作,创建文件系统并初始化导出目录:

drbdadm primary --force r0 mkfs.ext4 /dev/drbd0 mkdir -p /data/k8s mount /dev/drbd0 /data/k8s mkdir -p /data/k8s umount /data/k8s

这里有一个细节:我先手动把文件系统挂载一次,只是为了创建初始目录,然后立刻卸载,后续挂载交给Pacemaker管理。如果你不卸载就启动集群资源,Pacemaker会认为文件系统已经被外部挂载,资源起不来。

2.3 主从模式的数据一致性保障

DRBD在主从模式下,只有Primary节点可以挂载文件系统并进行读写,Secondary节点上的块设备是不可挂载的。这种设计从根本上避免了两个节点同时写同一个文件系统造成损坏的问题。

我用一个比喻解释DRBD在这套架构里的位置:它相当于把两台服务器各自的硬盘,在块设备层面做成了“一块逻辑盘”。上层文件系统只看得到 /dev/drbd0,不知道底层其实有两个物理副本。无论谁写入,另一份都会同步跟上。

生产环境还要注意一个点:必须设置DRBD的脑裂自动处理策略或配合Pacemaker的fencing。没有fencing的情况下,网络抖动可能导致两个节点都认为自己是主,这就是脑裂。后文第5章我会专门说脑裂处理。

3. 集群编排层:Pacemaker让NFS服务自动漂移

3.1 集群安装与节点认证

Pacemaker配套的是Corosync,它们负责集群成员管理、消息传递和资源调度。安装配置如下:

yum install -y pacemaker pcs corosync fence-agents-all # 启动 pcsd 服务 systemctl enable --now pcsd # 设置 hacluster 用户密码,用于节点间认证 echo '你的密码' | passwd --stdin hacluster

然后在其中一个节点上认证并创建集群:

pcs host auth nfs01 nfs02 -u hacluster -p '你的密码' pcs cluster setup nfs-ha-cluster nfs01 nfs02 pcs cluster start --all

有个经验:生产环境不要禁用stonith。如果没有真实的fencing设备(如IPMI、带外管理),至少可以配置一台专门的fence设备,或者在测试阶段临时禁用。我的建议是,如果你暂时没有fencing设备,这套集群就只放在测试环境验证,不要急于上生产。因为脑裂风险是真实存在的,没有fencing等于把最后一道防线拆了。

3.2 定义VIP、DRBD、文件系统、exportfs资源

集群起好后,按依赖顺序创建资源。资源本身可以并行启动,但依赖关系必须理顺:VIP先起,然后是DRBD提升,文件系统挂载,NFS服务启动,最后导出。

# 1. 虚拟IP pcs resource create nfs_vip ocf:heartbeat:IPaddr2 \ ip=172.16.10.10 cidr_netmask=24 \ op monitor interval=10s # 2. DRBD主从资源 pcs resource create drbd_r0 ocf:heartbeat:drbd \ drbd_resource=r0 promotable=true # 3. 文件系统挂载 pcs resource create nfs_fs ocf:heartbeat:Filesystem \ device=/dev/drbd0 directory=/data/k8s fstype=ext4 \ op monitor interval=30s # 4. NFS服务 pcs resource create nfs_server systemd:nfs-server \ op monitor interval=30s # 5. NFS导出 pcs resource create nfs_export ocf:heartbeat:exportfs \ directory=/data/k8s clientspec="172.16.0.0/16" \ options="rw,sync,no_root_squash,no_subtree_check" fsid=100 \ op monitor interval=10s

依赖关系用下面几条约束固化:

pcs constraint order start nfs_vip then drbd_r0:promote pcs constraint order promote drbd_r0:promote then start nfs_fs pcs constraint order start nfs_fs then start nfs_server pcs constraint order start nfs_server then start nfs_export pcs constraint colocation add nfs_export with nfs_server pcs constraint colocation add nfs_server with nfs_fs pcs constraint colocation add nfs_fs with nfs_server

资源定义里的 options 我用了 no_root_squash,这个在生产环境有争议。它解决的是容器内root用户写NFS目录的权限问题,但确实带来安全隐患。更稳妥的做法是用 anonuid/anongid 把匿名用户映射到一个固定UID,然后在NFS服务器上给这个UID授权,而不是完全关闭root_squash。这个我会在第5章权限部分展开。

3.3 故障切换验证:拔电式的演练

配置完成后先看集群状态:

pcs status

正常情况下你会看到 nfs_vip、drbd_r0:0、nfs_fs、nfs_server、nfs_export 全部落在 nfs01 上,备节点 nfs02 挂着 drbd_r0:1(Secondary)。

验证切换有两种方式。温和一点的是:

pcs node standby nfs01

然后观察 nfs02 是否自动接管所有资源。注意观察pcs status里资源的启动顺序是否符合预期。另一种是模拟硬件故障——直接把主节点关机,这个测试更真实,但前提是fencing配置已经就绪。

我实测的切换时间通常在10秒左右,期间客户端NFS挂载会卡一下,然后自动恢复。业务侧如果是普通的文件读写,基本无感知;如果是高频小文件写入,可能会有短暂的IO错误,这取决于应用是否重试。

4. Kubernetes侧动态存储:StorageClass与自动供给

4.1 对接前的NFS导出与客户端基础检查

集群编好后,先手工验证K8s节点能访问VIP:

# 在任意Kubernetes节点上执行 yum install -y nfs-utils showmount -e 172.16.10.10

确认能列出 /data/k8s 后,直接手工挂载测试:

mkdir -p /mnt/nfs-test mount -t nfs -o nfsvers=4 172.16.10.10:/data/k8s /mnt/nfs-test touch /mnt/nfs-test/test.txt umount /mnt/nfs-test

这一步能提前暴露网络连通性、防火墙、NFS版本协商等问题。K8s节点和NFS服务器之间的网络必须稳定,因为后续所有PV都依赖这个链路。

4.2 部署动态供给组件

动态供给的核心是一个轻量级组件:nfs-subdir-external-provisioner。它监听PVC事件,并调用NFS服务在导出目录下创建子目录,然后自动生成PV。

先创建ServiceAccount和RBAC授权:

apiVersion: v1 kind: ServiceAccount metadata: name: nfs-client-provisioner namespace: kube-system --- 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: ["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-system roleRef: kind: ClusterRole name: nfs-client-provisioner-runner apiGroup: rbac.authorization.k8s.io

Deployment配置的关键点:

apiVersion: apps/v1 kind: Deployment metadata: name: nfs-client-provisioner namespace: kube-system spec: replicas: 1 selector: matchLabels: app: nfs-client-provisioner template: metadata: labels: app: nfs-client-provisioner spec: serviceAccountName: nfs-client-provisioner containers: - name: nfs-client-provisioner image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: nfs-storage - name: NFS_SERVER value: 172.16.10.10 - name: NFS_PATH value: /data/k8s volumes: - name: nfs-client-root nfs: server: 172.16.10.10 path: /data/k8s

镜像地址我用的是官方仓库路径,生产环境建议先把镜像拉到内网私有仓库再替换,不要直接在线上环境拉公共镜像。

4.3 PVC完整生命周期演示

接着定义StorageClass:

apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-storage provisioner: nfs-storage parameters: archiveOnDelete: "true" pathPattern: "${.PVC.namespace}/${.PVC.name}" mountOptions: - nfsvers=4 - hard - noatime - actimeo=3

然后创建一个PVC试试:

apiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-pvc namespace: default spec: accessModes: - ReadWriteMany storageClassName: nfs-storage resources: requests: storage: 5Gi

创建后检查:

kubectl get pvc test-pvc kubectl get pv

你会看到系统自动生成了一个PV,命名规则通常是pvc-<uuid>。同时,去NFS服务器上查看:

ls /data/k8s

里面会出现default-test-pvc-pvc-<uuid>这样的目录,这就是实际存储数据的目录。provisioner做了两件事:在NFS共享目录下创建一个子目录,再生成对应PV对象。

再创建一个使用这个PVC的Pod:

apiVersion: v1 kind: Pod metadata: name: test-nfs-pod spec: containers: - name: app image: nginx:alpine command: ["/bin/sh", "-c", "echo data > /mnt/data.txt && sleep 3600"] volumeMounts: - name: data mountPath: /mnt volumes: - name: data persistentVolumeClaim: claimName: test-pvc

Pod起来后进去确认文件写到了NFS服务端,然后到K8s节点上执行df -h,能看到挂载点为 NFS 服务器IP加目录。到这里,动态存储的完整流程就走通了。

4.4 回收策略与存储类参数选择

删除PVC时有一个关键参数:archiveOnDelete。

  • archiveOnDelete: "true"(默认):删除PVC后,NFS上的数据目录会被重命名保留,文件名加archived-前缀。适合希望保留历史数据的场景。
  • archiveOnDelete: "false":删除PVC后直接清空数据目录,释放空间。

我在生产里遇到过很多次:团队默认用true,结果几个月后NFS磁盘被一堆“已删除PVC”的数据撑爆。所以设置这个参数前,必须明确业务是否需要保留删除后的数据。如果不需要,直接设为false。

StorageClass还建议开启卷扩容:

allowVolumeExpansion: true

NFS后端扩容比较特殊:因为NFS本身没有容量上限,所谓扩容只是修改PVC的请求容量,K8s侧不会真正去做文件系统的resize。真正需要关注的是NFS服务器磁盘是否还有剩余空间。所以开这个参数可以,但容量规划要落在NFS服务器本身。

5. 生产运维实录:踩过的坑与排查方法

5.1 DRBD脑裂

这是NFS高可用里最容易翻车的事故。某次生产环境网络设备短暂震荡,两个节点心跳中断,Pacemaker把NFS服务切到了nfs02,但nfs01上的DRBD还认为自己是Primary,两边同时接受写入,脑裂就发生了。

脑裂的典型特征:

drbdadm status r0

输出会显示Split-Brain状态。处理办法是确认哪一边的数据是最近的,保留它,丢弃另一边。假设保留nfs02的数据:

# 在 nfs01 上执行 drbdadm disconnect r0 drbdadm secondary r0 drbdadm connect --discard-my-data r0

之后数据会从nfs02向nfs01重新同步,同步期间性能会受影响。这里最重要的操作是:确认保留方向,不要两边都想留。

5.2 PVC卡在Terminating

删除命名空间或PVC时,卡在Terminating状态是常见问题。原因通常是PV上的finalizer没有清理,或者provisioner pod处于异常状态。

排查命令:

kubectl get pvc <name> -o yaml | grep finalizers kubectl get pv | grep <pvc-name>

如果确认provisioner已经无法正常工作(比如Deployment被误删),可以手工移除finalizer:

kubectl patch pvc <name> -p '{"metadata":{"finalizers":null}}'

还要注意NFS服务端可能残留了对应的数据目录,需要手动清理。生产上我建议保留一段时间的归档数据再清理,避免误删后无法恢复。

5.3 NFS挂载hang与超时控制

NFS故障切换期间,客户端挂载不会立即报错,而是进入等待状态。NFS的hard挂载方式下,客户端会无限重试,直到服务恢复。这是好事,不会因为切换丢数据;但如果你期望“几秒不恢复就报错”,就需要调整超时参数:

mountOptions: - nfsvers=4 - hard - timeo=600 - retrans=2

timeo是超时时间,单位是0.1秒,默认600表示60秒一次RPC超时;retrans是重传次数。调小timeo可以让客户端更早感知故障,但也会增加误报的可能性。我建议生产环境保留默认超时,不要为了“快速失败”把参数调得过于激进。

5.4 权限问题与squash映射

这是K8s对接NFS最烦的问题之一。表现是:Pod以root运行,在挂载目录里创建文件,然后另一个普通用户容器却无法读写这些文件;或者反过来,容器内进程写文件一直报Permission denied。

NFS默认开启root_squash,会把NFS客户端的root用户映射为匿名用户(通常nobody)。K8s里的容器运行用户五花八门,UID不一定映射得上,问题就被放大了。

我建议的稳妥配置是这样,在/etc/exports中不直接关闭root_squash,而是指定匿名用户映射:

/data/k8s 172.16.0.0/16(rw,sync,no_subtree_check,anonuid=1000,anongid=1000)

然后在NFS服务器上确保UID 1000对应的目录权限正确。如果应用必须在容器内以root运行并且需要完整权限,你再评估是否使用no_root_squash,前提是NFS服务器只暴露给可信内网网段。

5.5 常见问题速查表

现象可能原因排查与处理
pcs status 显示资源全部停止Corosync成员失联、fencing记录检查节点网络、pcsd状态,清理failcount后重启资源
VIP在两个节点飘来飘去网络抖动导致资源监控超时检查网卡、交换机,增加monitor间隔与超时值
PVC一直PendingStorageClass名字对不上、provisioner异常kubectl describe pvc看事件,检查provisioner部署状态
删除PVC后目录残留archiveOnDelete=true改成false或定期清理归档目录
K8s节点重启后挂载目录是stale服务恢复前old mount未卸载执行umount -l /挂载点再重新挂载
NFS写入慢网络带宽、DRBD同步限制、后端磁盘慢检查同步速率配置、磁盘类型,小文件场景上SSD
客户端报“is not exported”exportfs资源未启动或客户端IP不在网段exportfs -v查看实际导出规则

6. 性能调优与数据备份的底线

6.1 挂载参数与后端性能调优

NFS性能瓶颈往往不在协议本身,而在客户端挂载参数和服务端配置。

客户端挂载参数这里再补充两个经验值。actimeo控制客户端文件属性缓存的时间,默认比较保守,读多写多的场景可以调到3到5,大幅提升属性查询性能。但注意不要调得太大,否则多个节点看到的目录大小、文件列表可能不一致。noatime可以关闭访问时间更新,减少写IO,大部分业务都不需要atime。

服务端方面,NFS线程数默认偏少,并发高时会成为瓶颈:

# 查看当前线程数 cat /proc/fs/nfsd/threads # 调大线程数,例如128 echo 128 > /proc/fs/nfsd/threads

这个配置在重启后需要重新设置,最好做成systemd服务或写入rc.local。

6.2 压测与容量规划

上线前建议用fio简单压一下,至少确认读写性能和延迟符合业务预期:

yum install -y fio fio -filename=/data/k8s/testfile -size=1G -direct=1 \ -rw=rw -bs=4k -numjobs=16 -runtime=60 -group_reporting -name=nfs_iops

关注随机读写IOPS和延迟。NFS在4k小文件场景下延迟明显高于本地盘,这是正常的。如果业务对IOPS要求高,请换SSD作为DRBD底层盘,或者把这类工作负载放到本地存储上。

容量规划要同时看两件事:K8s的PVC总量,以及NFS服务器实际磁盘空间。用一个简单公式估算:预留空间 = PVC总容量之和 × 1.3冗余,多留30%给系统开销、归档数据和DRBD元数据。

6.3 备份策略:HA不等于不丢数据

这个是整篇文章里我最想强调的一点。很多团队搭建完HA后会产生一种虚假安全感,觉得“服务挂了能自动切,数据肯定安全”。但实际上,DRBD只是实时复制,它复制的是你的每一次写入,包括误删、病毒、应用bug写入的脏数据。

一次典型的灾难是:某个任务项目组误清了数据库共享目录下的旧文件,双节点同步后,两边数据都没了。HA没有阻止这件事发生。

因此备份策略必须独立于HA存在:

  • 每日定时用 rsync 把 /data/k8s 同步到另一台异地服务器。
  • 如果底层是LVM,可以定期做LVM快照,但快照只在本地,依然不能防物理灾难。
  • Kubernetes层面可以用Velero备份PVC数据和K8s资源,备份频率根据业务RPO要求定。

我一个人的习惯是:备份恢复演练每季度做一次,真正跑一把“从备份恢复某个命名空间全部数据”的流程,确保脚本没有因为磁盘路径变化而失效。

写在最后的个人体会

前前后后搭过好几套NFS高可用存储,最大的感触是:技术本身不难,难的是把备份、权限、脑裂这些“底线问题”提前想好。DRBD加Pacemaker这套组合虽然听起来老派,但它稳定、可维护、资料也多,非常适合企业内部普遍存在的那种“要给K8s一个不花太多钱又够用的共享存储”的需求。

如果你正准备上这套方案,我建议第一步不是急着写yaml和pcs命令,而是把业务侧的使用方式搞清楚:哪些应用确实需要RWX、哪些其实可以用本地块存储、备份窗口可以容忍多大。把这些定下来,后面的架构就是水到渠成的事了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询