服务器存储这块,越到后头越会发现,NFS这个东西又爱又恨。容器云跑久了,后端存储一旦还挂在单个NFS节点上,风险就明摆在那儿:节点宕机、网络抖动、内核锁问题,随便哪个都能让一堆Pod卡死。今天这篇我把自己在容器云后端存储NFS高可用适配这条路上的完整思路、踩坑记录和一版可落地的方案整理出来,给还在用单点NFS扛事的朋友一个参考。
1. 容器云后端存储为什么绕不开NFS
1.1 持久化存储的三条技术路线怎么选
做容器云的持久化存储,无非三条路:块存储、文件存储、对象存储。很多人一上来就追求高性能走块存储,但在实际业务场景里,文件存储的使用频率远比你想象的高。
块存储这块,iSCSI、Ceph RBD确实性能猛,但它的AccessMode在Kubernetes里天生受限。块设备本质上是一个裸盘,你没法让两个节点同时读写同一个块设备(除非配OCFS2、GFS2这种共享集群文件系统,那个复杂度直接拉满)。所以RBD卷在K8s里通常是ReadWriteOnce,只适合单节点、单Pod使用,遇到多副本同时写共享目录的场景就彻底没戏。
对象存储走S3协议,MinIO、Ceph RGW这类的,胜在容量大、扩展性好,适合存备份、日志、静态文件。但它不是POSIX文件系统,Pod要挂载得套s3fs、goofys这种FUSE层,性能损耗大,可靠性也一般。真要让应用像访问本地目录一样读写,对象存储不是正解。
文件存储这个赛道里,NFS能活到今天这地位确实有两把刷子。它天生支持多节点同时读写,Kubernetes原生就支持NFS卷,accessModes直接标ReadWriteMany。你不需要额外部署CSI插件(虽然有更好的选择),不需要搞什么FUSE,一台Linux机器上天然就有NFS客户端。对于自建容器云、私有化交付场景,NFS几乎是最低成本的共享存储方案。
1.2 NFS在真实容器云里的角色定位
我接触过的私有化容器云项目,NFS承担的角色基本就这几类:
- 应用共享上传目录,比如商城系统的商品图片、Oss替代方案里的公开读写目录
- CI/CD流水线共享构建缓存、Maven仓库、npm缓存
- 日志采集的共享挂载,Filebeat、Fluentd把日志写到共享目录再统一采集
- 数据库的备份文件落地目录,mysqldump、pg_dump输出到NFS再打包归档
- 有状态中间件的持久化目录,比如Elasticsearch的snapshot仓库
这几类业务有一个共同特点:数据不能丢,但性能要求不像数据库主库那么极致。NFS恰好卡在这个区间里,部署简单、协议成熟、运维门槛低。
单点NFS最危险的问题不在于性能,而在于——它一旦挂掉,所有挂载它的Pod全部卡死,甚至拖累整个节点的kubelet。Kubelet在挂载NFS的设备上做健康检查、容器启动、镜像拉取,只要NFS无响应,kubelet就会异常,节点上所有Pod都会被标记为NotReady。这就是为什么NFS高可用适配不是可选项,而是必选项。它解决的不仅是"存储不丢",还有"整个容器云不因为存储单点而雪崩"这个更严重的隐患。
2. 高可用NFS服务端的架构核心
2.1 三个层面的高可用要分开理解
做NFS高可用,最忌讳一上来就装个Keepalived把VIP漂移过去就完事。真正的高可用要拆成三个层面看:
第一层是服务进程高可用。NFS服务端进程挂掉、NFS服务无响应,需要有机制检测到并把服务拉起来。这一层靠Keepalived、Corosync这类集群软件做健康检查和VIP漂移就能覆盖。
第二层是数据高可用。服务进程切换过去了,数据得跟上。如果主节点物理机直接报废,数据还在旧节点的磁盘上,新节点顶上也是个空壳。这一层需要底层做数据同步,DRBD块级镜像、GlusterFS副本机制、rsync同步目录都属于这一层。
第三层是客户端挂载高可用。服务端一切正常,但客户端挂载参数的配置不当,NFS服务端短暂切换后客户端没有自动恢复,照样出问题。这一层靠挂载参数和协议版本选型来保障。
三层的核心逻辑其实是一条链:客户端怎么找到NFS服务端?通过VIP。VIP漂移由谁控制?集群软件。集群软件切换的依据是什么?健康检查结果。切换过去之后数据从哪来?底层同步机制。这四环缺一不可,每一环都关系到最终故障切换的成败。
2.2 数据同步方案选型:DRBD、GlusterFS还是rsync
数据同步这层,我见过不少野路子,这里把主流方案梳理一下。
DRBD + Pacemaker + Corosync这套是我目前最推荐的组合。DRBD做块级别实时镜像,写请求在主备两块磁盘上同时落盘,数据一致性有硬保证。Pacemaker负责资源编排,Corosync负责集群成员管理和心跳通信。故障时Pacemaker把VIP、DRBD主角色、NFS服务这套资源整体切换到备机。这套方案的优点是数据一致性最好,没有同步延迟窗口,备机随时可以顶上。缺点是两台机器磁盘性能对齐,网络最好走独立心跳线,部署复杂度偏高。
GlusterFS + CTDB + NFS-Ganesha是另一种常见做法。GlusterFS做分布式副本存储,CTDB做集群协调,NFS-Ganesha把GlusterFS卷导出成NFS协议。这套方案的好处是扩展性比DRBD好,可以做到双活,多个节点同时提供NFS服务。代价是组件太多,故障排查链路长,脑裂处理不好容易出数据一致性问题。中小规模集群用这套,运维成本偏高;大规模场景几百TB甚至PB级数据量,才值得上Ganesha。
rsync + inotify这套最轻量。inotify监听文件变化,实时rsync推送到备机。好处是部署极简,不需要块设备,适合数据量不大、容忍秒级延迟的场景。缺点是同步有延迟窗口,主节点"假死"时备机的数据一定落后,文件正在写一半的时候切换,丢数据几乎不可避免。这套方案只适合测试环境或数据可丢失的场景,生产不建议。
从我的实践经验来看,数据量几十TB以内、业务对一致性要求高的场景,DRBD主备是首选。它能保证切换时备机数据的完整性和一致性,这在容器云这种对数据可靠性要求极高的环境里非常关键。
2.3 主备模式和双活模式的取舍
很多人在设计NFS高可用时纠结要不要上双活,追求"两个节点同时干活"的爽感。但这里我想泼一盆冷水。
主备模式的精髓在于简单和可预期。主节点挂了,VIP飘到备节点,备节点把DRBD资源提升为主,启动NFS服务,整个过程通常30秒到1分钟内能恢复访问。切换期间业务中断是事实,但对容器云场景来说,Pod在NFS恢复后会自动重连,加上有状态服务的Pod调度和重启,这个中断窗口是可控的。主备模式最大的优点是脑裂风险极低——备机在没有拿到主角色之前是绝对不会提供服务的,数据一致性天然有保障。
双活模式两个节点同时提供NFS,客户端通过多个VIP或DNS轮询访问,理论上做到了负载均衡和故障无缝切换。但双活必须解决同时写同一个文件的锁协调问题,NFS协议本身在这方面是弱项。实际做下来,要么性能受锁协调拖累,要么干脆用分目录的方式做"伪双活"——不同目录由不同节点主服务,互为备份。搞到最后,复杂度上来了,收益却有限。
所以我的建议很直接:容器云后端存储NFS高可用,至少先做好主备,再谈双活。主备架构把风险降下来,运营一段时间积累足够的故障切换经验后,再考虑扩展。双活不是不好,是在NFS这个协议体系下去做好它,性价比确实不高。
3. 容器云侧NFS适配的细节实践
3.1 PV/PVC配置里的高频坑位
容器云这侧,PV、PVC这些配置看似简单,实际操作里到处是暗坑。
PV的server地址一定要写VIP。我之前接手过一个项目,PV里直连NFS节点IP,高可用集群做了一堆,结果故障切换时Pod全部卡死,原因就是PV指向了旧的主节点IP。容器云高可用适配的第一步,就是把所有NFS相关配置里的server地址统一改成VIP,要让客户端只认VIP,不认节点IP。
accessModes建议用ReadWriteMany。如果是ReadWriteOnce,Kubernetes会限制整个卷同时只能被一个节点挂载,多副本Pod调度到不同节点直接挂不上。NFS天然支持多节点读写,PV里直接声明ReadWriteMany就完了,别给自己找麻烦。
mountOptions建议显式指定nfsvers。不要依赖客户端自动协商,因为不同Linux发行版的NFS客户端默认行为不一样,有的自动协商到NFSv3,有的到NFSv4.0,行为差异很大。显式指定版本,行为可预期,排查问题也方便。
一个基本的PV定义大概长这样:
apiVersion: v1 kind: PersistentVolume metadata: name: nfs-shared-pv spec: capacity: storage: 500Gi accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain mountOptions: - nfsvers=4.1 - hard - timeo=30 - retrans=3 nfs: server: 192.168.1.100 path: /data/nfs/shared这里Retain策略的意思是PV释放后不自动删除数据,适合有状态应用的数据目录。如果你用的是动态供给,回收策略通常配Delete,PVC删除时自动清理数据,这个按业务需求来。
3.2 挂载参数调优:为什么我不推荐soft挂载
NFS挂载参数里,最经典也最容易引起争议的就是hard和soft的选择。我的结论很简单:容器云场景下,别用soft,用hard + 合理的timeo、retrans组合。
先解释原理。hard挂载下,NFS客户端在服务端无响应时会无限重试I/O请求,上层应用进程会阻塞在系统调用上,表现为进程挂起、文件读写无响应。soft挂载下,客户端在重试达到一定次数后会放弃并返回I/O错误,上层应用会收到Input/output error。
很多人觉得soft更"智能",服务端挂了至少不会无限卡死。这个想法在传统物理机场景下是对的,但在容器云里恰恰相反。Kubernetes里,Pod读写NFS返回I/O错误通常意味着应用崩溃或数据写入中断。比如数据库正在写redo日志,NFS突然返回I/O错误,数据库会认为磁盘损坏直接进入恢复模式,损失的数据比"卡一会儿"严重得多。而hard挂载虽然会让应用阻塞,但只要服务端在超时窗口内恢复,I/O请求会自动重放成功,应用几乎无感知。
timeo参数控制的是重试超时时间,单位是0.1秒。默认值600,也就是60秒,对故障切换场景来说太长了。我会把它调到30(3秒),这样VIP漂移过程中客户端每3秒重试一次,不会傻等一分钟才恢复。retrans控制重试次数,默认2次,我习惯调到3次,适当增加容忍度。整套组合下来,NFS切换期间Pod最多卡住几秒,VIP一恢复流量自动续上,业务几乎无感知。
rsize和wsize这两个参数,建议直接保持默认或设成1MB。这两个控制NFS读写缓冲块大小,调大了对顺序读写有提升,但会消耗更多内存;调小了在高并发场景会导致内核频繁唤醒发送线程,性能反而下降。实测下来多数场景1MB(1048576)是个比较稳的点,没必要为了极限性能去冒险。
3.3 动态供给和静态供给怎么选
容器云里NFS的供给方式分两种:手动创建PV配PVC的静态供给,和通过StorageClass自动创建子目录的动态供给。
静态供给适合「一片共享存储、多个应用复用」的场景。比如你有一块公共上传目录,让所有业务Pod都能挂载,管理员手动建一个PV指向NFS固定路径,各业务PVC直接绑定就行。这种方式管理成本低,数据归属清晰。
动态供给适合「每个PVC需要独立空间、按需创建」的场景。核心组件是nfs-subdir-external-provisioner,它会监听PVC创建事件,在NFS服务器上自动创一个以namespace-pvcname命名的子目录,然后创建PV并绑定。这样开发人员申请存储时不用找管理员手动创建PV目录,效率高很多。实际操作中,这个provisioner的部署非常简单,只需要提供NFS服务器的IP、共享路径、StorageClass名称即可。
在一个高可用NFS环境里,动态供给的底层server地址同样必须指向VIP,provisioner的Deployment最好也调度到能访问VIP的节点上。StorageClass定义大概长这样:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-sc provisioner: k8s-sigs.io/nfs-subdir-external-provisioner parameters: archiveOnDelete: "false" # PVC删除时是否保留数据 pathPattern: "${.PVC.namespace}/${.PVC.name}" # 目录命名规则 reclaimPolicy: Delete volumeBindingMode: Immediate mountOptions: - nfsvers=4.1 - hard - timeo=30 - retrans=3我的习惯是:共享目录类业务用静态PV,业务私有数据用动态供给。两条路配合使用,运维既有掌控力,业务也有灵活性。
4. 实操实录:一套可落地的NFS高可用方案
4.1 服务端部署全流程
为了讲清楚整个落地过程,我以Ubuntu 22.04 + Keepalived + DRBD为例,给你跑一遍完整流程。这套方案我实际部署过多次,稳定性和可维护性都经过验证。
环境规划如下:
| 角色 | IP地址 | 用途 |
|---|---|---|
| nfs-node1 | 192.168.1.10 | 主NFS节点 |
| nfs-node2 | 192.168.1.11 | 备NFS节点 |
| vip | 192.168.1.100 | NFS服务虚拟IP |
| 心跳网段 | 10.10.10.0/24 | DRBD数据同步专用 |
第一步,基础环境准备。两台节点分别安装所需软件包:
# 两台节点都执行 apt update && apt install -y nfs-kernel-server keepalived drbd-utils安装完成后,NFS服务默认配置文件为/etc/default/nfs-kernel-server。这里注意,DRBD需要独立磁盘分区,建议用独立数据盘,不要在系统盘上直接分区,否则系统盘满了数据就遭殃了。
第二步,配置DRBD数据同步。假设每台机器有一块独立的裸磁盘/dev/sdb,创建一个DRBD资源文件/etc/drbd.d/nfsdata.res:
resource nfsdata { protocol C; # 同步写协议,保证数据落盘后再确认 on nfs-node1 { device /dev/drbd0; disk /dev/sdb; address 10.10.10.1:7788; meta-disk internal; } on nfs-node2 { device /dev/drbd0; disk /dev/sdb; address 10.10.10.2:7788; meta-disk internal; } }初始化DRBD元数据并启用资源:
# 两台节点都执行 drbdadm create-md nfsdata drbdadm up nfsdata # 只在主节点nfs-node1上执行 drbdadm primary nfsdata --force mkfs.xfs /dev/drbd0 mount /dev/drbd0 /data这里protocol C是DRBD的强一致模式,写请求必须等备节点确认落盘后才返回成功。主备之间网卡速度要跟上,万兆网卡最佳。数据一致性和性能之间,我选一致性。
第三步,配置NFS导出。在/etc/exports中添加共享目录:
/data 192.168.1.0/24(rw,sync,no_root_squash,no_subtree_check)sync参数务必保持开启,它保证写入NFS缓存的数据同步落盘后才返回写成功。no_root_squash让容器内的root用户对NFS目录有完整读写权限——这在容器场景很关键,因为很多容器进程默认以root运行,如果开着root_squash,会出现"明明有权限却写不进去"的诡异问题。
第四步,配置Keepalived健康检查。这是整个高可用方案的灵魂。Keepalived做的不只是VIP漂移,还必须在NFS服务异常时主动让出VIP。写个健康检查脚本/etc/keepalived/check_nfs.sh:
#!/bin/bash # 检查NFS进程是否存活 if ! pgrep -x "nfsd" > /dev/null; then exit 1 fi # 检查NFS共享是否可访问 if ! showmount -e 127.0.0.1 > /dev/null 2>&1; then exit 1 fi # 检查DRBD主备状态 if ! drbdadm status nfsdata | grep -q "Primary"; then exit 1 fi exit 0然后写Keepalived主配置/etc/keepalived/keepalived.conf:
global_defs { router_id nfs_ha } vrrp_script check_nfs { script "/etc/keepalived/check_nfs.sh" interval 2 timeout 2 fall 2 rise 2 } vrrp_instance VI_NFS { state BACKUP interface ens160 virtual_router_id 51 priority 150 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 192.168.1.100 } track_script { check_nfs } }两个节点的配置几乎一样,差别只在于priority。主节点设150,备节点设100。一旦主节点NFS健康检查失败超过2次,备节点会接管VIP。RTO经验值大概在5到10秒。如果主节点配置了state MASTER且优先级更高,恢复后它会强行抢回VIP,这可能会导致双写窗口,所以我倾向于两个节点都配state BACKUP,靠优先级来区分主备——避免"抢主"过程带来的不必要抖动。
第五步,启动服务。主节点上顺序启动DRBD、挂载、启动NFS、启动Keepalived。备节点上DRBD处于Secondary状态,不挂载文件系统、不启动NFS服务,只启动Keepalived。整个过程中的启动顺序要严格遵守:先启动DRBD,确认主备状态,然后主节点挂载文件系统并启动NFS,最后才启动Keepalived。顺序错了会出现"备节点接管VIP但没有NFS服务"的尴尬局面。
完整验证方式,在任意客户端执行:
showmount -e 192.168.1.100能看到/data共享清单,说明高可用链路通了。再执行mount -t nfs 192.168.1.100:/data /mnt/test,写入测试文件后观察主节点和备节点的数据是否一致——DRBD的/dev/drbd0应该是同步的,挂载冗余角度来说看不到差别。
4.2 容器云侧StorageClass和provisioner部署
服务端就绪后,容器云这侧的接入就顺畅多了。先创建一个StorageClass再部署nfs-subdir-external-provisioner,组件以Deployment形式跑在集群里。
它的作用说白了两件事:监听PVC创建事件,在NFS共享目录里自动创建子目录;创建PV并绑定PVC。部署文件主体长这样:
apiVersion: v1 kind: ServiceAccount metadata: name: nfs-provisioner --- kind: Deployment apiVersion: apps/v1 metadata: name: nfs-provisioner spec: replicas: 1 selector: matchLabels: app: nfs-provisioner template: metadata: labels: app: nfs-provisioner spec: serviceAccountName: nfs-provisioner containers: - name: nfs-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: k8s-sigs.io/nfs-subdir-external-provisioner - name: NFS_SERVER value: 192.168.1.100 - name: NFS_PATH value: /data volumes: - name: nfs-client-root nfs: server: 192.168.1.100 path: /data这里的NFS_SERVER和NFS_PATH必须和服务端完全对齐。provisioner所在的Pod挂载底层NFS目录时,同样要指定nfsvers=4.1、hard这些参数。provisioner的副本数保持1个就行,它本身由Kubernetes的Deployment机制保障,节点挂了会调度到其他节点重新拉起。
接下来只需要在业务里声明PVC,StorageClass会自动完成存储供给:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nginx-data spec: accessModes: - ReadWriteMany storageClassName: nfs-sc resources: requests: storage: 10Gi创建这个PVC后,你可以去NFS服务器的/data目录下看一眼,系统已经自动创建了对应的子目录。整个流程不需要人工干预,这就是动态供给的价值。
4.3 故障切换演练与验证
方案部署完不等于万事大吉,故障切换必须演练。我一般会做三组常规测试。
第一组,停NFS进程。在主节点上执行systemctl stop nfs-kernel-server,此时健康检查脚本检测不到nfsd进程,Keepalived会在大约5秒内把VIP漂移到备机。观察客户端挂载恢复时间——我实测中通常在10-15秒内,客户端对挂载目录的读写恢复正常。如果超过30秒还没恢复,优先查Keepalived状态和健康检查脚本的返回值。
systemctl stop nfs-kernel-server # 观察VIP漂移 ip addr show | grep 192.168.1.100第二组,停Keepalived。这个测试极其重要,因为检查的是主节点Keepalived进程异常时VIP能否正确漂移。执行kill -9 $(pgrep keepalived)后,备节点也会在几秒内接管VIP。这个场景模拟了集群软件自身故障,比停NFS进程更残酷,也更接近真实事故。
第三组,模拟节点宕机。这个我建议在业务低峰期测试,直接在主节点上执行shutdown -h now,或者拔掉主节点网线。整个过程VIP漂移、NFS接管会自动完成,服务端视角的RTO也在15秒左右。关键观察点在于:旧的主节点起来之后,不会和新的主节点发生IP冲突、VIP漂移回旧节点等行为。
演练结束后,任何一次切换都要在主备节点上确认:
drbdadm status nfsdata # 确认DRBD状态为 Primary/Secondary 或 Secondary/Primary ip addr show | grep 192.168.1.100 # 确认VIP当前所在节点 showmount -e 127.0.0.1 # 确认当前节点的NFS服务正常这套演练流程走完,基本心里就有底了——再多的小概率故障,只要VIP能漂移、DRBD数据能跟上,容器云侧的Pod就不会大面积雪崩。
5. 常见问题与排查技巧实录
5.1 Pod挂载卡死怎么排查
容器云里NFS出问题最常见的一个现象:Pod还在Running状态,但应用读写无响应,df -h命令在节点上也卡住不动。这种情况十有八九是NFS服务端出了问题,客户端在等I/O超时。
排查顺序记住这条链路:
- 先看NFS服务端的健康状态。
systemctl status nfs-kernel-server在VIP所在节点上执行,看NFS进程是否在跑 - 看VIP在哪个节点上。在客户端执行
ip addr show或者cat /proc/net/fib_trie找VIP所在的MAC地址 - 在客户端手动尝试挂载。先umount再重新mount一次,把输出记录下来
- 查看节点内核日志。
dmesg -T | grep nfs,会看到类似NFS: nfs4_discover_server_trunking unhandled error -110这种报错,-110表示超时
如果客户端已经卡死,强制卸载再用重启Pod来解决:
umount -lf /var/lib/kubelet/pods/xxx/volumes/kubernetes.io~nfs-lf参数是lazy强制卸载,先摘掉挂载关系,再让内核后台清理。这一步在真实故障中经常用来"救火"——先让kubelet恢复,再处理NFS服务端的问题。Pod侧的恢复我一般建议直接删除Pod让它重新调度,Kubernetes会自动重新挂载。
5.2 故障切换后数据异常或权限异常
主备切换后最糟心的问题是数据找不到了。先别慌,这类问题大概率不是数据丢了,而是挂载到了旧状态。
场景一,VIP漂移后客户端ARP缓存没更新。NFS客户端通过ARP找到VIP对应的MAC地址,VIP漂移到备机后,部分客户端还在往旧MAC发数据。处理方式是在客户端执行arp -d清理缓存,或者等ARP老化时间到期。这个问题在跨网段、经过交换机环境更隐蔽,建议在网络设备层面做VRRP相关的组播和ARP配置时,提前确认路由器不缓存旧ARP。
场景二,DRBD主备角色和数据没同步。主备切换后,一定要确认当前节点是DRBD Primary,且底层文件系统挂载OK。如果DRBD状态不对,NFS共享目录看到的可能是旧的空目录。这时候先别乱动,确认DRBD同步情况再继续:
drbdadm status nfsdata cat /proc/drbd场景三,权限错乱。这个几乎每个NFS高可用环境都会遇到,特别是应用容器以root运行时。no_root_squash和root_squash混用,或者NFS导出目录的属主变了,都会导致Pod写文件报Permission denied。建议把NFS共享目录属主统一设置为nfsnobody或者用all_squash加anonuid/anongid固定映射用户,避免每次切换后属主错乱。
5.3 NFS高可用适配注意事项速查表
按我踩坑的经验,把最常见的坑整理成一张表,部署前对着过一遍能省不少事:
| 问题 | 风险等级 | 建议做法 | 原因 |
|---|---|---|---|
| PV/server直连节点IP而非VIP | 高 | 所有NFS地址统一指向VIP | 直连节点IP时,故障切换对客户端无效 |
| 使用soft挂载 | 中 | hard + timeo=30 + retrans=3 | soft返回I/O错误,可能导致数据写坏 |
| NFS版本未锁定 | 高 | mountOptions显式指定nfsvers=4.1 | 自动协商可能降到v3,锁行为差异引发诡异问题 |
| 导出根目录/ | 高 | 单独数据分区,导出/data等子目录 | 导出根目录会导致客户端可访问整个文件系统,风险极大 |
| 忽略root_squash设置 | 中 | 根据容器场景选择no_root_squash或固定映射 | 容器内root用户可能无法写入,或权限过宽 |
| DRBD和心跳共用业务网络 | 高 | 独立心跳专线,建议万兆 | DRBD协议C的数据同步占用带宽大,业务网络拥堵时数据同步受阻 |
| 主节点state配成MASTER | 中 | 用BACKUP+优先级区分主备 | MASTER节点恢复后会强抢VIP,引发不必要抖动 |
| 缺乏故障演练 | 高 | 至少每季度一次切换演练 | 从没验证过的高可用等于没有高可用 |
这里特别提醒一句,NFS文件锁的问题值得重点关注。多副本Pod同时写NFS上的同一个文件,而NFSv4之前的版本锁支持不完善,容易出现锁冲突、锁丢失的问题。容器云场景下,如果业务要并发写同一个文件,建议应用侧做文件分片,或者直接改走数据库、对象存储——NFS的定位是共享存储,不是并发锁服务。
5.4 优化NFS挂载性能的其他细节
除了高可用适配,很多人也会问:同样挂NFS,为什么我的读写比别人的慢一大截?我提供一个排查思路。
先检查网络。ping VIP的延迟如果超过5ms,那NFS读写延迟基本没救,NFS每个读请求至少一个RTT。再检查挂载参数。如果挂载时没加noatime,每次文件访问都会触发atime更新,相当于每个读请求都要附带一个写操作,性能直接打对折。加挂载参数时,把noatime带上。
还有一点容易被忽略:NFS客户端在Kubernetes节点上的挂载数量。当节点上挂载的NFS卷数量过多时(几十个起步),内核的NFS客户端线程会成为瓶颈。这个可以通过调整/sys/module/nfs/parameters/nfs_congestion_kB适当增大拥塞窗口。不过这个调优属于进阶话题,先保证高可用适配到位,再扣性能细节。
另外一个通用建议:不要在容器云节点上直挂大量NFS卷再共享给Pod。每个NFS卷在节点上就是一个挂载点,数量多了之后kubelet的volume管理器压力非常大。更好的做法是通过StorageClass动态供给、按需创建,避免闲置NFS卷在每台节点上堆积。
写在实际操作之后的几点体会
这套NFS高可用适配方案跑下来,最大的收获不仅仅是VIP能漂移、数据能同步,而是让我对容器云存储的整体风险有了更清晰的认知。存储高可用是一个系统工程,不是某一个组件能单独兜底的。Keepalived能解决服务漂移,DRBD能解决数据同步,但最终决定故障切换成不成的,往往是那些最容易被忽视的细节——PV里写的是哪个IP、挂载参数用的什么策略、exportfs做了哪些权限映射、健康检查脚本到底查了什么。
如果让我给一个刚起步的团队做NFS高可用,我会说:先别急着上复杂的GlusterFS集群或者CephFS,从DRBD主备加Keepalived这套组合开始,跑熟切换流程,把故障演练固化成例行操作,再慢慢演进到更复杂的架构。高可用不追求极致技术,追求的是故障发生时系统有确定性的行为和可预期的恢复时间。
最后分享一个小技巧:把你的健康检查脚本包含的检查项写到监控系统里,每5分钟跑一次。这样你在NFS真正故障之前,就能通过脚本的检查结果提前发现隐患,比如DRBD同步延迟变大、NFS进程即将异常等。存储系统的故障从来不是瞬间发生的,抓住前兆,比事后切换更重要。