一次NFS存储服务端故障导致客户端进程陷入D状态业务整体假死的排查记录

业务接口大面积超时但 CPU 正常、load 飙到数百,根因是 NFS 服务端无响应,默认 hard 挂载下客户端进程无限等待陷入不可中断睡眠(D 状态),kill -9 都杀不掉。

一、问题背景

公司一套 OA 系统的附件服务,把用户上传的合同、扫描件等文件统一写到一台 NAS 上,客户端以 NFS 方式把 NAS 的 export 挂载到应用服务器的 /data/attachment 目录。这套架构运行了两年多,平时应用线程只要 write() 一下就能落盘,开发同学几乎忘了"本地磁盘"和"网络存储"的区别。

变故发生在一次周二的上午。运维群里突然弹出一连串告警:附件服务 HTTP 接口 P99 耗时从平时的 200ms 涨到 30s 以上,超时率飙到 40%,连带依赖附件的审批流接口也开始大面积 5xx。但诡异的是,监控大盘上这台应用服务器的 CPU 利用率只有 15% 左右,内存也富余,唯一异常的是 load average——从平时的 4 左右一路爬到了 300 多。第一反应是"是不是被打了",但 iptables 连接数、入口流量都正常,事情没那么简单。

二、故障现象

把现场能串起来的现象摊开看,至少有四条明显对不上"常规高负载":

  1. 接口超时但资源空闲:应用 HTTP 层大量 Read timed out / AsyncRequestTimeoutException,然而 top 里 CPU 大多数时间在 id(空闲),us/sy 都不高——说明不是计算密集,而是线程"卡住"了。
  2. load average 离谱地高:1 分钟负载一度冲到 380,但进程数并没有暴增。load 高通常意味着"运行队列 + 不可中断睡眠"的进程多,而这里明显是后者。
  3. 部分命令直接卡死:运维上机后想用 df -h 看磁盘,df 命令输出了本地盘后就卡住不动,过了好几分钟才勉强结束;ls /data/attachment 同样无响应。说明某个挂载点把进程"吸"住了。
  4. kill -9 杀不掉:把几个明显不干活的应用 worker kill -9 之后,进程依然存在,状态栏显示一个 D。这是整件事最关键的反常信号——连 SIGKILL 都杀不掉的进程,只可能处于不可中断睡眠。

三、排查过程

1. 先确认是不是进程"卡死"而非"忙死"

top 按状态过滤,立刻看到一大片状态为 D(即 D = uninterruptible sleep,不可中断睡眠)的进程,而且这些进程的 COMMAND 几乎都是 Java 应用的工作线程、日志滚动脚本,以及刚才那个卡住的 df

1
2
top -b -n1 | awk '$8=="D"{print}'
ps -eo pid,ppid,stat,wchan:32,cmd | awk '$3=="D"'

关键看 WCHAN 列(进程当前睡眠在内核的哪个函数上),这些 D 状态进程清一色停在 nfsrpc_wait_bit_killablefuse 相关的等待点上——全部和 NFS 客户端等待服务端回包有关。再补一刀确认:

1
2
cat /proc/<pid>/wchan      # 输出类似于 nfs_wait_bit_killable
cat /proc/<pid>/stack      # 内核栈能看到 nfs 调用链

2. 锁定"吸住"的挂载点

既然 df -h 会卡在挂载点,用 mount 列出 NFS 挂载,并单独测试它:

1
2
3
mount | grep nfs
# 10.20.30.40:/volume1/oa_attach on /data/attachment type nfs ...
df -h /data/attachment     # 直接卡住

再开一个终端用 strace 跟踪一个卡住的进程系统调用,看到它停在:

1
stat("/data/attachment",  <detached ...>

符号非常明确:进程在 stat() 这个 NFS 挂载点时,永远等不到服务端回复,于是挂在内核的 NFS 等待逻辑里。

3. 把怀疑下沉到 NFS 服务端

既然客户端在等服务端,那就看服务端(NAS,10.20.30.40)是否还"活":

1
2
3
ping 10.20.30.40                 # 能通,网络层没断
rpcinfo -p 10.20.30.40           # 卡住,无响应
showmount -e 10.20.30.40         # 同样卡住

rpcinfoshowmount 都走 RPC(端口 111 / 20048),它们卡住说明 NFS 服务端的 rpcbind / mountd / nfsd 已经不响应了。再在客户端抓包确认请求是不是"肉包子打狗":

1
2
tcpdump -nn -i any host 10.20.30.40 and port 2049 -c 50
# 客户端不断发 NFS READ/WRITE,但服务端没有任何 ACK 回包

最后翻客户端内核日志,印证判断:

1
2
3
dmesg -T | grep -i nfs
# [Tue Jul  7 10:12:03] nfs: server 10.20.30.40 not responding, still trying
# [Tue Jul  7 10:12:31] nfs: server 10.20.30.40 not responding, still trying

日志里 not responding, still trying 反复刷,是典型的 NFS 服务端无响应、客户端按 hard 语义死等的表现。

4. 服务端侧根因(事后复盘)

登录 NAS 带外管理口查看,发现是一块磁盘出现介质错误,nfsd 工作线程在向后端文件系统刷写时被卡在底层 IO 上,所有 nfsd 线程被这块坏盘拖死,对外不再响应任何新的 NFS 请求。也就是说:坏盘 → nfsd 线程阻塞 → NFS 服务端"假死" → 客户端 hard 挂载无限等待 → 进程 D 状态。

四、解决方案

1. 先止血:把挂载点从客户端摘掉

当务之急是让几十上百个 D 状态进程解除阻塞。按风险从低到高:

1
2
3
4
5
# 优先用 lazy umount,断开挂载点与目录的关联,正在等待的进程会拿到错误返回而解除 D 状态
umount -l /data/attachment

# 若仍不奏效,再尝试强制卸载
umount -f /data/attachment

umount -l(lazy)是关键招:它不要求当前没有进程使用该挂载点,而是立刻把挂载点从命名空间摘下,之后等待中的进程在下次 IO 时会收到 I/O error 而醒来(虽仍可能被应用层重试逻辑再次卡住,但至少解除了内核层的死等)。执行后 top 里 D 状态进程数迅速归零,load average 从 380 一路回落到个位数,HTTP 接口超时率也在几分钟内降到正常。

2. 恢复业务

临时把附件写入切到本地盘(mount --bind 一个本地目录顶上,或改应用配置指向本地路径),让审批流先恢复;同时通知 NAS 管理员重启 nfs-server 服务、隔离坏盘、修复 RAID。服务端 nfsd 恢复响应后,再重新挂载:

1
mount -t nfs 10.20.30.40:/volume1/oa_attach /data/attachment

五、根因分析

整条故障链的源头有两点,缺一不可:

  1. 服务端侧:NAS 单盘故障导致 nfsd 线程被后端 IO 阻塞,NFS 服务整体失去响应能力,且服务端本身没有对 nfsd 线程做隔离/超时保护。
  2. 客户端侧(更关键的放大因素):Linux NFS 客户端默认是 hard 挂载hard 而非 soft)。hard 模式下,NFS 请求如果得不到服务端确认,客户端会无限重试、永不放弃,对应进程进入 TASK_UNINTERRUPTIBLE(D 状态)。一旦进入 D 状态,SIGKILL 也无效——因为内核不允许进程在等待关键 IO 时被中途掐掉。结果就是:一个服务端故障,被 hard 语义放大成客户端成百上千个进程永久卡死、load 爆表、业务整体假死。

换句话说,这不是"客户端扛不住",而是客户端对存储故障缺乏兜底:它把"网络存储临时不可用"当成了"必须无限等待"的硬约束。

六、预防措施

这次故障之后,我们从客户端、服务端、架构三层做了加固:

  1. 客户端挂载加容错参数:把默认 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  0
    

    timeo=30 表示每次 RPC 超时 3 秒(单位是 0.1s),retrans=3 重试 3 次后返回错误,retry=2 限制挂载阶段重试。这样 NFS 不可用时进程会快速失败而不是永久 D 住。_netdev 保证网络就绪后再挂载、关机时先 umount。

  2. 监控要能抓到 D 状态:在 Node Exporter 之外补充采集 D 状态进程数和 load1,一旦 D 状态进程数 > 0 持续超过阈值就告警——这是 NFS 类故障最早的信号,比 HTTP 超时早得多。

  3. 对挂载点做存活探测:用 timeout 包裹的 stat 定时探 /data/attachment,或部署监控脚本在探测超时时自动 umount -l 兜底,避免人工响应不及时。

  4. 服务端高可用:NAS 侧对 nfsd 做进程级健康检查与自动重启;重要业务改用双机 + 共享存储(如 DRBD)或直接使用对象存储(S3/S3 兼容)替代 NFS 共享,从根本上消除"单存储挂掉拖垮应用"的强耦合。

  5. 应用层解耦:附件写入增加本地缓冲队列与异步刷盘,NFS 不可用时先落本地、恢复后再同步,避免业务线程直接同步阻塞在远程存储上。

七、总结

这次故障最"反直觉"的地方在于:监控上 CPU、内存都正常,业务却整体假死。根因不在算力,而在 NFS 客户端默认的 hard 挂载语义——它把"存储暂时不可用"翻译成了"进程永久等待"。排查的关键三步是:top 抓 D 状态进程 → wchan/stack 确认卡在 NFS 等待 → rpcinfo/tcpdump/dmesg 证明服务端无响应。止血用 umount -l 立竿见影。

记住一条运维铁律:凡是跨网络的文件系统挂载,默认 hard 都是隐患;要么加 soft + 超时重试,要么让应用在存储故障时能快速失败而不是永久等待。 否则,一个远端的坏盘,就足以让你的整台应用服务器"活着但死了"。

使用 Hugo 构建
主题 StackJimmy 设计