☰
高可用NFS动态存储:Kubernetes持久化实战全解析
2026/10/21 23:15:24 网站建设 项目流程

很多人一开始接触 Kubernetes,总觉得存储是块硬骨头。特别是当集群里跑起有状态应用,比如数据库、消息队列、日志服务之类,Pod 一调度、一迁移,数据怎么办?本地磁盘显然不行,云盘又贵又绑死厂商,这时候 NFS 这种老牌网络存储选手反而成了最灵活、最实在的解法。但是,单点 NFS 又让人心里不踏实,Server 一挂,整个集群的存储全完蛋。所以就有了这个项目:在 Kubernetes 集群里,搭一套高可用的动态存储 NFS 集群,既解决数据持久化,又解决 NFS 服务本身的单点故障,还能让开发同学像用云盘一样,随手申请一块存储直接挂载。这篇文章我就把整个项目的设计思路、核心细节、实操过程、踩坑记录全部梳理一遍,希望能给正在折腾 Kubernetes 存储的朋友一些参考。

这篇内容适合谁?我觉得大致这么几类人:一是刚把 Kubernetes 集群跑起来、正准备接有状态应用的人;二是已经在用 NFS 但被单点故障坑过、想上高可用方案的人;三是想搞清楚 StorageClass、PVC、PV 这套动态存储机制到底怎么落地的人。不管你是运维还是开发,只要能跟着敲命令,这篇文章里的方案你是可以完整复现的。我会尽量讲清楚每一个关键步骤背后的“为什么”,而不是只丢给你一堆 YAML。

1. 整体设计与思路拆解

1.1 需要高可用 NFS 的典型场景

先聊聊为什么需要它。假设你有一个 Kubernetes 集群,里面跑着几套业务,其中一套是日志收集系统,另外一套是内部的 Wiki 系统,还有一套是测试环境的数据库。这些应用都有个共同点:需要把数据写到某个地方,而且这个“地方”不能因为 Pod 重启就丢数据。

最朴素的做法是给 Pod 挂载宿主机目录,但问题很明显:Pod 被调度到另一台机器上,数据就找不到了。用云服务商提供的块存储也行,但如果你是多云环境、混合云环境,或者干脆是自建机房,块存储的迁移和跨节点挂载能力就很弱。块存储通常只能挂到同一台节点上的 Pod,这在 Kubernetes 的调度模型下非常不灵活。而 NFS 是网络文件系统,天然支持多节点同时读写,一个 PV 可以被多个 Pod 共享,这就非常适合那些需要共享数据的有状态服务。

但 NFS 的问题也摆在那里:如果只有一台 NFS 服务器,这台机器宕机、网络隔离或者磁盘损坏,整个集群依赖存储的业务就会全部停摆。所以这个项目要解决的,就是在保留 NFS 灵活性的同时,把它的单点故障拿掉,让它变成高可用服务。也正是因为这件事的收益很大,投入产出比很划算,我才决定认真折腾一下。

1.2 高可用方案的整体架构选择

实现 NFS 高可用,社区里常见的路子主要有三种。

第一种是 DRBD 加 Pacemaker。DRBD 做数据镜像,Pacemaker 管资源切换,这套方案能保证数据在节点间有实时副本,故障切换的粒度也能做到很细。但它的问题是配置复杂,版本兼容性要非常小心,尤其是 DRBD 和内核的适配,升级内核后经常要重新编译模块,维护成本偏高。

第二种是使用分布式文件系统,比如 GlusterFS 或者 CephFS,把它们提供的存储再以 NFS 协议导出。这套方案的优点是可扩展性极好,容量规模可以做得很大,但问题是架构太重,Ceph 生产环境至少需要三个节点起步,运维心智负担比较大。如果你只是想给 Kubernetes 集群提供几十 GB 到几 TB 的共享存储,这套方案明显是杀鸡用牛刀。

第三种方案,也就是我在项目中实际采用的方案,是用 Keepalived 加 Rsync,加上 NFS 本身的能力,实现主备模式的高可用。Keepalived 负责虚拟 IP 的漂移,Rsync 负责主备节点的数据同步,NFS 服务本身跑在 active 节点上。备节点平时不提供服务,但只要主节点出现故障,虚拟 IP 会迅速漂移到备节点,NFS 服务接着拉起,Kubernetes 集群里的 Pod 几乎感知不到存储后端发生了什么。

这个方案为什么适合?首先它足够轻,两台服务器就能跑起来,不需要额外引入分布式系统的复杂度;其次 Keepalived 和 Rsync 都是非常成熟的老牌工具,资料多、坑少,出了问题容易排查;再就是数据同步的形式简单直接,对中小规模集群非常友好。当然,主备模式也有它天然的短板,比如备节点在正常情况下是闲置的,存在资源浪费的问题,另外 Rsync 的同步是准实时的,极端情况下可能丢失几秒数据。但对于大部分非核心业务场景,这个丢数据窗口是可以接受的。如果哪天业务真的要求数据零丢失,我可能会转向第一种方案或者直接上分布式存储,但那是后话了。

1.3 动态存储在 Kubernetes 里的定位与价值

老一批 Kubernetes 使用者在存储这件事上,习惯的做法是先手动创建 PV,再创建 PVC 去绑定它。但这套做法有一个很烦人的问题:PV 得提前准备好,容量是固定的,写着 100Gi 就只能是 100Gi,想扩容你得手动操作底层存储,然后再改 PV 对象。这运维体验在一个动态变化的业务环境里,是很让人头疼的。

而动态存储解决的就是这个问题。动态存储依托于 StorageClass 这个抽象层,让存储的创建和销毁变得自动化。开发者要一块 20Gi 的存储,直接写一个 PVC 对象声明一下,Kubernetes 就会自动调用存储后端的接口,创建一块对应大小的存储,再绑定给这个 PVC。整个过程不需要运维人员介入,完全自助服务。

在这个项目里,NFS 集群作为存储后端,对外通过 StorageClass 暴露能力。Kubernetes 集群中的 PVC 会通过 provisioner 插件,调用 NFS 服务在指定的共享目录里自动创建一个子目录,作为独立的 PV 使用。因为每个 PV 对应一个唯一的子目录,多个 PVC 之间可以做到逻辑上的数据隔离,这就既保留了 NFS 共享存储的便利性,又实现了访问隔离和容量配额的效果。

我在实际调研中发现,市面上的公有云托管 Kubernetes 集群,几乎都提供了原生的 StorageClass 能力,比如阿里云的 NAS 类型、AWS 的 EFS 类型,原理上其实和我这个项目里做的事情是一样的。所谓云上服务,只不过是把底层这些组件打包封装成了黑盒,而咱们自己做,无非就是把黑盒打开,把每个零件都摸清楚,可控性反而更高。

2. 核心技术点拆解

2.1 NFS 协议要点与服务配置

NFS,全称 Network File System,从诞生到现在已经几十年了,目前在 Linux 世界里用得最多的还是 NFSv4 和 NFSv3 版本。NFSv4 在安全性、锁管理、状态恢复方面做了大量优化,唯一需要注意的是它默认使用 TCP 协议,对网络质量的依赖比 NFSv3(可以用 UDP)更高,但现代内网环境基本都不是问题。

配置 NFS 服务端时,最关键的是规划好共享目录和导出规则。导出规则的格式类似这样:

/data/nfs-share 10.0.0.0/24(rw,sync,no_root_squash,no_subtree_check)

这里有几个容易搞混的参数,我具体说明一下。sync选项在数据落盘之前不返回写操作完成,一定程度上影响性能,但可靠性更高,建议生产环境开启。no_root_squash允许 root 用户保持 root 权限访问共享目录,这个选项要非常谨慎,如果客户端是不可信环境,建议保持默认的 root_squash,让 root 被映射成匿名用户,否则会产生严重的安全隐患。no_subtree_check是一种性能优化选项,对于目录层次频繁改动的共享目录,建议加上。

还有一点经常被忽略,NFS 服务端对导出的目录本身是有权限要求的。目录属主、属组、权限位这些必须提前设置好,因为 NFS 的权限判定是基于客户端传来的 UID/GID 和服务端文件系统的权限位共同决定的。如果两边的 UID/GID 对不上,就会出现明明有权限写入却报 Permission denied 的奇怪问题。这个问题的排查思路我后面会专门细说。

2.2 Keepalived 与虚拟 IP 的工作原理

Keepalived 的核心是基于 VRRP 协议实现虚拟 IP 的漂移。VRRP,全称 Virtual Router Redundancy Protocol,它的工作逻辑可以理解为一组路由器(或者服务器)抢一个虚拟 IP 的“控制权”。每台参与选举的机器都有一个优先级,优先级高的成为 MASTER,负责持有虚拟 IP 并提供服务;其他机器处于 BACKUP 状态,持续监听 MASTER 的心跳。正常情况下 BACKUP 是“默默无闻”的,但只要超过设定时间没有收到 MASTER 的心跳包,优先级最高的 BACKUP 就会升级为 MASTER,把虚拟 IP 抢过来,继续对外提供服务。

在 NFS 高可用这个场景里,虚拟 IP 的作用很巧妙。Kubernetes 节点上的 NFS 客户端,挂载 NFS 时使用的地址是虚拟 IP,而不是某台具体的服务器 IP。这样在客户端眼里,存储后端的地址永远是不变的。当主节点故障时,虚拟 IP 漂移到备节点,备节点的 NFS 服务也在运行,客户端只需要在短暂的网络中断后重连,就能继续使用存储服务,不需要重新挂载或者改任何配置。

Keepalived 的配置里面有两个参数,对故障检测的灵敏度影响很大。一个是virtual_router_id,同一组虚拟 IP 的服务器必须配置一致,否则会选举异常。另一个是nopreempt,这个参数决定了当更高优先级的节点恢复后,是自动抢占回虚拟 IP,还是继续保持当前状态。我之前在生产环境遇到过一个问题,主节点因为磁盘 IO 卡顿导致 Keepalived 误判,故障切换到了备节点,而主节点恢复后又自动抢占,结果业务在这期间经历了两次切换,每次切换期间存储短暂不可用。后来加了nopreempt,让节点恢复后不自动抢回,彻底避免了这种震荡问题。

2.3 数据同步机制选择与取舍

主备 NFS 之间要保证数据尽可能一致,我选择的是 Rsync。Rsync 同步的基本策略是:在主节点上定期对共享目录执行增量同步,把变化的数据推送到备节点。因为是增量同步,所以正常情况下压力很小,几秒钟同步一次也完全可以承受。

这里有一个关键的取舍要说明。Rsync 只能保证同步时刻数据是一致的,同步间隔内产生的数据变化可能会丢失,这就是所谓的数据丢失窗口。比如你设置的同步间隔是 10 秒,主节点刚好在同步完成后的第 2 秒宕机,那么这 2 秒内新写入的数据就没来得及同步到备节点,故障切换后这些数据就没了。

要想缩小这个窗口,有两个思路。一个是缩短同步间隔,比如从 10 秒缩短到 2 秒,能有效减小丢失窗口,但会稍微增加一些网络开销。另一个是用 Lsyncd 这类工具,监听文件系统事件,配合 Rsync 做到接近实时的同步。我在实际项目中用的是 Lsyncd,因为它能监听到文件的 modify、create、delete 事件,然后立即触发同步动作,基本上把丢失窗口控制在了 1 秒以内。不过要注意,Lsyncd 配置如果写得不好,频繁触发同步也可能导致 CPU 占用偏高,尤其是大文件密集创建的场景,要针对性地做一些聚合和延迟配置。

数据同步这块,还有一个很容易忽略的点:删除操作。Rsync 默认在同步时,如果源端删除了某个文件,目标端并不会自动删除。如果希望主备严格一致,需要在 Rsync 命令里加上--delete参数。我遇到过因为没有加这个参数,主节点上的一个过期数据目录一直同步不到备节点,导致故障切换后业务读到旧数据,排查了半天才找到原因。

2.4 Kubernetes 动态存储 Provisioner 机制

Kubernetes 实现动态存储的核心组件是 CSI(Container Storage Interface)或者 out-of-tree 的 provisioner。对于 NFS 来说,目前比较流行的实现是 NFS Subdir External Provisioner,它的原理是在 NFS 服务器的某个共享目录下,为每一个 PVC 自动创建一个以 PV 名称命名的子目录,这个子目录就作为 PV 的实际存储路径。

它的工作流程是这样的:开发者提交一个 PVC 对象,声明存储大小和 StorageClass 名称;Kubernetes 的 controller manager 发现这个 PVC 之后,调用对应 StorageClass 里指定的 provisioner;provisioner 通过网络协议访问 NFS 服务器,在根目录下创建子目录;然后创建一个 PV 对象,把它和 PVC 绑定。整个过程完全自动化,从提交 PVC 到拿到 PV,通常只需要几秒钟。

这个方案有个小限制,就是 PVC 声明的容量大小不会真实限制子目录的容量。什么意思呢?因为 NFS 子目录本身没有配额系统,PV 里写的 20Gi 只是声明值,实际你写超过 20Gi 的数据,底层 NFS 并不会拒绝。这个“软约束”的问题,在某些要求严格配额管理的业务场景下需要注意。如果是严格控制容量的场景,可能需要引入额外的配额管理机制,比如在 NFS 服务器上对目录设置磁盘配额(quota),但配置起来比较复杂,我在项目里因为业务不敏感所以暂时没有做这一步,后续有需求再补。

3. 实操过程与核心环节实现

3.1 环境准备与规划参数

先说下我当时的环境规划。整个高可用 NFS 集群用了两台物理服务器,系统是 CentOS 7.9,各自配置了 4 核 CPU、8GB 内存、一块 200GB 的系统盘和三块 1TB 的数据盘,数据盘做了 RAID5,实际可用空间大约 1.8TB。两台服务器在同网段,网络是千兆内网,延迟基本可以忽略。Kubernetes 集群是 1.24 版本,跑在另外三台节点上,使用的是 Calico 网络插件。

具体的 IP 规划如下:

角色主机名IP 地址说明
NFS 主节点nfs-master10.0.10.11承载虚拟 IP,提供读写服务
NFS 备节点nfs-backup10.0.10.12同步数据,故障时接管服务
虚拟 IP(VIP)-10.0.10.20Kubernetes 集群挂载时使用的地址
Kubernetes 节点 1k8s-node110.0.20.31运行业务 Pod
Kubernetes 节点 2k8s-node210.0.20.32运行业务 Pod
Kubernetes 节点 3k8s-node310.0.20.33运行业务 Pod

容量规划这块,需要注意一点:NFS 共享目录的总容量是由数据盘决定的,你要给整个 Kubernetes 集群用的存储量,最好预留至少 20% 的余量。我在最初规划时只预留了 10% 的余量,结果上线没多久就遇到了容量告警,被迫临时加盘,非常被动。如果让我重新做,我会预留 30% 以上的余量,或者直接加一块盘专门等着扩容用。

3.2 NFS 服务端部署与目录规划

两台服务器都需要安装 NFS 相关软件包。在 CentOS 7.9 上,安装命令是:

yum install -y nfs-utils

安装完成后,需要把nfs-server和rpcbind服务设置为开机自启并启动:

systemctl enable --now rpcbind systemctl enable --now nfs-server

共享目录的规划,我建议把共享根目录单独放在一块数据分区上,不要和系统盘混在一起。比如我在每台服务器的/data目录下创建了一个nfs-share目录,挂载点是数据盘。这个目录的权限要设置成 755,属主是nfsnobody,因为这个用户是 NFS 默认的匿名用户,确保客户端以匿名方式写入时也有权限。

导出规则的配置文件是/etc/exports,我最终的配置是:

/data/nfs-share 10.0.0.0/8(rw,sync,no_wdelay,insecure_locks,no_root_squash,no_subtree_check)

这里我把网段放得比较宽,实际生产环境建议收紧到 Kubernetes 节点所在的网段,比如10.0.20.0/24。no_wdelay是关闭 NFS 的写延迟合并,对性能有一定提升,但同时也会增加少量 IO 压力。这个参数在实际验证中效果还是明显的,随机小文件写入的性能大约提升了 15%-25%,所以我保留了下来。

修改完/etc/exports之后,执行exportfs -arv让配置生效。可以用showmount -e localhost验证共享目录是否已经成功导出。

3.3 Keepalived 高可用配置实战

两台服务器都要安装 Keepalived:

yum install -y keepalived

主节点的配置我贴在下面,这份配置是整个高可用方案的核心,需要每个参数都理解清楚:

! Configuration File for keepalived global_defs { router_id LVS_NFS_MASTER vrrp_skip_check_adv_addr vrrp_strict } vrrp_instance VI_NFS { state MASTER interface ens192 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass nfs-ha-secret } virtual_ipaddress { 10.0.10.20/24 dev ens192 label ens192:1 } }

备节点的配置基本一样,只有几处关键差异:state改为 BACKUP,priority改为 90,router_id改为 LVS_NFS_BACKUP。注意virtual_router_id两台机器必须一致,auth_pass也必须一致,否则它们互相不认识,虚拟 IP 会各抢各的,集群根本无法工作。

还有个参数是vrrp_strict,这个参数启用后,Keepalived 会严格遵守 VRRP 协议的规范,对收到的报文做严格检查。但如果你的底层网络环境比较复杂,比如有交换机做了 VLAN 隔离,或者组播被禁用,这参数反而会成为麻烦。我在某次部署中就是因为开了vrrp_strict,而交换机组播配置有问题,导致心跳一直不通,纠结了半天,后来检查交换机配置才发现问题,把vrrp_strict去掉后才正常。所以我的经验是:默认先开着,如果心跳异常,优先排查底层网络组播,而不是直接关掉。

启动服务:

systemctl enable --now keepalived

验证主节点是否持有虚拟 IP:

ip addr show dev ens192

正常情况下,主节点的ens192:1上会看到10.0.10.20这个地址。然后手动挡掉主节点的 Keepalived 服务,再看备节点,如果几秒钟内虚拟 IP 出现在备节点上,说明 VRRP 的心跳和抢占逻辑是正常的。

3.4 数据同步配置与验证

数据同步我用的 Lsyncd,所以需要先在主节点上安装:

yum install -y epel-release yum install -y lsyncd

Lsyncd 的配置文件是/etc/lsyncd.conf,我最终的配置是这样:

settings { logfile = "/var/log/lsyncd/lsyncd.log", statusFile = "/var/log/lsyncd/lsyncd.status", insync = 1, delay = 3, } sync { default.rsync, source = "/data/nfs-share", target = "root@10.0.10.12:/data/nfs-share", delete = true, exclude = { ".nfs*" }, rsync = { binary = "/usr/bin/rsync", archive = true, compress = true, rsh = "/usr/bin/ssh -p 22 -o StrictHostKeyChecking=no" } }

这里有几个关键点。insync = 1表示当有文件变动时,最多等待 1 秒就开始同步,这是控制丢失窗口的核心参数,我设置的是 1 秒,意味着理论上最多丢 1 秒的数据。delay = 3则是事件聚合的窗口,意思是多个文件事件在 3 秒内会被合并成一次同步,这是为了防止高并发写入时频繁触发 Rsync 造成资源浪费。exclude排除了.nfs*开头的临时文件,这些文件是 NFS 内部的临时文件,如果同步过去反而会产生不必要的问题。

使用 Lsyncd 之前,我特意做了主节点到备节点的 SSH 免密登录,否则 Rsync 每次同步都需要输入密码,根本没法自动化。这一步很简单,主节点上执行ssh-keygen -t rsa生成密钥,然后用ssh-copy-id root@10.0.10.12把公钥推送到备节点就行了。

配置完成后启动 Lsyncd,在主节点的共享目录里创建一些测试文件,等了大概几秒钟再去备节点上看,文件确实同步过去了。为了验证删除同步,再做一个删除测试,因为我在配置里加了delete = true,所以源端删除文件之后,目标端也会跟着删除。这一步验证完成,数据同步这块的可靠性就算有底了。

3.5 Kubernetes StorageClass 与 Provisioner 部署

Kubernetes 侧要做的第一件事,是部署 NFS Subdir External Provisioner。这个组件可以用 Helm 安装,也可以直接写 Deployment YAML 手动部署。我为了更清楚地掌握每个配置,选择了手动部署。

Deployment 的配置核心在环境变量里,关键的几个变量如下:

PROVISIONER_NAME: k8s-nfs-provisioner NFS_SERVER: 10.0.10.20 NFS_PATH: /data/nfs-share

这几个变量的含义很重要。PROVISIONER_NAME是自定义的 provisioner 名称,后面 StorageClass 里要引用它,必须保持一致;NFS_SERVER要填虚拟 IP 而不是某个具体节点 IP,这样才能保证 NFS 服务在主备之间切换后,provisioner 依然能正常访问存储;NFS_PATH是 NFS 共享目录的根路径。

另外,这个 Deployment 里需要挂载一个名为nfs-client-root的 PV,它代表 NFS 共享目录本身。这个 PV 的 YAML 如下:

apiVersion: v1 kind: PersistentVolume metadata: name: nfs-client-root spec: capacity: storage: 1Ti volumeMode: Filesystem accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain nfs: server: 10.0.10.20 path: /data/nfs-share

这个 PV 不会直接给业务 Pod 用,它的作用是把 NFS 共享目录先挂载到 provisioner 所在的 Pod 里,provisioner 才能在这个共享目录里创建子目录。

最后创建 StorageClass:

apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-dynamic provisioner: k8s-nfs-provisioner reclaimPolicy: Delete volumeBindingMode: Immediate

reclaimPolicy设置为 Delete 意味着,当 PVC 被删除时,对应的 PV 和 NFS 子目录也会被自动清理,这块要注意,如果业务有保留数据的需求,要提前改掉这个策略。volumeBindingMode设置为 Immediate,表示 PVC 创建后立即开始绑定 PV,不需要等到 Pod 调度。对于 NFS 这种支持 ReadWriteMany 的存储类型,Immediate 模式是合理的选择。

3.6 动态挂载验证与读写测试

配置完成后,我们需要验证整套链路是否真正打通。我的验证方式比较简单直接:创建一个测试 PVC,然后跑一个挂载该 PVC 的 Pod,在里面写入数据,再确认数据确实落到了 NFS 服务器上。

PVC 的 YAML:

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

创建之后,观察 PVC 的状态:

kubectl get pvc test-pvc

正常情况它会很快从Pending变成Bound,这个过程说明 provisioner 已经在 NFS 服务器上创建了子目录,并且 PV/PVC 已经绑定成功。

验证阶段还可以做个更有意思的测试:创建两个 Pod 同时挂载同一个 PVC,测试并发读写是否正常。NFS 是支持多节点读写共享的,这也是它相对于块存储最大的优势。我在测试中创建了两个 Pod,分别写入不同的文件,同时互相读取对方的文件,一切正常。

最后一步验证是从 Kubernetes 节点反向检查,在 NFS 服务器上看看 provisioner 到底创建了什么目录结构:

ls -l /data/nfs-share/

你会看到类似test-pvc-pvc-xxxxx这样的目录,这就说明整套动态存储链路已经完整跑通了。

4. 常见问题与排查技巧实录

4.1 权限类问题的排查套路

权限问题是我在实际操作中遇到最多、也最让人头疼的一类问题。其中最有代表性的就是客户端挂载 NFS 之后,写文件时报Permission denied,即使你在服务端已经设了 777 权限也一样报错。

这种问题的根源,通常是客户端传入的 UID/GID 与 NFS 服务端的目录属主不匹配。举个例子,如果你的 Pod 以 root 身份运行,在容器里 UID 是 0,而 NFS 共享目录的属主是nfsnobody(UID 65534),服务端会对 root 做 squash 映射,把它映射成匿名用户,如果匿名用户对共享目录没有写权限,自然就拒绝了。

排查的思路其实很简单:先确认客户端的 UID,再去看服务端共享目录的属主和权限位。在你无法确定客户端 UID 的情况下,最稳妥的做法是让共享目录对所有用户开放写权限,然后再根据实际客户端身份逐步收敛权限。我在项目里直接把共享目录的权限设成了 777,并配合no_root_squash选项以规避权限问题。虽然这个配置在严格的安全审计下会被标记为风险项,但对于内网环境、业务可控的场景,简单直接的权限策略往往更实用。如果你做的是面向公网的业务,那就不建议这么干,还是老老实实配置用户映射和 ACL。

4.2 故障切换时 NFS 客户端卡死的问题

当一个 NFS 服务器宕机时,已经挂载了该 NFS 的客户端可能会出现完全卡死、文件操作无响应的现象,这在默认配置下尤其明显。原因是 NFS 客户端默认会无限重试,直到 TCP 超时。但好在 NFS 客户端本身有自动重连机制,当虚拟 IP 漂移到备节点后,客户端会在重试后自动连接到新的服务端,IO 操作得以恢复。

不过,如果你使用的是 NFSv3 并且锁机制启用的话,故障切换期间客户端可能无法重新获取文件锁,旧的锁也不会被自动释放,导致业务进程挂起。这个问题的缓解办法是在客户端挂载参数里加上soft,timeo=50,retrans=2,让请求在超时后快速失败而不是无限等待,同时降低重试次数。这个参数配置在 Kubernetes 的 PV 定义里就能搞定。但要注意soft模式也可能导致极端情况下数据写入不完整,如果你对数据一致性要求很高,需要做好权衡。

在本次项目中,因为使用的是 NFSv4,状态恢复机制比 NFSv3 好很多,故障切换后客户端能较快恢复。为了验证切换对业务的真实影响,我做了好几次主节点强制宕机测试,业务 Pod 的 IO 中断时间大约在 10 秒到 30 秒之间,后续能自愈。

4.3 Lsyncd 同步延迟和忘记删除参数的问题

Lsyncd 在实际运行中,最大的坑是同步延迟突然变大。这种情况通常发生在源端出现大量小文件写入时,比如你往共享目录里放了上万个文件,Lsyncd 要逐个监听事件、再触发同步,很容易积压事件。

遇到这类问题的排查路径有这么几步:先看 Lsyncd 日志,确认是不是有大量同步任务堆积;再用top查看 rsync 进程的状态,看它是不是一直在传输数据;然后检查目标端磁盘的 IO 状况,如果备节点磁盘性能本身拉胯,也会拖慢同步速度。

解决办法是调整delay参数,把小文件的频繁触发聚合成大的同步批次。我当时把delay从 3 秒调到 5 秒,配合insync设为 2 秒,整体同步压力就降下来了。但注意,delay调得太大,丢失窗口也跟着变大,这个参数要根据实际业务容忍度来调,不要为了追求性能牺牲数据安全。

4.4 故障切换后数据一致性检查清单

故障切换后最怕的是什么?是数据看起来正常,但实际和预期不一致。所以我在每次故障演练或者真实故障恢复后,都会按照下面这个检查清单过一遍。

第一步,确认虚拟 IP 已经漂移到目标节点,用ip addr show检查。第二步,确认备节点的 NFS 服务正在运行,并且共享目录已经导出了。第三步,从 Kubernetes 节点重新挂载并访问 NFS,确认 IO 正常。第四步,对比主备节点的关键目录文件数量和大小,确认数据同步没有异常。第五步,检查应用日志,看存储相关的报错是否已经完全恢复。这套清单执行下来,基本上能覆盖大部分隐患,让我对集群状态心中有数。

4.5 日常巡检与监控建议

这类高可用架构不是搭完就一劳永逸了,日常巡检和监控非常重要。我个人的习惯是至少做三项监控:

第一项是 Keepalived 状态监控。写个脚本定时检查虚拟 IP 在哪个节点上,当虚拟 IP 发生漂移时立刻告警,说明发生了主备切换,需要关注当时是否有异常事件。

第二项是 NFS 服务可用性监控。从 Kubernetes 节点上定时执行showmount -e $VIP,确认 NFS 服务在虚拟 IP 上正常响应。如果连续多次失败,就要检查 Keepalived 和 NFS 服务的状态。

第三项是磁盘容量监控。这个最容易被人忽视。因为 NFS 的共享目录是动态增长的,一旦数据盘写满,整个存储服务会变得极其缓慢甚至直接不可用。我就经历过一次,因为备份任务异常,一天内写满了整个数据盘,导致 NFS 服务完全卡死,Kubernetes 集群里所有使用存储的 Pod 全部异常。那次之后,我在监控系统里为数据盘容量设置了 80% 的告警阈值,并加了自动化清理任务,再也没出过类似的事故。

现在再回头看这个项目,我个人觉得最值得分享的心得其实是两条。第一条是架构设计时要敢于做减法。搭建高可用 NFS 集群这件事,完全可以用更复杂的分布式存储方案来做,但对我来说,主备加虚拟 IP 加实时同步这套组合已经能覆盖 95% 的稳定性需求,剩下那 5% 的极端场景,越复杂的方案反而越容易在关键时刻出幺蛾子。第二条是故障演练真的很重要。我在正式上线前做了至少五轮主节点强制宕机测试,每一轮都能发现新的问题,比如一开始 Keepalived 的心跳超时设置太短导致误切换、后来 Lsyncd 的删除同步参数没配好等等。这些问题如果不通过故障演练提前暴露,真到了故障来临时才去现找原因,这个代价就太大了。如果你也在规划类似的高可用存储方案,建议你把自己的数据丢失容忍度、故障切换时间、运维维护能力这三个指标先列出来,再对照着选技术方案,不要盲目追求“全高可用”,最适合你的才是最好的。

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

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

立即咨询