跳转至

NFS 存储类(StorageClass)交付 SOP

交付目录:nfs-storage/ 变更类型:C 类生产变更(宿主机 NFS 服务端 + 集群侧 CSI 驱动与存储类) 首次交付:2026-09-17 状态基准:本文档已按 2026-09-19 挂载参数调优后的当前生效状态收敛,可直接作为重建、日常运维与回滚的依据 实现方式声明:本方案按用户明确指令未采用 Python 部署工程,实现体为 nfs-storage/values-nfs-csi.yaml 与 nfs-storage/manifests/ 下的清单。第 5 章的 shell 代码块属文档呈现,用于阅读、复核与手工复现, 真正决定最终状态的是实现体,以实现体为准。 边界声明:挂载参数调优由独立项目 nfs-mount-tuning/ 完成(其实现体为 Python 工程,过程见该项目 docs/SOP.md)。 本文档只记录其最终生效值,不重复其变更过程;两者若出现不一致,以本文件第 4 章实况核对为准并回查该项目日志。 与 RUNBOOK 的分工:本文只回答"这次改了什么、怎么改、怎么验、怎么退"。 服务的日常使用与排障(速查命令、故障场景处置、监控入口、容量扩缩操作)见 nfs-storage/RUNBOOK.md,本文不重复。

目录


1. 背景

  • 集群 k8s_ins(v1.36.4,4 节点:1 control-plane + 3 worker)在本次交付前没有任何 StorageClass、PV、PVC, 有状态应用无法落地;早先规划的 GlusterFS 方案因上游停止维护被放弃(集成层已被 K8s 移除、Heketi 归档)。
  • 历史经验:早期 GlusterFS 集群长期不可用,实际承担共享存储的一直是 NFS。故本环境的真实需求是 一个稳定、可 RWX、可动态供给的 StorageClass,而非分布式块存储。
  • 宿主机 dell-R620(Ubuntu,32 线程 / 117G 内存)是 4 台 k8s VM 与 openwrt 的 KVM 宿主, 有独立 SSD 系统盘与 1.8T HDD 数据盘,具备提供 NFS 服务端的条件。
  • 交付后按实际使用演进过两轮,构成本文档的当前状态:
  • 快照落盘分流:驱动默认把快照归档写到源卷所在 share,会与卷数据抢 SSD 的 120GiB 预算,故将归档分流到 HDD;
  • 挂载参数调优:HDD 后端在 sync 导出下元数据 RTT 高达 60–80ms,改 async + nfsvers=4.2 + nconnect=4 后降至 0.2–0.4ms 量级(详见第 6 章性能基线)。
  • 触发条件:为测试与学习场景提供默认 StorageClass,并保留快照/恢复能力,同时满足元数据延迟敏感性要求。

2. 目标

变更完成后应持续维持以下可验证状态:

编号 目标状态 验证方式
G1 宿主机存在三处服务于本方案的导出:/srv/nfs/k8s(SSD)、/srv/nfs/k8s-hdd(HDD)、/data/nfs-k8s-snap(快照),均为 async 第 6 章 V1
G2 nfsd 线程数为 32,且重启后仍生效 第 6 章 V2
G3 两个 loop 后端挂载含 noatime,prjquota,容量上限分别为 120GiB / 500GiB 第 6 章 V3
G4 集群存在 nfs-csi(默认, SSD) 与 nfs-csi-hdd(非默认, HDD) 两个 StorageClass 第 6 章 V4
G5 客户端挂载参数为 nfsvers=4.2, nconnect=4, hard, timeo=600, retrans=3, noatime,且 SC / Secret / 已有 PV 三处一致 第 6 章 V5、V6
G6 PVC 可自动供给并 Bound,跨节点 RWX 读写正常 第 6 章 V8、V9
G7 支持快照与从快照恢复,且归档落在 HDD,不占用 SSD 预算 第 6 章 V7
G8 删除 PVC 时后端子目录被回收 第 6 章 V10
G9 宿主机重启后 loop 挂载、导出、nfsd 线程数自动恢复 第 6 章 V11(尚未验证)
G10 全部操作幂等:重复执行零变更;且不影响服务端上的共存业务(Slurm) 第 6 章 V12

3. 前置条件

权限

  • 宿主机需 root,且 sudo -n 免密可用(当前已确认)。
  • 集群需 cluster-admin(当前上下文 kubernetes-admin@kubernetes)。

工具与依赖

依赖 位置 当前版本 说明
helm 宿主机 v3.16.3(/usr/local/bin/helm) 本次交付时安装
kubectl 宿主机 v1.36.4 已有
nfs-kernel-server 宿主机 1:2.6.4-3ubuntu5.1 本次交付时安装
csi-driver-nfs chart 本地固化 4.13.4 nfs-storage/charts/csi-driver-nfs/
节点侧 nfs 客户端 无需安装 — 驱动镜像基于 debian-base 且显式安装 mount nfs-common(官方 Dockerfile v4.13.4),挂载由容器执行

执行前检查项

  1. 宿主系统盘可用空间充足:sda3 220G,实测可用约 160G(120GiB 镜像为按需增长)。
  2. /data(HDD)已挂载、可用约 1.7T;其上同时有运行中的 VM qcow2、本方案的 HDD 后端与快照目录、 以及 Slurm 状态导出。禁止对 /data 本体做任何 umount 操作。
  3. 集群节点可拉取 registry.k8s.io 镜像(节点经 openwrt 旁路,实测可直连)。
  4. 集群内快照 CRD 由本方案以 kubectl 独立管理,确认无第二套实现重复注册。
  5. 变更服务端共享配置(导出参数、/etc/nfs.conf 的 nfsd 线程、nfs-server 服务)前, 须确认 Slurm 与本方案全部业务的影响面(见第 4 章"共存服务与共享资源")。
  6. 涉及导出参数或挂载参数变更时,先停写敏感业务(当前为 server/mariadb、server/minio),流程见 nfs-mount-tuning/docs/SOP.md。

4. 影响范围

主机与组件

对象 角色 影响
dell-R620 sda3/SSD(192.168.31.249) NFS 服务端:SSD 卷后端 120GiB 镜像 /kvm/nfs/k8s-120g.img + loop 挂载 /srv/nfs/k8s;占用系统盘空间(按需增长)
dell-R620 /data/HDD NFS 服务端:HDD 卷后端 + 快照归档 500GiB 镜像 /data/nfs/k8s-hdd-500g.img + loop 挂载 /srv/nfs/k8s-hdd;快照目录 /data/nfs-k8s-snap
dell-R620 /etc/exports、/etc/nfs.conf、nfs-server 服务 共享资源 本方案与 Slurm 导出共用:改导出、改 nfsd 线程、启停服务会同时影响 Slurm
k8s-master-01 / node-01/02/03 CSI node 插件(DaemonSet) 每节点常驻 3 个容器(含 master,其 tolerations 容忍全部污点),约 10m CPU / 20Mi 起
kube-system 驱动组件 controller Deployment、snapshot-controller Deployment、SA / ClusterRole / Secret
集群存储对象 本方案对象 两个 StorageClass、CSIDriver nfs.csi.k8s.io、3 个 snapshot CRD、VolumeSnapshotClass

是否需要停服

  • 常态运维(日常增删卷、查日志、改 values 中与参数无关的项):不需要停服。
  • 变更导出参数(sync/async)或客户端挂载参数:需要先停写敏感业务,并按 nfs-mount-tuning/docs/SOP.md 的静默流程执行;已完成卷需逐个 patch PV 并重启使用方(见第 5 章 5.3 与风险 R11)。
  • 重启 nfs-server 服务:会同时中断本方案与 Slurm 的存储访问,非必要不做(nfsd 线程数支持热调整,无需重启)。

窗口期:无强制窗口,建议低峰执行。/data 全程不做 umount,运行中的 VM 不受影响。

在跑的业务(2026-09-19 实况,共 11 个 NFS PV)

命名空间 PVC StorageClass 容量 使用方
server minio-data nfs-csi 50Gi MinIO(GitLab 对象存储)
server storage-mariadb-0 nfs-csi 20Gi MariaDB(数据库)
monitoring kube-prometheus-stack-grafana nfs-csi 10Gi Grafana
monitoring alertmanager-…-db-…-0 nfs-csi 10Gi Alertmanager
monitoring prometheus-…-db-…-0 nfs-csi-hdd 100Gi Prometheus
server conf-openldap-0 / data-openldap-0 / backups-openldap-0 nfs-csi-hdd 1/8/8Gi OpenLDAP(server 一套)
ldap conf-openldap-0 / data-openldap-0 / backups-openldap-0 nfs-csi-hdd 1/8/8Gi OpenLDAP(ldap 一套,与 server 并存,疑为迁移残留,待确认)

此外 GitLab 本体的 gitaly/postgres/redis 使用手工 PV + 集群既有的 local-ssd(非本方案引入)。

任何对 /srv/nfs/k8s、/srv/nfs/k8s-hdd、/data/nfs-k8s-snap、/etc/exports、/etc/nfs.conf 或 nfs-server 服务的操作,都会直接波及上表业务;执行前须确认其 PVC 处于 Bound 且无写入高峰。

共存服务与共享资源(Slurm)

  • 服务端上还有第四处导出 /data/nfs/slurm-state(HDD 裸目录,非 loop 镜像),供 Slurm 集群存放 slurmctld-state(uid/gid 64030)。该导出不属于本方案的交付与回滚范围,但与本方案共享以下资源:
共享资源 共享方式 影响
nfs-server 服务 同一进程同时对外提供全部导出 启停服务会同时中断 Slurm 与本方案
/etc/exports 文件 同一文件承载全部导出行 编辑、重载、全局撤销导出都会影响 Slurm
/etc/nfs.conf 的 [nfsd] threads 同一参数服务所有导出 调低线程数会同时拖慢 Slurm 的 NFS 访问
  • 因此本方案第 7 章的回滚只按导出粒度操作,不使用 exportfs -ua、不 stop nfs-server(详见 7.2)。

单机提示:存储服务端与全部计算节点同宿主,无冗余。宿主故障 = 存储与计算同时中断, 故第 6、7 章必须写实、可执行。

5. 分步操作

对应实现体:nfs-storage/values-nfs-csi.yaml(客户端参数与两个 SC)、 nfs-storage/manifests/*.yaml(Secret、CRD、快照类)。宿主机侧无脚本实现体,命令序列以本节为准。 高危动作已标注「高危」并给出对应备份命令。以下序列按当前生效参数编写,幂等可重复执行。 关键步骤后给出预期输出;若输出不符,先看该步注释里的判断依据,跨步骤问题转 RUNBOOK.md 第 8 章对应场景。 文中的 <节点IP>、<pv名>、<命名空间> 等占位符取值来源在各处就地说明。

5.1 宿主机侧(服务端)

# ---- 步骤 0:备份(高危动作前置,必须先做)----
sudo cp -n /etc/fstab   "/etc/fstab.bak.$(date +%Y%m%d)"       # 保护启动挂载
sudo cp -n /etc/exports "/etc/exports.bak.$(date +%Y%m%d)"     # 保护导出;首次执行时该文件可能不存在
sudo cp -n /etc/nfs.conf "/etc/nfs.conf.bak.$(date +%Y%m%d)"   # 保护 nfsd 线程等全局配置

# ---- 步骤 1:安装 NFS 服务端(高危:装系统包)----
dpkg -s nfs-kernel-server >/dev/null 2>&1 || sudo DEBIAN_FRONTEND=noninteractive apt-get install -y nfs-kernel-server

# ---- 步骤 2:创建后端镜像并格式化(高危:占用磁盘空间)----
# 2.1 SSD 卷后端:120GiB,位于系统盘 sda3
sudo mkdir -p /kvm/nfs
[ -f /kvm/nfs/k8s-120g.img ] || sudo fallocate -l 120G /kvm/nfs/k8s-120g.img
sudo blkid /kvm/nfs/k8s-120g.img >/dev/null 2>&1 || \
  sudo mkfs.ext4 -F -O project -L k8s-nfs /kvm/nfs/k8s-120g.img        # -O project 是 prjquota 的前提
# 2.2 HDD 卷后端:500GiB,位于 /data(sdb1)
sudo mkdir -p /data/nfs
[ -f /data/nfs/k8s-hdd-500g.img ] || sudo fallocate -l 500G /data/nfs/k8s-hdd-500g.img
sudo blkid /data/nfs/k8s-hdd-500g.img >/dev/null 2>&1 || \
  sudo mkfs.ext4 -F -O project -L k8s-nfs-hdd /data/nfs/k8s-hdd-500g.img
# 2.3 快照归档目录:HDD 裸目录(非镜像,弹性占用 /data 剩余空间)
[ -d /data/nfs-k8s-snap ] || sudo mkdir -p /data/nfs-k8s-snap

# ---- 步骤 3:挂载并持久化(高危:改 fstab)----
# 3.1 SSD 后端
sudo mkdir -p /srv/nfs/k8s
findmnt -no SOURCE /srv/nfs/k8s >/dev/null 2>&1 || sudo mount -o loop,prjquota /kvm/nfs/k8s-120g.img /srv/nfs/k8s
grep -qF '/kvm/nfs/k8s-120g.img' /etc/fstab || printf '%s\n' \
  '/kvm/nfs/k8s-120g.img /srv/nfs/k8s ext4 noatime,loop,prjquota,nofail,x-systemd.device-timeout=10 0 2' | sudo tee -a /etc/fstab
sudo tune2fs -m 1 /kvm/nfs/k8s-120g.img                                # 保留块 5%→1%,释放约 4.7G
# 3.2 HDD 后端
sudo mkdir -p /srv/nfs/k8s-hdd
findmnt -no SOURCE /srv/nfs/k8s-hdd >/dev/null 2>&1 || sudo mount -o loop,prjquota /data/nfs/k8s-hdd-500g.img /srv/nfs/k8s-hdd
grep -qF '/data/nfs/k8s-hdd-500g.img' /etc/fstab || printf '%s\n' \
  '/data/nfs/k8s-hdd-500g.img /srv/nfs/k8s-hdd ext4 noatime,loop,prjquota,nofail,x-systemd.device-timeout=10 0 2' | sudo tee -a /etc/fstab
sudo tune2fs -m 1 /data/nfs/k8s-hdd-500g.img
# 3.3 若后端已挂载但缺少 noatime,用 remount 补齐(不卸载,不影响在用文件)
findmnt -no OPTIONS /srv/nfs/k8s     | grep -q noatime || sudo mount -o remount,noatime /srv/nfs/k8s
findmnt -no OPTIONS /srv/nfs/k8s-hdd | grep -q noatime || sudo mount -o remount,noatime /srv/nfs/k8s-hdd
sudo systemctl daemon-reload && sudo mount -a

# ---- 步骤 4:配置导出(高危:对外提供存储;会写入与 Slurm 共用的 /etc/exports)----
# 注意:仅追加/确保本方案的三行,不要重写整个文件,避免破坏 /data/nfs/slurm-state 的导出行
grep -qF '/srv/nfs/k8s ' /etc/exports     || printf '%s\n' '/srv/nfs/k8s 192.168.31.0/24(rw,async,no_subtree_check,no_root_squash)'      | sudo tee -a /etc/exports
grep -qF '/srv/nfs/k8s-hdd' /etc/exports  || printf '%s\n' '/srv/nfs/k8s-hdd 192.168.31.0/24(rw,async,no_subtree_check,no_root_squash)'  | sudo tee -a /etc/exports
grep -qF '/data/nfs-k8s-snap' /etc/exports || printf '%s\n' '/data/nfs-k8s-snap 192.168.31.0/24(rw,async,no_subtree_check,no_root_squash)' | sudo tee -a /etc/exports
sudo exportfs -ra                                                       # 重载全部导出(幂等,不影响已有导出)
sudo exportfs -v                                                        # 确认本方案三处 + Slurm 导出均在列且为 async
# 预期输出(四处,顺序不限):
#   /srv/nfs/k8s          192.168.31.0/24(async,...,rw,...,no_root_squash,...)
#   /srv/nfs/k8s-hdd      192.168.31.0/24(async,...)
#   /data/nfs-k8s-snap    192.168.31.0/24(async,...)
#   /data/nfs/slurm-state 192.168.31.0/24(async,...)      <- Slurm 的,不得消失
# 不符时:缺少本方案任一行 → 检查上一条 tee 是否写入成功;缺少 Slurm 行 → 立即用
#         /etc/exports.bak.* 还原(见 7.2),切勿继续后续步骤

# ---- 步骤 5:nfsd 线程数(热调整,无需重启服务)----
printf '32\n' | sudo tee /proc/fs/nfsd/threads                          # 运行时生效
sudo cat /proc/fs/nfsd/threads                                          # 应为 32
# 持久化到 /etc/nfs.conf(幂等)。必须用 nfsconf 写入,它会把键放进正确的 [nfsd] 段;
# 不要用 tee -a 追加:该文件末尾是 [svcgssd] 段,追加会写错段,导致重启后不生效
sudo nfsconf --set nfsd threads 32
nfsconf --get nfsd threads                                              # 解析确认:应输出 32
# 预期输出:32
# 不符时:检查是否误写入其它段——用 `nfsconf --dump -` 查看 [nfsd] 段内容,修正后用同样命令重设
sudo systemctl enable --now nfs-server                                  # enable 保证开机自启(不重启运行中的服务)

# ---- 步骤 6:本机自测(确认导出可用再动集群)----
sudo mkdir -p /mnt/nfs-probe
sudo mount -t nfs -o vers=4.2,nconnect=4,hard,timeo=600,retrans=3 192.168.31.249:/srv/nfs/k8s /mnt/nfs-probe
head -c 1048576 /dev/urandom > /tmp/probe.bin && sudo cp /tmp/probe.bin /mnt/nfs-probe/probe.bin
sudo md5sum /mnt/nfs-probe/probe.bin /tmp/probe.bin                     # 两个 md5 必须一致
sudo rm -f /mnt/nfs-probe/probe.bin && sudo umount /mnt/nfs-probe && sudo rmdir /mnt/nfs-probe

5.2 集群侧

# ---- 步骤 7:安装 helm(宿主机,已装则跳过)----
command -v helm >/dev/null || { curl -fsSL -o /tmp/helm.tgz https://get.helm.sh/helm-v3.16.3-linux-amd64.tar.gz && \
  tar -xzf /tmp/helm.tgz linux-amd64/helm && sudo install -m 0755 linux-amd64/helm /usr/local/bin/helm; }

# ---- 步骤 8:固定 chart 版本到本地(便于离线重建与版本锁定)----
cd nfs-storage
helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
helm pull csi-driver-nfs/csi-driver-nfs --version 4.13.4 --untar --untardir charts

# ---- 步骤 9:先装 snapshot CRD(顺序不可颠倒,否则 Helm 无法解析 VolumeSnapshotClass)----
helm template nfs-csi charts/csi-driver-nfs -n kube-system -f values-nfs-csi.yaml \
  --show-only templates/crd-csi-snapshot.yaml | kubectl apply --server-side=true -f -

# ---- 步骤 10:挂载参数 Secret(DeleteVolume / DeleteSnapshot 需要,且须与 SC 参数一致)----
kubectl apply -f manifests/00-mount-options-secret.yaml

# ---- 步骤 11:部署驱动与两个 StorageClass(高危:部署特权 DaemonSet)----
helm upgrade --install nfs-csi charts/csi-driver-nfs -n kube-system \
  -f values-nfs-csi.yaml --wait --timeout 6m
# 预期输出:末行 STATUS: deployed(首次为 Installing it now 后接 deployed)
kubectl -n kube-system get pods | grep -E 'csi-nfs|snapshot'
# 预期:csi-nfs-controller-* 5/5、csi-nfs-node-*(4 个,含 master-01)各 3/3、snapshot-controller-* 1/1
# 不符时:ImagePullBackOff → 检查节点到 registry.k8s.io 的连通性;
#         controller CrashLoopBackOff → 确认 controller.replicas 为 1(附录 A.2),或看日志端口冲突

# ---- 步骤 12:快照归档分流到 HDD ----
# chart 模板不支持透传 VolumeSnapshotClass 的 parameters,只能以对象方式维护。
# 重要:每次 helm upgrade 之后都必须重跑本步(原因见 R9)。
kubectl apply -f manifests/02-volumesnapshotclass.yaml
kubectl get volumesnapshotclass csi-nfs-snapclass -o jsonpath='{.parameters}{"\n"}'   # 应输出 server 与 share

# ---- 步骤 13:核对两个 SC 与默认值 ----
kubectl get sc
# 预期输出:
#   nfs-csi (default)   nfs.csi.k8s.io   Delete   Immediate   true
#   nfs-csi-hdd         nfs.csi.k8s.io   Delete   Immediate   true
# 不符时:只有 nfs-csi → 核对 values 中 storageClasses 列表是否被删;
#         两个都带 (default) → 立即去掉其中一个的默认注解,否则省略 storageClassName 的 PVC 会绑定失败

5.3 变更挂载参数时的追加操作(已有卷生效路径)

# 参数固化在 PV 对象上,改 SC 不会自动作用到已有 PV,必须逐个 patch 并重新挂载
kubectl get pv -o custom-columns='NAME:.metadata.name,SC:.spec.storageClassName,MOUNT:.spec.mountOptions'   # 找出未达标的 PV
# <pv名> 取自上一行的 NAME 列
kubectl patch pv <pv名> --type=merge -p \
  '{"spec":{"mountOptions":["nfsvers=4.2","nconnect=4","hard","timeo=600","retrans=3","noatime"]}}'
# 高危:重启使用方以重新挂载(nconnect 仅在重新 stage 时生效),逐个滚动、不要同时
# <命名空间>/<StatefulSet名> 由该 PV 反查:
#   kubectl get pv <pv名> -o jsonpath='{.spec.claimRef.namespace}{" "}{.spec.claimRef.name}'
kubectl -n <命名空间> rollout restart statefulset/<StatefulSet名>
# 复核:<节点IP> 取自 kubectl get node <节点名> -o jsonpath='{.status.addresses[0].address}'
ssh root@<节点IP> 'findmnt -t nfs4 -o TARGET,OPTIONS'      # 应含 vers=4.2 与 nconnect=4

6. 验证

编号 命令 预期结果 实测
V1 sudo exportfs -v 本方案三处导出均为 async,且 Slurm 导出仍在列 通过(四处导出并存)
V2 sudo cat /proc/fs/nfsd/threads、nfsconf --get nfsd threads 均为 32 通过(运行态 32,配置解析 32)
V3 findmnt -no OPTIONS /srv/nfs/k8s /srv/nfs/k8s-hdd、sudo blockdev --getsize64 <loop> 含 noatime,prjquota;上限 128849018880 / 536870912000 字节 通过(rw,noatime,prjquota)
V4 kubectl get sc nfs-csi(default) 与 nfs-csi-hdd,PROVISIONER 为 nfs.csi.k8s.io 通过
V5 kubectl get sc -o jsonpath、kubectl -n kube-system get secret nfs-mount-options -o jsonpath 两者参数完全一致 通过
V6 kubectl get pv 11 个 NFS PV 全部为 nfsvers=4.2 nconnect=4 … 通过
V7 打快照后 sudo ls -l /srv/nfs/k8s 与 sudo find /data/nfs-k8s-snap -maxdepth 2 SSD 侧不得出现 snapshot-*;HDD 侧出现 snapshot-<uid>/<源卷 uuid>.tar.gz 通过(HDD 侧归档 5244782 字节;删除快照后同步回收)
V8 kubectl apply -f examples/01-pvc.yaml → kubectl get pvc demo-data Bound,ACCESS MODES=RWX 通过
V9 kubectl apply -f examples/02-cross-node-pods.yaml(或 04 用于 HDD),跨节点写读 内容与 md5 一致 通过:node-01 写 → node-02 读一致;HDD 版 volumeHandle 含 /srv/nfs/k8s-hdd
V10 删除 PVC 后 ls /srv/nfs/k8s 子目录被回收,仅剩 lost+found 通过
V11 sudo reboot 宿主机(择机执行) 重启后 findmnt 两个后端已挂载且含 noatime、四处导出恢复、nfsd threads=32、Pod 自动重挂 未执行(会中断全部业务与 Slurm,需约定窗口)
V12 复跑第 5 章宿主机检查项 + helm upgrade --install 宿主机各项全 SKIP;helm 升级后 Pod 创建时间与重启计数不变 通过(宿主机检查项全 SKIP;复跑后 Pod creationTimestamp 未变)

性能基线(nfs-mount-tuning 于 2026-09-19 实测,节点侧 /proc/self/mountstats per-op 平均 RTT)

节点/后端 操作 调优前 调优后
node-02 / HDD(prometheus) OPEN 67.60 ms 0.25 ms
node-02 / HDD(prometheus) SETATTR 79.76 ms 0.23 ms
node-02 / SSD(alertmanager) SETATTR 3.22 ms 0.38 ms
node-01 / SSD(mariadb) SETATTR 3.05 ms 0.19 ms
node-03 / HDD(openldap) SETATTR 75.47 ms 0.23 ms

收益主要来自服务端 async 消除了机械盘的 fsync 等待(宿主本地回环路径的 fio 指标几乎未变)。

已知且可接受的现象

  • 首次部署时 csi-provisioner、csi-resizer、node-driver-registrar 各有 1 次 Error(exit 1) 重启, 属 sidecar 与主容器的启动竞态,重启后计数不再增长,无需干预。

组件状态基线

kubectl -n kube-system get pods | grep -E 'csi-nfs|snapshot'
# csi-nfs-controller-*      5/5 Running
# csi-nfs-node-*            4 个(含 master-01)3/3 Running
# snapshot-controller-*     1/1 Running

7. 回滚

7.1 备份清单

备份文件的全量台账(位置、产生时间、因何产生、能回退什么、何时可删)见 nfs-storage/backup/README.md; 本节只列与本次回滚直接相关的项。服务端配置备份因需就近恢复、且部分被 nfs-mount-tuning 的回滚逻辑依赖, 按规范例外保持在原位(已在台账中登记)。

对象 备份方式 位置 / 现状
/etc/exports 步骤 0 备份;历次变更另有时间戳备份 /etc/exports.bak.20260917-pre-snapshot-share、…-pre-hdd-share、/etc/exports.bak.20260918_124543、/etc/exports.bak.20260919-133357
/etc/nfs.conf 同上 /etc/nfs.conf.bak.20260919-133357
/etc/fstab 同上 /etc/fstab.bak.20260917、…-pre-hdd-share、/etc/fstab.bak.20260919-133357
集群配置 values 与清单留档 nfs-storage/values-nfs-csi.yaml、nfs-storage/manifests/(同目录另有调优工程产生的 .bak.*,不要删除,回滚依赖该命名)
数据 SSD 镜像 /kvm/nfs/k8s-120g.img、HDD 镜像 /data/nfs/k8s-hdd-500g.img 删除镜像文件即数据全失,回滚默认保留

7.2 回滚步骤

原则:只回滚本方案的三处导出与集群对象,不触碰 /data/nfs/slurm-state,不使用全局撤销或停服动作。

# ---- 集群侧(先做,避免残留 PV 指向已失效后端)----
kubectl get pvc -A                                   # 确认使用两个 NFS SC 的业务已迁移或可接受中断
helm uninstall nfs-csi -n kube-system                # 移除 driver / controller / 两个 SC / VolumeSnapshotClass
kubectl delete -f manifests/00-mount-options-secret.yaml
kubectl delete -f manifests/01-snapshot-crds.yaml    # 高危:会连带删除集群内全部快照对象,有快照数据时跳过本行

# ---- 宿主机侧:按导出粒度撤销(不要用 exportfs -ua,会连带撤销 Slurm 导出)----
sudo exportfs -u 192.168.31.0/24:/srv/nfs/k8s        # 逐个导出撤销,其余导出的客户端不受影响
sudo exportfs -u 192.168.31.0/24:/srv/nfs/k8s-hdd
sudo exportfs -u 192.168.31.0/24:/data/nfs-k8s-snap
sudo exportfs -v                                     # 确认仅本方案三处消失,Slurm 导出仍在

# ---- 宿主机侧:卸载后端并清理持久化配置 ----
sudo umount /srv/nfs/k8s && sudo umount /srv/nfs/k8s-hdd
sudo sed -i.bak '/kvm\/nfs\/k8s-120g.img/d; /data\/nfs\/k8s-hdd-500g.img/d' /etc/fstab   # 仅删本方案两行(自动留 .bak)
sudo nfsconf --unset nfsd threads                    # nfsd 线程数恢复系统默认(不改动其他段)
sudo systemctl daemon-reload
# 快照目录:先校验路径存在且为空,再移除(不使用通配删除)
[ -d /data/nfs-k8s-snap ] && [ -z "$(ls -A /data/nfs-k8s-snap)" ] && sudo rmdir /data/nfs-k8s-snap
# 镜像文件默认保留;确认不再需要时再执行(高危:数据不可恢复)
# sudo rm -f /kvm/nfs/k8s-120g.img /data/nfs/k8s-hdd-500g.img

# 注意:不要执行 systemctl stop/disable nfs-server —— Slurm 导出依赖同一服务;
#       仅当确认 Slurm 已退役时才可停服。

7.3 回滚后的验证

  1. kubectl get sc 不再出现 nfs-csi / nfs-csi-hdd;kubectl -n kube-system get pods | grep csi-nfs 无输出。
  2. sudo exportfs -v 仅剩 /data/nfs/slurm-state(Slurm 不受影响);findmnt /srv/nfs/k8s* 无输出。
  3. nfsconf --get nfsd threads 回到系统默认(8),/proc/fs/nfsd/threads 相应回落。
  4. df -h / 与 df -h /data 反映镜像释放情况(默认保留镜像时占用不减)。
  5. Slurm 状态目录可正常读写,kubectl get no 全 Ready,既有 VM 无异常。

7.4 回滚触发判据(量化,不临场判断)

任一满足即回滚:

  1. PVC 创建后 60 秒内未 Bound;
  2. Pod 跨节点挂载失败,或 node 插件日志出现 mount 报错 / Pod 报 Input/output error;
  3. 改造后 per-op 平均 RTT 相对同后端基线劣化超过 20%;
  4. 宿主 / 使用率超过 90%,或 /data 使用率超过 90%;
  5. 本方案变更后 Slurm 侧出现 NFS 访问异常(此时优先按 exportfs 粒度回退本次改动,而非停服)。

8. 风险说明

# 风险 影响 缓解措施
R1 存储与计算同宿主,宿主故障时存储与 4 台 VM 同时中断,重启顺序不可控 依赖 PV 的 Pod 全部挂载失败直至 NFS 恢复 驱动自动重试 + hard 挂载自动恢复;如要真冗余须把 NFS 服务端迁到独立主机
R2 async 导出丢失持久化语义:客户端 COMMIT 被直接确认而不落盘 宿主非正常掉电/内核崩溃时可能丢最近写入;承载数据库的导出有损坏可能 已按用户指令对三处导出统一 async;变更前须停写 mariadb/minio;如需更强保证,把数据库所在导出单独改回 sync(按导出粒度可分域)
R3 120GiB 镜像位于系统盘(sda3),按需增长;当前 / 已用约 49G 与宿主自身增长、3 个 etcd raw(各 10G)争空间,写满会波及 VM 与 libvirt 加 / 使用率 >80% 告警;镜像写满由 loop 上限(120GiB)兜底
R4 容量不被强制:NFS 后端无 CSI 层配额,PVC 请求值仅记账 应用可能写到远超请求值,且多个卷共享同一后端容量 已启用 prjquota,必要时用 setquota -P 对子目录手工限额;容量规划不要依赖 PVC 请求值
R5 HDD 卷与 VM 的 qcow2 共用同一块物理盘,且后端叠加 loop 层 HDD 卷的高 IO 与 VM(含 k8s 节点自身)、Slurm 状态目录争抢磁盘 定位为容量优先、IO 不敏感场景;用 iostat -x 1 观察 %util/await;不要在 HDD SC 上跑数据库类小随机 IO
R6 快照是 tar 归档(默认 .tar.gz)非 COW;恢复耗时与数据量成正比 归档占用 HDD 空间;RESTORESIZE 是归档体积而非逻辑容量 已分流至 /data/nfs-k8s-snap;大数据量可用 --set controller.enableSnapshotCompression=false(.tar 更快更占空间)
R7 hard 挂载:服务端不可用时 Pod I/O 阻塞而非报错 Pod 表现为挂起,可能触发探针失败 保数据一致性的取舍;排查时先确认 NFS 可达,不建议改 soft
R8 no_root_squash 导出:192.168.31.0/24 内任意主机以 root 可读写全部数据 内网横向风险;并影响本方案与 Slurm 的全部数据 收紧网段,或改 all_squash,anonuid/anongid + Pod runAsUser/runAsGroup;本环境为隔离测试网段
R9 helm upgrade 会重置 VolumeSnapshotClass:Helm 的 three-way merge 会删除模板中不存在的 parameters 升级驱动后快照静默回落到源卷 share(SSD),可能撑满才发现 每次 helm upgrade 后重跑 kubectl apply -f manifests/02-volumesnapshotclass.yaml,并按 V7 复验
R10 快照 CRD 由 kubectl 独立管理(customResourceDefinitions.enabled=false) 升级 chart 不会同步 CRD 定义 刻意设计:避免 helm uninstall 连带删除 CRD 与全部快照对象;升级驱动时人工核对 CRD 版本
R11 mountOptions 变更不自动作用于已有 PV:参数固化在 PV 上,nconnect 仅在重新 stage 时生效 只看 SC 会误以为已生效,旧卷仍是旧参数 逐个 patch PV 并重启使用方(第 5.3 节),节点侧 findmnt -t nfs4 复核实际选项
R12 启动竞态导致 sidecar 重启 1 次 首次部署短暂不可用 观察计数不再增长即可忽略;持续增长需查 node 插件日志
R13 master-01 也运行 node 插件(DaemonSet 容忍全部污点) 控制面节点增加约 3 个常驻容器 资源请求很小(10m CPU/20Mi 起);如需排除,加 nodeSelector 或改 tolerations
R14 与 Slurm 共享服务端全局资源(nfs-server 服务、/etc/exports、[nfsd] threads) 本方案的全局动作(停服、全局撤销导出、调低线程)会同时中断或拖慢 Slurm 回滚只按导出粒度操作(7.2);改 [nfsd] threads 前后评估两侧负载;停服前确认 Slurm 状态
R15 当前 /etc/nfs.conf 中 threads = 32 位于 [nfsd] 段尾行、紧邻 [statd] 之前(形态易被误读);且该文件末尾是 [svcgssd] 段,用 tee -a 追加会写进错误段 人工整理或追加写入时容易把键放进错误段,导致重启后线程数回落、nconnect 并发优势丧失 一律用 sudo nfsconf --set nfsd threads 32 维护、nfsconf --get nfsd threads 解析复核(不能只看文件内容);已加入 V2
R16 ldap 与 server 两个命名空间同时存在同名的 openldap PVC 疑似迁移残留,误删会损数据 清理前先核实两个命名空间的实际使用方,确认无引用后再删 PVC

附录 A:核心配置项说明与调整方法

客户端与集群侧配置集中在 nfs-storage/values-nfs-csi.yaml,改完执行 helm upgrade --install nfs-csi charts/csi-driver-nfs -n kube-system -f values-nfs-csi.yaml --wait 即可生效(注意:升级后必须重跑快照分流那一步,见 R9)。 宿主机侧配置为 /etc/exports、/etc/nfs.conf、/etc/fstab,改后按 5.1 的重载命令生效。

A.1 驱动与权限

配置项 当前值 作用 怎么改 / 改了会怎样
driver.mountPermissions 0 挂载后是否对挂载根目录 chmod;0 = 不动 保持 0。设为 0777 会让目录对所有 Pod 可写且不解决跨 GID 隔离,官方列为最后手段
feature.enableFSGroupPolicy true 允许 kubelet 应用 Pod 的 fsGroup 保持 true;前提是导出带 no_root_squash,否则表现为挂载失败而非写入失败
kubeletDir /var/lib/kubelet node 插件据此挂载 pods-mount-dir 仅当 kubelet 数据目录非默认时修改

A.2 controller(供给与快照编排)

配置项 当前值 作用 怎么改 / 改了会怎样
controller.replicas 1 供给器副本数 不要加。controller 用 hostNetwork 且 liveness-probe 占固定宿主端口 29652,同节点多副本会端口冲突 → CrashLoopBackOff
controller.enableSnapshotter true 启用 csi-snapshotter sidecar 不需要快照可设 false
controller.enableSnapshotCompression true 快照以 .tar.gz 存储 数据已压缩或体量大时设 false(.tar 更快、更占空间)
controller.defaultOnDeletePolicy delete PVC 删除时后端子目录处置 改 retain 可防误删,代价是空间不回收
controller.runOnControlPlane false controller 是否调度到控制面 控制面资源紧张时保持 false

A.3 StorageClass

配置项 当前值 作用 怎么改 / 改了会怎样
storageClass.name / …is-default-class nfs-csi / "true" 默认 SC(SSD 后端) 有多个 SC 时只应一个为默认,否则省略 storageClassName 的 PVC 会绑定失败
parameters.server 192.168.31.249 NFS 服务端地址 换成域名需保证集群 DNS 可解析
parameters.share /srv/nfs/k8s 后端导出路径 必须与 /etc/exports 中的路径一致
parameters.subDir 未设置 子目录命名规则,支持 ${pvc.metadata.name} 等变量 默认由驱动生成 pvc-<uuid>;改为可读名会让同名 PVC 重建时复用旧目录
parameters.uid / gid 未设置 CreateVolume 时对新建子目录 chown fsGroup 会覆盖 gid,两者不要设成不同值
mountOptions nfsvers=4.2, nconnect=4, hard, timeo=600, retrans=3, noatime 客户端挂载参数,必须与 Secret 及已有 PV 三处一致 改动只对新建 PV 生效;已有 PV 需按 5.3 逐个 patch 并重挂
reclaimPolicy Delete 删 PVC 时是否删 PV 与后端目录 改 Retain 后新建 PV 才生效
volumeBindingMode Immediate 绑定时机 NFS 无拓扑约束,保持 Immediate
allowVolumeExpansion 模板固定 true 是否允许改 PVC 请求容量 仅影响能否改请求值,实际容量不受限(见 R4)

附加 StorageClass(HDD 版):由 storageClasses(复数列表)定义,与单数的 storageClass 并存。

配置项 当前值 作用 怎么改 / 改了会怎样
storageClasses[].name nfs-csi-hdd SC 名称 改名后既有 PVC 仍绑旧 SC,需重建 PVC
storageClasses[].parameters.share /srv/nfs/k8s-hdd 指向 HDD 的 500GiB 镜像 换路径须先在宿主机建目录与导出,再 helm upgrade
storageClasses[].mountOptions 同 SSD 版 客户端挂载参数 两者不同则需各自配套 provisioner-secret(当前共用 nfs-mount-options)
默认注解 未设置 保持默认仍是 SSD 版 不要给两个 SC 同时打默认注解

A.4 快照

配置项 当前值 作用 怎么改
externalSnapshotter.enabled true 部署快照控制器(集群全局一份) 若已有其它实现,设 false 避免重复
externalSnapshotter.customResourceDefinitions.enabled false 是否由 Helm 创建 CRD 保持 false(见 R10);CRD 用 kubectl apply -f manifests/01-snapshot-crds.yaml
volumeSnapshotClass.name / deletionPolicy csi-nfs-snapclass / Delete 快照类与删除策略 需保留快照时改 Retain
VolumeSnapshotClass.parameters.server / share 192.168.31.249 / /data/nfs-k8s-snap 快照归档落盘位置;不填则继承源卷 share(落回 SSD) 只能改对象、不能改 values(见 R9):编辑 manifests/02-volumesnapshotclass.yaml 后 kubectl apply -f

归档路径规则(源码 pkg/nfs/controllerserver.go v4.13.4): <share>/<snapshot 名>/<源卷 uuid>.tar.gz;据此可在宿主机上直接定位某份快照的归档文件。 分流后:卷数据在 /srv/nfs/k8s*/pvc-*/,快照归档在 /data/nfs-k8s-snap/snapshot-*/; 从快照恢复时,新数据仍按目标 StorageClass 的 share 落回对应后端。

A.5 服务端关键参数

参数 当前值 作用 怎么改 / 注意
导出选项 rw,async,no_subtree_check,no_root_squash async 换取元数据延迟,代价见 R2 按导出粒度可分域;改后 exportfs -ra
[nfsd] threads 32(原 8) 服务端并发 RPC 线程,配合 nconnect=4 热调整运行时:printf '32\n' \| sudo tee /proc/fs/nfsd/threads;持久化:sudo nfsconf --set nfsd threads 32;复核:nfsconf --get nfsd threads(见 R15)
loop 挂载选项 noatime,loop,prjquota,nofail 抑制 atime 写、启用项目配额、镜像异常不阻塞启动 改后 mount -o remount 或更新 fstab
保留块 1%(tune2fs -m 1) 原始默认 5% 会浪费约 4.7G/25G 已设置;新增镜像同样设置

A.6 日常运维速查

日常运维命令(服务状态、日志、容量查看、驱动升级、故障场景处置)统一维护在 nfs-storage/RUNBOOK.md 的第 3 章「日常速查表」与第 4 章「日常操作」,本文不重复。

此处只保留与配置调整直接相关的一致性核对——改完参数后,三处必须一致:

kubectl get sc -o jsonpath='{range .items[*]}{.metadata.name}{"  "}{.mountOptions}{"\n"}{end}'      # ① SC 侧
kubectl -n kube-system get secret nfs-mount-options -o jsonpath='{.data.mountOptions}' | base64 -d   # ② Secret 侧
kubectl get pv -o custom-columns='NAME:.metadata.name,MOUNT:.spec.mountOptions' | grep nfs-csi        # ③ 已有 PV 侧
# 预期:三处参数完全相同(当前为 nfsvers=4.2,nconnect=4,hard,timeo=600,retrans=3,noatime)
# 不符时:① 与 ② 不一致 → 改 values 与 Secret 后重新 helm upgrade;
#         ③ 落后 → 按第 5.3 节逐个 patch PV 并重启使用方

附录 B:交付物清单

路径(相对项目根) 说明
nfs-storage/SOP.md 本文件(变更实施、验证与回滚)
nfs-storage/RUNBOOK.md 服务运行手册:日常速查、故障场景处置、监控、容量扩缩(值班用)
nfs-storage/backup/README.md 备份台账:全部备份文件的位置、用途与可删条件
nfs-storage/values-nfs-csi.yaml 核心实现体:客户端参数、两个 StorageClass、快照控制器配置
nfs-storage/manifests/00-mount-options-secret.yaml DeleteVolume / DeleteSnapshot 所需的挂载参数(须与 SC 一致)
nfs-storage/manifests/01-snapshot-crds.yaml 快照 CRD 固化清单(独立于 Helm 管理)
nfs-storage/manifests/02-volumesnapshotclass.yaml 快照落盘分流配置(指向 HDD),每次 helm 升级后需重新 apply
nfs-storage/charts/csi-driver-nfs/ 本地固化的 chart 4.13.4
nfs-storage/examples/01-pvc.yaml PVC 动态供给验证(SSD 默认 SC)
nfs-storage/examples/02-cross-node-pods.yaml 跨节点 RWX 读写验证
nfs-storage/examples/03-snapshot-and-restore.yaml 快照与恢复验证
nfs-storage/examples/04-pvc-hdd.yaml HDD 存储类跨节点读写验证
nfs-storage/backup/README.md 备份台账说明(解释同目录 .bak.* 为何保留原位)
nfs-mount-tuning/docs/SOP.md 挂载参数调优的独立变更记录(本文档当前参数的来源)

范围外但同服务端共存:/data/nfs/slurm-state(Slurm 集群状态导出),由 slurm 相关部署管理, 不属本方案的交付、验证与回滚范围;本方案任何全局动作前须评估其影响(见 R14)。


来源:/home/rain/Documents/k8s_ins/nfs-storage/SOP.md | 最后更新:2026-09-19 16:29(由 build/sync.py 同步,请勿在站点侧修改)