一、问题背景
公司一套 OA 系统的附件服务,把用户上传的合同、扫描件等文件统一写到一台 NAS 上,客户端以 NFS 方式把 NAS 的 export 挂载到应用服务器的 /data/attachment 目录。这套架构运行了两年多,平时应用线程只要 write() 一下就能落盘,开发同学几乎忘了"本地磁盘"和"网络存储"的区别。
变故发生在一次周二的上午。运维群里突然弹出一连串告警:附件服务 HTTP 接口 P99 耗时从平时的 200ms 涨到 30s 以上,超时率飙到 40%,连带依赖附件的审批流接口也开始大面积 5xx。但诡异的是,监控大盘上这台应用服务器的 CPU 利用率只有 15% 左右,内存也富余,唯一异常的是 load average——从平时的 4 左右一路爬到了 300 多。第一反应是"是不是被打了",但 iptables 连接数、入口流量都正常,事情没那么简单。
二、故障现象
把现场能串起来的现象摊开看,至少有四条明显对不上"常规高负载":
- 接口超时但资源空闲:应用 HTTP 层大量
Read timed out/AsyncRequestTimeoutException,然而top里 CPU 大多数时间在id(空闲),us/sy都不高——说明不是计算密集,而是线程"卡住"了。 - load average 离谱地高:1 分钟负载一度冲到 380,但进程数并没有暴增。load 高通常意味着"运行队列 + 不可中断睡眠"的进程多,而这里明显是后者。
- 部分命令直接卡死:运维上机后想用
df -h看磁盘,df命令输出了本地盘后就卡住不动,过了好几分钟才勉强结束;ls /data/attachment同样无响应。说明某个挂载点把进程"吸"住了。 kill -9杀不掉:把几个明显不干活的应用 workerkill -9之后,进程依然存在,状态栏显示一个D。这是整件事最关键的反常信号——连 SIGKILL 都杀不掉的进程,只可能处于不可中断睡眠。
三、排查过程
1. 先确认是不是进程"卡死"而非"忙死"
用 top 按状态过滤,立刻看到一大片状态为 D(即 D = uninterruptible sleep,不可中断睡眠)的进程,而且这些进程的 COMMAND 几乎都是 Java 应用的工作线程、日志滚动脚本,以及刚才那个卡住的 df:
|
|
关键看 WCHAN 列(进程当前睡眠在内核的哪个函数上),这些 D 状态进程清一色停在 nfs、rpc_wait_bit_killable 或 fuse 相关的等待点上——全部和 NFS 客户端等待服务端回包有关。再补一刀确认:
|
|
2. 锁定"吸住"的挂载点
既然 df -h 会卡在挂载点,用 mount 列出 NFS 挂载,并单独测试它:
|
|
再开一个终端用 strace 跟踪一个卡住的进程系统调用,看到它停在:
|
|
符号非常明确:进程在 stat() 这个 NFS 挂载点时,永远等不到服务端回复,于是挂在内核的 NFS 等待逻辑里。
3. 把怀疑下沉到 NFS 服务端
既然客户端在等服务端,那就看服务端(NAS,10.20.30.40)是否还"活":
|
|
rpcinfo 和 showmount 都走 RPC(端口 111 / 20048),它们卡住说明 NFS 服务端的 rpcbind / mountd / nfsd 已经不响应了。再在客户端抓包确认请求是不是"肉包子打狗":
|
|
最后翻客户端内核日志,印证判断:
|
|
日志里 not responding, still trying 反复刷,是典型的 NFS 服务端无响应、客户端按 hard 语义死等的表现。
4. 服务端侧根因(事后复盘)
登录 NAS 带外管理口查看,发现是一块磁盘出现介质错误,nfsd 工作线程在向后端文件系统刷写时被卡在底层 IO 上,所有 nfsd 线程被这块坏盘拖死,对外不再响应任何新的 NFS 请求。也就是说:坏盘 → nfsd 线程阻塞 → NFS 服务端"假死" → 客户端 hard 挂载无限等待 → 进程 D 状态。
四、解决方案
1. 先止血:把挂载点从客户端摘掉
当务之急是让几十上百个 D 状态进程解除阻塞。按风险从低到高:
|
|
umount -l(lazy)是关键招:它不要求当前没有进程使用该挂载点,而是立刻把挂载点从命名空间摘下,之后等待中的进程在下次 IO 时会收到 I/O error 而醒来(虽仍可能被应用层重试逻辑再次卡住,但至少解除了内核层的死等)。执行后 top 里 D 状态进程数迅速归零,load average 从 380 一路回落到个位数,HTTP 接口超时率也在几分钟内降到正常。
2. 恢复业务
临时把附件写入切到本地盘(mount --bind 一个本地目录顶上,或改应用配置指向本地路径),让审批流先恢复;同时通知 NAS 管理员重启 nfs-server 服务、隔离坏盘、修复 RAID。服务端 nfsd 恢复响应后,再重新挂载:
|
|
五、根因分析
整条故障链的源头有两点,缺一不可:
- 服务端侧:NAS 单盘故障导致 nfsd 线程被后端 IO 阻塞,NFS 服务整体失去响应能力,且服务端本身没有对 nfsd 线程做隔离/超时保护。
- 客户端侧(更关键的放大因素):Linux NFS 客户端默认是 hard 挂载(
hard而非soft)。hard 模式下,NFS 请求如果得不到服务端确认,客户端会无限重试、永不放弃,对应进程进入TASK_UNINTERRUPTIBLE(D 状态)。一旦进入 D 状态,SIGKILL也无效——因为内核不允许进程在等待关键 IO 时被中途掐掉。结果就是:一个服务端故障,被 hard 语义放大成客户端成百上千个进程永久卡死、load 爆表、业务整体假死。
换句话说,这不是"客户端扛不住",而是客户端对存储故障缺乏兜底:它把"网络存储临时不可用"当成了"必须无限等待"的硬约束。
六、预防措施
这次故障之后,我们从客户端、服务端、架构三层做了加固:
-
客户端挂载加容错参数:把默认 hard 改为 soft 并加超时与重试,避免无限等待:
1 2# /etc/fstab 示例(注意 soft + timeo + retrans + retry) 10.20.30.40:/volume1/oa_attach /data/attachment nfs soft,timeo=30,retrans=3,retry=2,_netdev 0 0timeo=30表示每次 RPC 超时 3 秒(单位是 0.1s),retrans=3重试 3 次后返回错误,retry=2限制挂载阶段重试。这样 NFS 不可用时进程会快速失败而不是永久 D 住。_netdev保证网络就绪后再挂载、关机时先 umount。 -
监控要能抓到 D 状态:在 Node Exporter 之外补充采集
D状态进程数和load1,一旦 D 状态进程数 > 0 持续超过阈值就告警——这是 NFS 类故障最早的信号,比 HTTP 超时早得多。 -
对挂载点做存活探测:用
timeout包裹的stat定时探/data/attachment,或部署监控脚本在探测超时时自动umount -l兜底,避免人工响应不及时。 -
服务端高可用:NAS 侧对 nfsd 做进程级健康检查与自动重启;重要业务改用双机 + 共享存储(如 DRBD)或直接使用对象存储(S3/S3 兼容)替代 NFS 共享,从根本上消除"单存储挂掉拖垮应用"的强耦合。
-
应用层解耦:附件写入增加本地缓冲队列与异步刷盘,NFS 不可用时先落本地、恢复后再同步,避免业务线程直接同步阻塞在远程存储上。
七、总结
这次故障最"反直觉"的地方在于:监控上 CPU、内存都正常,业务却整体假死。根因不在算力,而在 NFS 客户端默认的 hard 挂载语义——它把"存储暂时不可用"翻译成了"进程永久等待"。排查的关键三步是:top 抓 D 状态进程 → wchan/stack 确认卡在 NFS 等待 → rpcinfo/tcpdump/dmesg 证明服务端无响应。止血用 umount -l 立竿见影。
记住一条运维铁律:凡是跨网络的文件系统挂载,默认 hard 都是隐患;要么加 soft + 超时重试,要么让应用在存储故障时能快速失败而不是永久等待。 否则,一个远端的坏盘,就足以让你的整台应用服务器"活着但死了"。