NFS 挂载卡死:一个「df 都卡住」的故障现场
故障现象通常是这样报上来的:服务器还能 SSH 上去,但敲 ls 没反应、df -h 也挂在那里不动、网站某个页面永久转圈,top 里能看到一两个进程处于 D 状态并且 CPU 占用是 0。kill -9 杀不掉它们,重启服务也不行,最后只能硬重启机器。
这就是典型的 NFS 挂载点失联。它的杀伤力被严重低估:NFS 是内核层的文件系统,一旦服务端不可达,任何 stat() 这个挂载点下文件的操作都会在内核里无限等待(默认的 hard 模式),而且这个等待不可中断——kill -9 对 D 状态进程无效,因为信号要等到系统调用返回才会被处理,而系统调用永远不返回。更糟的是,这个等待是不可重入的:如果卡住的进程持有别的锁(比如 MySQL 的表锁),整站就会跟着雪崩。
这篇文章讲三件事:为什么 NFS 会变成这样(hard vs soft 的真实取舍)、怎么在不重启的情况下识别和抢救、以及一套能让故障「可控降级」而不是「全站挂死」的挂载参数。
hard 与 soft:一个经典但不完整的答案
网上搜 NFS 卡死,得到的标准答案通常是「把 hard 改成 soft」。这个建议只说对了一半,照抄会掉进另一个坑。
- hard(默认):请求失败会无限重试,直到服务端恢复。优点是不会静默丢数据、不会返回假成功;缺点是服务端长时间不可达时进程永久卡死在
D状态。 - soft:重试超过
retrans次数后返回错误(EIO/ETIMEDOUT)给应用。好处是进程不会卡死;坏处是数据可能被静默写坏——应用收到 EIO 之后如果没做好错误处理,可能继续往下走,留下一个半写入的文件。
所以对「存网站数据、数据库文件、上传目录」这类场景,soft 是危险的。正确的组合是保持 hard,但加上 intr 和合理的 timeo/retrans,让等待可被信号中断、并控制单次重试的等待时长:
# /etc/fstab 里的推荐写法
nfs-server:/export/data /data nfs4 rw,hard,intr,vers=4.2,timeo=50,retrans=3,noatime,_netdev 0 0
逐个解释这些参数为什么重要:
timeo=50:单次 RPC 超时时间是 0.1 秒 × 50 = 5 秒(NFS 的timeo单位是 0.1 秒,不是毫秒,这是最容易记错的点)。默认值 600 意味着单次等待 60 秒,用户端体感就是「永远转圈」。retrans=3:重试 3 次。配合上面的timeo,意味着大约 15 秒后应用就能拿到错误,而不是无限等下去。intr:允许被信号中断(老内核很重要;新内核 2.6.25+ 后hard模式默认就可中断,但显式写上没坏处,也便于团队理解)。_netdev:这个参数经常被忽略但极其关键。它告诉 systemd 「这个挂载依赖网络」,从而在network-online.target之后才挂载。不写的话,机器重启时 systemd 可能在网络还没起来就去挂 NFS,卡住几分钟甚至触发依赖超时,导致整机启动变慢或进入 degraded 状态。noatime:NFS 环境下每次读取都回写 atime 会产生大量网络往返,关掉能明显降低负载。
已经卡死了怎么办:不重启的抢救流程
假设现在线上的机器已经卡住了,你需要做的是「先判断范围,再定点清理」,而不是直接 reboot。
第一步:找出 D 状态进程,并确认它卡在哪个挂载点。
# 1. 列出所有不可中断睡眠进程
ps -eo pid,stat,wchan:30,cmd | awk '$2 ~ /^D/'
# 2. 对可疑 PID,看它卡在什么内核函数
cat /proc//stack
cat /proc//wchan
# 3. 看这个进程打开了哪些文件(能定位到是不是 NFS 路径)
ls -l /proc//fd 2>/dev/null | grep nfs
如果 /proc/ 里出现 rpc_wait_bit_killable、nfs_wait_on_request、__nfs4_proc_* 这类字样,基本可以确定是 NFS 等待。
第二步:确认服务端状态,不要盲目等待。
# 从客户端测 NFS 服务端端口是否通(NFSv4 只用 2049)
nc -zv -w 3 nfs-server 2049
# 看本机 NFS 统计,重点看重传和超时
nfsstat -rc
# retrans 那一列持续增长 = 网络/服务端确实在丢包
# 看 RPC 队列里堆积了多少未完成请求
cat /proc/net/rpc/nfs
第三步:如果服务端确实不可用,用 umount 强制卸载。注意普通 umount 在卡死状态下也会挂住,必须用惰性卸载或强制卸载:
# 惰性卸载:立即从命名空间摘除,等引用计数归零后真正清理
umount -l /data
# 如果 -l 不行,用强制卸载(有数据丢失风险,但能救活机器)
umount -f /data
# 万不得已的最后一招:卸载 /etc/mtab 里的残留条目
umount -f -l /data && sed -i '\|/data|d' /etc/mtab
umount -l 之后进程通常会立刻从 D 状态恢复(返回 EIO),这才是让应用重启并恢复正常的关键。这也是为什么推荐 hard,intr 而不是裸 hard:intr 让信号能打断等待,配合 umount -l 效果最好。
让故障「可控」而不是「全站雪崩」的四个工程实践
把上面的一次性抢救变成长期设计,需要在架构和运维层面做四件事。
1. 绝对不要把 NFS 挂载点放在「关键路径的父目录」上。这是最致命也最常见的错误。举例:如果站点根目录 /www 是本地盘,而你把上传目录 /www/uploads 挂成了 NFS,那么 NFS 一挂,任何对 /www 的遍历操作都会卡住,包括 Nginx 启动、PHP 的 realpath 解析,整站直接不可用。正确做法是让 NFS 挂载点尽可能深、尽可能独立,比如 /mnt/nfs-uploads,然后通过软链接或者应用配置去引用它——但要注意软链接穿越仍会卡,所以更稳的是让应用层持有绝对路径。
2. 用 autofs 做按需挂载。autofs 的机制是:只有进程真正访问挂载点时才去挂载,闲置一段时间自动卸载。这带来一个巨大的好处:NFS 服务端挂掉时,如果没人访问那个目录,就不会有任何卡死;即使访问了,autofs 也能在超时后干净地返回错误,不会像静态挂载那样把内核状态搞死。配置示例:
# /etc/auto.master.d/data.autofs
/mnt/nfs /etc/auto.data --timeout=60
# /etc/auto.data
uploads -rw,hard,intr,vers=4.2,timeo=50,retrans=3 nfs-server:/export/uploads
--timeout=60 表示闲置 60 秒自动卸载。个人站如果 NFS 只是用来放备份或图片,autofs 是比静态挂载稳得多的选择。
3. 给挂载点加监控,而不是等用户报障。最直接的探针不是「ping 服务端」,而是「在挂载点做一次带超时的真实 IO」,因为 ping 通不代表 NFS 正常(服务端 rpcbind 挂了、nfsd 线程满了都会导致 ping 通但 IO 卡):
#!/bin/bash
# /usr/local/bin/nfs-probe.sh —— 带硬超时的 NFS 健康探针
set -euo pipefail
MOUNT=/mnt/nfs-uploads
if ! timeout 5 stat "$MOUNT" >/dev/null 2>&1; then
echo "NFS 挂载点 $MOUNT 无响应(5 秒超时)"
# 触发告警:邮件 / webhook
fi
timeout 5 stat 是关键——直接 stat 也会卡死,必须用 timeout 包一层。timeout 本身在极端情况下也可能杀不掉 D 状态进程,但至少脚本不会永久挂住。
4. 服务端侧:不要只配一个 NFS 节点。如果 NFS 服务端是单点,那它宕机就等于客户端全灭。个人站长如果非要用 NFS,至少要让服务端本身高可用(哪怕是两台机器 + keepalived 漂移 VIP),或者干脆换成 GlusterFS/Ceph 这类分布式方案——当然,对个人站来说,更务实的建议是:能用对象存储(S3 兼容)就不要自建 NFS,对象存储天生没有「挂载点卡死」这个问题,SDK 调用超时是应用层可控的。
一个容易被忽略的细节:NFSv3 与 NFSv4 的端口差异
很多人排查 NFS 不通时,习惯性去放行 111(portmap/rpcbind)、2049(nfs)、32768-60999(v3 的随机端口范围)这一套。这在 NFSv3 下是对的,但 NFSv4 只需要 2049——v4 把 rpcbind 和 mountd 都内联进了 2049 端口。
所以如果你在客户端看到 mount.nfs: Connection timed out,先确认版本:
# 看当前挂载用的什么版本
nfsstat -m
# 强制用 v4.2 挂载
mount -t nfs4 -o vers=4.2 nfs-server:/export/data /mnt/test
反过来,如果你在服务端只放行了 2049 但还是连不上,检查是不是客户端在退化成 v3 —— 常见原因是客户端内核或 /etc/nfsmount.conf 里写死了 vers=3,或者服务端 /etc/nfs.conf 的 [nfsd] vers3= 配置不匹配。把客户端和服务端的版本显式对齐(两边都写 vers=4.2),可以规避大量「莫名其妙连不上」的问题。
小结:NFS 故障处理的三条铁律
- 挂载参数用
hard,intr,timeo=50,retrans=3,noatime,_netdev,不要用裸hard(无限等待),也不要轻信「改soft就好」的说法(有静默写坏数据的风险)。 - NFS 挂载点绝不放在关键路径的父目录;能按需挂载就用
autofs;能用对象存储就不要自建 NFS。 - 监控要在挂载点做带
timeout的真实 IO 探针,而不是 ping 服务端;已经卡死时用umount -l让进程恢复,别硬重启机器。
NFS 是那种「配置 10 分钟、故障处理 3 小时」的组件,它的复杂度不在于用起来难,而在于故障时的表现完全不按常理出牌——kill -9 杀不掉、df 会挂住、ls 会永久等待。把上面这些参数和抢救流程提前写进运维手册,比事后 Google 快得多。