跳转至

INCIDENT-20260919 挂载参数判定逻辑缺陷导致误报与多余重启

主题:NFS 挂载参数优化变更期间,校验逻辑对内核回显格式判断错误,导致幂等复跑误判「未达标」 并触发一轮非预期的工作负载重启;同一批缺陷还造成 exportfs -v 校验误报。 记录人:运维(AI 协助);记录日期:2026-09-19

1. 概要

项 内容
时间 2026-09-19 13:36:29(首次误报)~ 13:39:10(修复前最后一次误动作),2026-09-19 当日闭环
等级 低(无数据损坏、无配置错误落地、短暂服务中断)
影响面 6 个 NFS 使用方被额外重启一次:server/mariadb、server/minio、server/openldap、monitoring/alertmanager-…、monitoring/prometheus-…、monitoring/kube-prometheus-stack-grafana
当前状态 已闭环。判定逻辑已修正,幂等复跑四阶段全部 skipped,8/8 条挂载参数达标

一句话结论:挂载参数本身改对了,是校验代码把配置里的 nfsvers= 与内核回显的 vers= 直接比对, 永远判不等,于是幂等复跑把「已达标」误判成「需重新挂载」,连带重启了业务。

2. 影响范围

  • 业务:6 个工作负载各被重启一次(逐台滚动,非同时)。其中 mariadb(数据库)与 minio 有约 1 分钟的服务中断, Prometheus/Grafana/Alertmanager/openldap 中断数十秒。期间无写入高峰,用户此前已确认这些业务数据可重建。
  • 数据:无损坏、无丢失。误动作是「重新挂载」,挂载参数本身正确,且 NFS 数据始终在服务端; 重启过程中无并发写入异常。
  • 配置:无错误落地。StorageClass、Secret、PV、/etc/exports、/etc/nfs.conf、/etc/fstab 的最终值均正确。
  • 未受影响:GitLab 本体(走 local-ssd)、宿主机上的 4 台 VM、/data 上的 qcow2。

3. 时间线

时间 事件
13:36:29 首次执行 --stages volume 后的校验中,出现「验证失败」日志,但被检的挂载参数实为 vers=4.2+nconnect=4(误报首现,当时误以为是旧 Pod 残留挂载,未深究)
13:36:30 该模块最终标记 success(校验未通过只记 warning),改造本身完成
13:37:5x 开始幂等复跑 main.py --skip-measure
13:38:12 volume_remount 阶段输出 7 项「实际挂载参数未达标,需重新挂载」漂移(误判),其中 6 项是真实存在的工作负载,1 项为已不存在的 ldap/openldap
13:38:14 开始「重启 server/mariadb 以重新挂载卷」
13:39:08 依次重启 server/minio、server/openldap、monitoring/alertmanager-…、monitoring/prometheus-…、monitoring/kube-prometheus-stack-grafana
13:39:09 对 ldap/openldap 执行恢复副本数时报 statefulsets.apps "openldap" not found,模块以 failed 结束
13:40 前后 定位根因(见第 4 章),修正 vers 比对与「对象不存在」处理
13:4x 再次幂等复跑:四阶段全部 skipped,零变更(闭环)
14:0x 节点侧复核:8/8 条挂载为 vers=4.2 + nconnect=4;业务 Pod 全部 Running

4. 根因

主因:配置侧选项名与内核回显选项名被直接比较。

实现体 volume_module.py 中的判定逻辑:

options = {option_key(opt): opt for opt in self.expect_options}
self.expect_version = options.get("nfsvers", "")     # 得到字符串 "nfsvers=4.2"
...
if self.expect_version and keys.get("vers") != self.expect_version:   # 实际值是 "vers=4.2"
    return False

内核在 findmnt / /proc/mounts 中回显的键名是 vers=,而 StorageClass 配置里写的是 nfsvers=, 两者字符串永不相等,于是所有已达标挂载都被判为未达标。

证据(同一行日志里,参数正确却被判失败):

[volume_remount] [ERROR] 验证失败: 节点 k8s-node-02 挂载参数为
/var/lib/kubelet/pods/0d071b89-…/volumes/kubernetes.io~csi/pvc-467e1cc4-…/mount
rw,noatime,vers=4.2,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,nconnect=4,…

次因:对「对象已被外部删除」未做区分。

ldap/openldap 在变更期间被外部迁移/删除(ldap 命名空间当时已空),而代码把「读取失败」与「副本数不符」 都当作漂移,于是对一个不存在的 StatefulSet 执行 scale,直接导致模块 failed。

同类缺陷:exportfs -v 校验误报。

exportfs -v 对长路径会换行输出:

/srv/nfs/k8s-hdd
                192.168.31.0/24(async,wdelay,hide,no_subtree_check,…)

原实现按「行内是否含路径」取行再判断 async,取到的是只有路径的那一行,于是误报「未体现 async」 (当时三处导出实际都已是 async)。该误报只影响日志,未触发任何动作。

非生产影响但同源的问题:元数据压测最初使用了 ioengine=fileops,而 Ubuntu 的 fio 3.36 已把该引擎 拆分为 filecreate / filestat / filedelete,导致压测最初无法执行;已在度量模块中修正。

5. 处置过程

  1. 先止损:确认误动作本身未改变任何配置(PV 与挂载参数均正确),因此无需回滚,只需停止继续误判。
  2. 修判定:把期望值归一化成内核回显形式再比对 —— nfsvers=4.2 → vers=4.2:
version_option = options.get("nfsvers") or options.get("vers") or ""
self.expect_version = f"vers={version_option.split('=', 1)[1]}" if "=" in version_option else ""
  1. 修容忍度:新增 _target_state(target) -> (是否存在, 期望副本数),NotFound 明确返回「不存在」, 调用处一律「对象不存在即跳过」(sync_status / execute / validate 三处), 并同步应用到静默模块。
  2. 修解析:exportfs -v 的校验改为「先压平换行,再按导出路径切出片段」判断 async。
  3. 复跑验证:四阶段全部 skipped;节点侧 8/8 挂载参数达标;业务 Pod 全部恢复 Running。

未做的动作:未回滚任何配置(无错误落地,回滚反而会引入偏差);未删除任何备份。

6. 恢复验证

cd /home/rain/Documents/k8s_ins/nfs-mount-tuning
.venv/bin/python main.py --config config/tune.yaml --skip-measure
# 预期:workload_quiesce / server_nfs_tuning / cluster_csi_tuning / volume_remount 四阶段均为 skipped
# 实测:全部 skipped,零变更

sudo exportfs -v                     # 预期:三处导出均为 async —— 实测通过
sudo cat /proc/fs/nfsd/threads       # 预期:32 —— 实测通过

节点侧实际挂载复核(经 csi-nfs-node 的 nsenter):8/8 条为 vers=4.2 + nconnect=4。 业务侧:kubectl get pods -n server -n monitoring 中相关 Pod 全部 Running,PVC 全部 Bound。

7. 复盘

有效的做法

  • 实现体自带完整日志(命令、返回码、参数原值),根因定位只依赖日志即可完成,无需复现环境。
  • 校验与动作分离:误报只让模块记 warning,没有直接触发破坏性动作;真正的多余动作也只发生在幂等复跑阶段。
  • 备份与回滚链路齐备,本次虽未用到,但确认了 --rollback 可用。
  • 关键参数有节点侧真实读数(findmnt / mountstats)作为最终判据,避免「文档说改了」等于「真的生效」。

拖慢恢复的因素

  • 判据未用真实回显格式自测:写判定时按配置的写法(nfsvers=)设想数据,没有先看一眼内核真实回显。
  • 预演(dry-run)不覆盖幂等路径:dry-run 只验证「会做什么」,不验证「判定是否正确」, 于是缺陷一直藏到真实复跑才暴露。
  • 首次误报(13:36:29)被归因为「旧 Pod 残留挂载」而未深究,错过了提前发现的机会。
  • 对外部环境变化的假设过强:静态清单假设对象一定存在,实际环境正在被人改动。

8. 行动项

事项 责任人 期限 状态
判定逻辑改为「内核回显形式」比对,并对同类键名(nfsvers/vers、nconnect)统一归一化 运维(rain) 已完成(2026-09-19) 已完成
工作负载清单改为「对象不存在即跳过」,并保留动态发现以覆盖清单外对象 运维(rain) 已完成(2026-09-19) 已完成
exportfs -v 校验改为压平换行后按路径切段判断 运维(rain) 已完成(2026-09-19) 已完成
元数据压测改用 fio 3.36 的 filecreate/filestat/filedelete 引擎 运维(rain) 已完成(2026-09-19) 已完成
变更前先确认「无他人同时操作」并记录当时的工作负载清单,作为误判时的对照基线 运维(rain) 下次变更前 待办
为判定逻辑补一组「用真实 findmnt 输出做输入」的自测样例,避免再次按设想写判据 运维(rain) 下次变更前 待办
排障期间确认的「旧 Pod 残留挂载」属正常滚动过渡,写入手册故障场景以免重复误判 运维(rain) 已完成(见 docs/RUNBOOK.md 第 8 章) 已完成

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