跳转至

NFS 挂载参数优化(元数据延迟优先)— 变更 SOP

交付目录:nfs-mount-tuning/ 变更日期:2026-09-19 变更类型:C 类生产变更(服务端导出参数 + 客户端挂载参数 + 已有卷重新挂载) 实现体:nfs-mount-tuning/ 的 Python 部署工程(main.py + modules/ + config/tune.yaml) 本文第 5 章的 shell 代码块用于阅读、复核与手工复现;幂等、备份与回滚由实现体保证,以实现体为准

目录


1. 背景

集群 k8s_ins 的 NFS 存储由 nfs-storage/SOP.md 于 2026-09-17 交付,当时客户端参数为 nfsvers=4.1, hard, timeo=600, retrans=3, noatime,服务端三处导出为 rw,sync,no_subtree_check,no_root_squash。

  • 触发条件:用户要求以「元数据延迟优先」为目标优化 NFS 挂载参数,并允许全层联动调整 (客户端参数、协议版本、服务端导出),且确认现有业务数据可删除重建。
  • 实测暴露的瓶颈:HDD 后端(/srv/nfs/k8s-hdd)在节点侧的 OPEN / SETATTR 平均 RTT 高达 60–80ms,而 SSD 后端仅 1–3ms。根因是 sync 导出下每次元数据写都要等机械盘落盘, 且该盘同时承载 4 台 k8s VM 的 qcow2(与 nfs-storage/SOP.md R13 记录一致)。
  • 不涉及的资源:GitLab 本体的 gitaly / postgres / redis 走 local-ssd,与本次变更无关。

2. 目标

编号 目标状态 验证方式
G1 三处导出由 sync 改为 async,其余选项不变 第 6 章 V1
G2 nfsd 线程数 8 → 32,且重启后仍生效 第 6 章 V2
G3 两个 loop 镜像挂载追加 noatime 第 6 章 V3
G4 客户端参数为 nfsvers=4.2, nconnect=4, hard, timeo=600, retrans=3, noatime 第 6 章 V4、V5
G5 三处参数一致(values / Secret / StorageClass),已有 PV 全部重新挂载生效 第 6 章 V6、V7
G6 元数据延迟显著下降并有留档 第 6 章 V11
G7 全部操作幂等:第二次执行零变更、全跳过 第 6 章 V10

3. 前置条件

权限与凭据

项 要求 取值来源
宿主机 sudo 需要覆盖 /etc/exports、/etc/nfs.conf、/etc/fstab、/proc/fs/nfsd/threads、loop remount 的免密提权 已验证 sudo -n true 可用
集群 cluster-admin kubeconfig:/home/rain/.kube/config,上下文 kubernetes-admin@kubernetes
NFS 服务端 无独立凭据,导出使用 sec=sys(AUTH_SYS,按客户端 IP 网段授权) /etc/exports

工具与依赖

依赖 实际路径 版本
helm /usr/local/bin/helm v3.16.3
kubectl /usr/local/bin/kubectl v1.36.4
fio /usr/bin/fio 3.36
python3 系统 python3 3.12.3(工程内 .venv 由它创建)
csi-driver-nfs chart nfs-storage/charts/csi-driver-nfs/(本地固化) 4.13.4

执行前检查项

  1. 服务端 nfs-server 处于 active 且三处导出存在。
  2. /data 上有运行中的 VM qcow2,禁止对 /data 本体做任何 umount 操作。
  3. 确认写敏感业务可停:本次为 server/mariadb(数据库)、server/minio。
  4. 确认无其他人同时做存储相关变更(本次执行期间环境曾被外部改动,见第 8 章 R3)。
  5. 工程可正常加载配置与预演:
cd /home/rain/Documents/k8s_ins/nfs-mount-tuning
.venv/bin/python main.py --config config/tune.yaml --dry-run
# 预期:四个阶段均输出「[dry-run] 将执行变更: …」,最后一行「执行结束 | 失败阶段: 无」

4. 影响范围

主机与组件

对象 角色 受影响内容
dell-R620(192.168.31.249) NFS 服务端 覆盖 /etc/exports、/etc/nfs.conf、/etc/fstab;两个 loop 挂载 remount;nfsd 线程数热调整
k8s-node-01/02/03、k8s-master-01 CSI node 客户端 所有 nfs4 挂载点重新挂载(连接级参数必须重新挂载才生效)
kube-system CSI 驱动 helm upgrade 滚动重建 controller 与 node 插件
server / monitoring 命名空间 业务负载 逐个重启(非同时),期间服务短暂中断

是否停服:需要。

  • server/mariadb、server/minio 在服务端切 async 之前主动停写(副本置 0),改造完成后再拉起;
  • 其余 NFS 使用者(prometheus、grafana、alertmanager、openldap)逐个重启以重新挂载。

窗口期:无强制窗口,建议低峰执行;/data 全程不动,运行中的 VM 不受影响。本次分步执行实测约 6 分钟(不含基线压测)。

变更前在跑的 NFS 使用者(实机核查结果)

命名空间 卷 StorageClass 使用方
server minio-data 50Gi、storage-mariadb-0 20Gi nfs-csi minio、mariadb(数据库)
server conf/data/backups-openldap-0 nfs-csi-hdd openldap
monitoring kube-prometheus-stack-grafana 10Gi、alertmanager-…-db 10Gi nfs-csi Grafana、Alertmanager
monitoring prometheus-…-db 100Gi nfs-csi-hdd Prometheus

变更期间 ldap 命名空间的 openldap 被外部迁移到 server 命名空间(详见第 8 章 R3); 日常查看这些负载的方法见 docs/RUNBOOK.md。

5. 分步操作

对应实现体:nfs-mount-tuning/main.py 与 modules/*.py;期望状态集中在 config/tune.yaml。 每步的真实动作、备份与校验均由实现体执行,下面的命令序列与其等价,供手工复现与复核。

5.1 预演(不改任何系统状态)

cd /home/rain/Documents/k8s_ins/nfs-mount-tuning
.venv/bin/python main.py --config config/tune.yaml --dry-run   # 预期:各阶段列出「将执行变更」,无实际写入

5.2 采集元数据延迟基线

# 高危:会在导出根目录下创建临时测试目录 .meta-bench 并执行元数据压测
# 测完由实现体自动清理;清理前对路径做白名单校验(仅允许挂载点的 .meta-bench* 直接子目录)
.venv/bin/python main.py --config config/tune.yaml --stages measure-before
echo $?                                                        # 预期:0
ls -t logs/measure-before-*.json | head -1                     # 预期:输出一份报告路径

5.3 静默写敏感业务

# 高危:停服。mariadb 走 CR 的 spec.replicas,minio 走 StatefulSet 副本数;改完必须恢复
kubectl -n server patch mariadb mariadb --type=merge -p '{"spec":{"replicas":0}}'   # 预期:mariadb.k8s.mariadb.com/mariadb patched
kubectl -n server scale statefulset minio --replicas=0                              # 预期:statefulset.apps/minio scaled
.venv/bin/python main.py --config config/tune.yaml --stages quiesce                 # 预期:验证通过: server/mariadb 已停止、server/minio 已停止

实现体会自行判断「服务端是否已是 async」:若已生效则整体跳过,不会重复停业务。

5.4 服务端调优

# 高危:覆盖系统配置。实现体在改动前自动备份(备份位置见第 7 章与 backup/README.md)
sudo cp -a /etc/exports  "/etc/exports.bak.$(date +%Y%m%d-%H%M%S)"    # 手工复现时的备份动作
sudo cp -a /etc/nfs.conf "/etc/nfs.conf.bak.$(date +%Y%m%d-%H%M%S)"
sudo cp -a /etc/fstab    "/etc/fstab.bak.$(date +%Y%m%d-%H%M%S)"

# 5.4.1 三处导出 sync -> async(其余选项保持 rw,no_subtree_check,no_root_squash)
#        目标文件:/etc/exports 中的 /srv/nfs/k8s、/srv/nfs/k8s-hdd、/data/nfs-k8s-snap
sudo exportfs -ra                                                     # 重载导出(幂等)
sudo exportfs -v                                                      # 预期:三处均含 async,且不含 sync

# 5.4.2 nfsd 线程 8 -> 32(热调整,写 /proc 即可生效,无需重启服务)
printf '32\n' | sudo tee /proc/fs/nfsd/threads                        # 预期:回显 32
sudo cat /proc/fs/nfsd/threads                                        # 预期:32(若为 8,见第 6 章 V2 的处置)
# 持久化到 /etc/nfs.conf 的 [nfsd] 段(threads = 32),保证重启后仍生效

# 5.4.3 loop 镜像挂载追加 noatime(remount 不卸载,不影响在用文件)
sudo mount -o remount,noatime /srv/nfs/k8s                            # 预期:无输出
sudo mount -o remount,noatime /srv/nfs/k8s-hdd                        # 预期:无输出
findmnt -no OPTIONS --target /srv/nfs/k8s                             # 预期:rw,noatime,prjquota

# 交由实现体统一执行(含状态巡检、备份、写入、热生效与验证)
.venv/bin/python main.py --config config/tune.yaml --stages server
# 预期:验证通过: exportfs -v 中三处导出已为 async / 验证通过: nfsd 线程数 = 32 / 验证通过: 两个挂载点选项含 noatime

5.5 集群侧参数同步(三处必须一致)

# 5.5.1 实现体更新两处文件:values 的两个 mountOptions 块、Secret 清单的 stringData
# 5.5.2 应用 Secret -> helm upgrade -> 重新 apply 快照类(防止 helm 重置 parameters)
kubectl apply -f /home/rain/Documents/k8s_ins/nfs-storage/manifests/00-mount-options-secret.yaml
helm upgrade --install nfs-csi /home/rain/Documents/k8s_ins/nfs-storage/charts/csi-driver-nfs \
  -n kube-system -f /home/rain/Documents/k8s_ins/nfs-storage/values-nfs-csi.yaml --wait --timeout 6m
kubectl apply -f /home/rain/Documents/k8s_ins/nfs-storage/manifests/02-volumesnapshotclass.yaml

.venv/bin/python main.py --config config/tune.yaml --stages cluster
# 预期:验证通过: StorageClass nfs-csi / nfs-csi-hdd mountOptions=[…4.2, nconnect=4…];验证通过: Secret mountOptions=…

5.6 已有 PV 生效(patch 参数 + 重新挂载)

# 高危:patch 后需重启使用方,服务短暂中断;nconnect 只在重新挂载(重新 stage)时才生效
kubectl get pv -o custom-columns='NAME:.metadata.name,OPTS:.spec.mountOptions' | grep nfs-vers   # 先看现状
kubectl patch pv <pv-name> --type=merge \
  -p '{"spec":{"mountOptions":["nfsvers=4.2","nconnect=4","hard","timeo=600","retrans=3","noatime"]}}'

kubectl -n server scale statefulset minio --replicas=1                # 恢复被静默的对象
kubectl -n monitoring rollout restart statefulset/prometheus-kube-prometheus-stack-prometheus

.venv/bin/python main.py --config config/tune.yaml --stages volume
# 预期:逐个 PV 输出「验证通过: PV … mountOptions=[…]」,并在节点侧复核实际挂载参数

实现体会按「挂载路径中的 Pod UID」反查控制器,自动重启所有实际挂载参数未达标的工作负载, 不依赖静态清单,因此清单外的新增业务也会被覆盖。

5.7 复测与幂等复核

.venv/bin/python main.py --config config/tune.yaml --stages measure-after      # 预期:生成 logs/measure-after-*.json
.venv/bin/python main.py --config config/tune.yaml --skip-measure              # 预期:四阶段全部 skipped,零变更

5.8 功能回归

kubectl apply -f /home/rain/Documents/k8s_ins/nfs-storage/examples/01-pvc.yaml
kubectl apply -f /home/rain/Documents/k8s_ins/nfs-storage/examples/02-cross-node-pods.yaml
kubectl exec pvc-writer -- sh -c 'echo hello > /data/hello.txt'       # 预期:无输出
kubectl exec pvc-reader -- cat /data/hello.txt                        # 预期:hello
kubectl delete pod pvc-writer pvc-reader --ignore-not-found=true
kubectl delete pvc demo-data

6. 验证

编号 命令 预期结果 本次实测
V1 sudo exportfs -v 三处导出均含 async,wdelay,…,不含 sync 通过
V2 sudo cat /proc/fs/nfsd/threads 32;/etc/nfs.conf 的 [nfsd] 段有 threads = 32 通过
V3 findmnt -no OPTIONS --target /srv/nfs/k8s rw,noatime,prjquota 通过
V4 kubectl get sc -o jsonpath='{range .items[?(@.provisioner=="nfs.csi.k8s.io")]}{.metadata.name}{" -> "}{.mountOptions}{"\n"}{end}' 两个 SC 均为 ["nfsvers=4.2","nconnect=4","hard","timeo=600","retrans=3","noatime"] 通过
V5 kubectl -n kube-system get secret nfs-mount-options -o jsonpath='{.data.mountOptions}' \| base64 -d nfsvers=4.2,nconnect=4,hard,timeo=600,retrans=3,noatime 通过
V6 kubectl get pv -o custom-columns='NAME:.metadata.name,OPTS:.spec.mountOptions' 11 个 NFS PV 全部为新参数 通过
V7 逐节点 findmnt -t nfs4 -o TARGET,OPTIONS(经 csi-nfs-node 容器 nsenter) 全部挂载含 vers=4.2 与 nconnect=4 通过(8/8)
V8 新建 PVC 后查其 PV 的 mountOptions 自动继承新参数 通过
V9 跨节点读写(node-01 写 / node-02 读) 内容一致 通过
V10 main.py --skip-measure 复跑 四阶段 skipped,零变更、无报错 通过
V11 对比 logs/measure-before-*.json 与 logs/measure-after-*.json 元数据 RTT 显著下降 通过,见下表

元数据延迟对比(节点侧 /proc/self/mountstats 的 per-op 平均 RTT;挂载重建后重新计数)

节点 / 后端 操作 改造前 改造后 改善
node-02 / HDD(prometheus 卷) OPEN 67.60 ms 0.25 ms 约 270×
node-02 / HDD(prometheus 卷) SETATTR 79.76 ms 0.23 ms 约 354×
node-02 / SSD(alertmanager 卷) SETATTR 3.22 ms 0.38 ms 约 8×
node-01 / SSD(mariadb 卷) SETATTR 3.05 ms 0.19 ms 约 16×
node-01 / SSD(minio 卷) OPEN 1.01 ms 0.32 ms 约 3×
node-03 / SSD(grafana 卷) OPEN 2.87 ms 0.37 ms 约 8×

宿主本地挂载点 fio(服务端侧路径,无网络因素):create p50 0.0265 → 0.026 ms、stat p50 0.0048 → 0.0033 ms、 delete p50 0.0468 → 0.0453 ms,变化不显著 —— 说明收益主要来自服务端 async 消除了机械盘的 fsync 等待。

输出不符时的处置:V1/V2 不达标 → 重跑 --stages server 并检查 sudo -n 权限; V4/V5 不达标 → 检查 values-nfs-csi.yaml 与 Secret 清单是否被手工改回,重跑 --stages cluster; V7 存在旧参数挂载 → 说明对应工作负载尚未重新挂载,重跑 --stages volume; V10 出现变更 → 说明有配置被外部改动,按输出逐条核对后再决定是否接受。

7. 回滚

7.1 备份位置

对象 位置 说明
/etc/exports、/etc/nfs.conf、/etc/fstab 原位:同目录 *.bak.<时间戳> 实现体 --rollback 依赖「同目录最新 .bak.*」,故不迁移
nfs-storage/values-nfs-csi.yaml、nfs-storage/manifests/00-mount-options-secret.yaml nfs-mount-tuning/backup/nfs-storage/… 结构为 backup/<相对项目根的原路径>/<原名>.bak.<时间戳>

完整台账(含每份备份的产生原因、回滚对象、可删时机)见 nfs-mount-tuning/backup/README.md。

7.2 回滚步骤

cd /home/rain/Documents/k8s_ins/nfs-mount-tuning

# 1) 服务端:按最新时间戳备份还原三个文件,并重新生效
.venv/bin/python main.py --config config/tune.yaml --rollback
#    预期:「已回滚 /etc/exports <- /etc/exports.bak.<ts>」等三行,最后「回滚成功」
#    等价手工动作:
#    sudo cp -a /etc/exports.bak.<ts>  /etc/exports
#    sudo cp -a /etc/nfs.conf.bak.<ts> /etc/nfs.conf
#    sudo cp -a /etc/fstab.bak.<ts>    /etc/fstab
#    printf '8\n' | sudo tee /proc/fs/nfsd/threads    # nfsd 线程回到默认
#    sudo exportfs -ra                               # 导出回到 sync
#    sudo mount -o remount /srv/nfs/k8s && sudo mount -o remount /srv/nfs/k8s-hdd

# 2) 集群侧:把 values 与 Secret 清单还原为 backup/ 下的旧版本后重新升级
cp backup/nfs-storage/values-nfs-csi.yaml.bak.<ts> /home/rain/Documents/k8s_ins/nfs-storage/values-nfs-csi.yaml
cp backup/nfs-storage/manifests/00-mount-options-secret.yaml.bak.<ts> \
   /home/rain/Documents/k8s_ins/nfs-storage/manifests/00-mount-options-secret.yaml
kubectl apply -f /home/rain/Documents/k8s_ins/nfs-storage/manifests/00-mount-options-secret.yaml
helm upgrade --install nfs-csi /home/rain/Documents/k8s_ins/nfs-storage/charts/csi-driver-nfs \
  -n kube-system -f /home/rain/Documents/k8s_ins/nfs-storage/values-nfs-csi.yaml --wait --timeout 6m
kubectl apply -f /home/rain/Documents/k8s_ins/nfs-storage/manifests/02-volumesnapshotclass.yaml
#    已有 PV 需再次 patch 回 `["nfsvers=4.1","hard","timeo=600","retrans=3","noatime"]` 并重启使用方

# 3) 业务恢复(若仍处于静默状态)
kubectl -n server patch mariadb mariadb --type=merge -p '{"spec":{"replicas":1}}'
kubectl -n server scale statefulset minio --replicas=1

7.3 回滚后的验证

sudo exportfs -v                       # 预期:三处导出回到 sync
sudo cat /proc/fs/nfsd/threads         # 预期:8
kubectl get sc -o jsonpath='{range .items[?(@.provisioner=="nfs.csi.k8s.io")]}{.metadata.name}{" -> "}{.mountOptions}{"\n"}{end}'
# 预期:回到 ["nfsvers=4.1","hard","timeo=600","retrans=3","noatime"]
cd /home/rain/Documents/k8s_ins/nfs-mount-tuning && .venv/bin/python main.py --config config/tune.yaml --skip-measure
# 预期:因参数回到旧值而检测出状态漂移(说明确实已回退;此时可再次正向执行)

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

任一达标即回滚:

  1. 任一 nfs4 挂载点挂载失败,或 Pod 出现 Input/output error;
  2. 改造后 per-op 平均 RTT 相对同后端基线劣化超过 20%;
  3. exportfs -v 或 nfsd 线程数未达目标值,且重试 2 次仍失败;
  4. 业务 Pod 在 300s 内未达到就绪状态。

8. 风险说明

# 风险 影响 缓解措施
R1 async 导出丢失持久化语义:客户端 COMMIT 被直接确认而不落盘 宿主非正常掉电或内核崩溃可能丢最近写入;承载数据库的导出有损坏可能 按用户明确指令执行;变更前已停止 mariadb/minio 写入;如需更强保证,应按导出粒度把数据库所在导出单独改回 sync 后 exportfs -ra
R2 nconnect=4 提高并发连接数 服务端 nfsd 线程不足时会转为排队 已同步把 nfsd 线程提到 32,实测元数据 RTT 全面下降
R3 环境在变更期间被外部改动:ldap/openldap 被删除、server/openldap 与 3 个新 PV 出现 静态清单中的对象可能已不存在,导致误判为漂移甚至执行失败 实现体已改为「对象不存在即跳过」,并按挂载路径中的 Pod UID 动态发现所有 NFS 使用者;执行前仍应确认无并发变更
R4 已有 PV 必须逐个 patch 并重新挂载才能生效 期间相关服务逐个中断 逐个滚动、非同时;用 findmnt -t nfs4 复核后才算完成
R5 属性缓存保持默认(acregmax=60、acdirmax=60) 跨客户端可见性维持原状 按用户决策不动属性缓存,避免 RWX 共享场景一致性退化
R6 loop 挂载 remount 失败会导致 noatime 未生效 宿主层仍有 atime 写放大 实测两项均已生效;实现体在默认 remount 失败时会带显式源与类型重试
R7 元数据压测会在导出根目录创建临时目录 占用少量空间与 IO 固定白名单路径 .meta-bench,仅允许删除挂载点的直接子目录,测完即清理
R8 判据写错会导致误报或漏报(本次曾把内核回显的 vers= 与配置的 nfsvers= 直接比对) 幂等复跑误判为「未达标」,触发一轮多余重启 已修正为统一成 vers= 后比对;排查过程见 docs/INCIDENT-20260919-mount-option-judgement.md

附录 A:交付物清单

路径(相对项目根) 说明
nfs-mount-tuning/docs/SOP.md 本文件
nfs-mount-tuning/docs/RUNBOOK.md 服务运行与使用手册(日常运维、故障处置)
nfs-mount-tuning/docs/INCIDENT-20260919-mount-option-judgement.md 变更期间一次误判事件的排障记录
nfs-mount-tuning/backup/README.md 备份台账(第 7 章所指)
nfs-mount-tuning/main.py 工程入口与阶段编排
nfs-mount-tuning/config/tune.yaml 期望状态配置(核心实现体,日常调整改这里)
nfs-mount-tuning/modules/server_module.py 服务端:导出 async、nfsd 线程、loop noatime
nfs-mount-tuning/modules/cluster_module.py 集群侧:values / Secret / StorageClass 同步
nfs-mount-tuning/modules/workload_module.py 静默写敏感业务(async 生效后自动跳过)
nfs-mount-tuning/modules/volume_module.py PV patch + 重新挂载 + 复核(含动态发现)
nfs-mount-tuning/modules/measure_module.py 元数据延迟度量(fio + mountstats)
nfs-mount-tuning/utils/ 本工程私有工具(命令执行、文本与文件处理)
nfs-mount-tuning/logs/ 运行日志与 before/after 度量报告
nfs-storage/values-nfs-csi.yaml 客户端参数实现体(本次已更新)
nfs-storage/manifests/00-mount-options-secret.yaml 删除卷所需的挂载参数 Secret(本次已更新)
nfs-storage/SOP.md 上一主题(存储类交付)的 SOP,参数与在跑业务清单已同步

附录 B:实现说明与已知坑

  1. 度量引擎名:Ubuntu 的 fio 3.36 已把 fileops 拆成 filecreate / filestat / filedelete 三个独立引擎;元数据压测的 jobfile 由 measure_module.py 生成,文件名通过 filename_format=meta-bench.$filenum 统一,确保 stat / delete 命中同一批文件。
  2. 实现体的备份行为:服务端模块把备份留在 /etc 同目录(--rollback 依赖), 集群模块把备份写入 backup/。两者都在 backup/README.md 登记。
  3. dry-run 语义:预演只输出变更明细,不写配置、不做备份、不跑压测、不改集群对象。
  4. 阶段短路:任一变更类阶段失败即终止后续阶段,并提示恢复命令: .venv/bin/python main.py --config config/tune.yaml --stages volume。
  5. 度量目录:measure_module 会在 /srv/nfs/k8s 下创建 .meta-bench,删除前校验 「必须是挂载点的直接子目录且以 .meta-bench 开头」,不使用通配删除。

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