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. 处置过程¶
- 先止损:确认误动作本身未改变任何配置(PV 与挂载参数均正确),因此无需回滚,只需停止继续误判。
- 修判定:把期望值归一化成内核回显形式再比对 ——
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 ""
- 修容忍度:新增
_target_state(target) -> (是否存在, 期望副本数),NotFound明确返回「不存在」, 调用处一律「对象不存在即跳过」(sync_status/execute/validate三处), 并同步应用到静默模块。 - 修解析:
exportfs -v的校验改为「先压平换行,再按导出路径切出片段」判断async。 - 复跑验证:四阶段全部
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同步,请勿在站点侧修改)