上周五晚上,客户半夜打电话来:网站接口大面积超时,登录页转圈十几秒,监控面板上 CPU 倒是不高,10% 都不到。我登上服务器第一眼看 top,进程列表里有个 nginx worker 挂着,状态列写着一个我不太想看到的字母D。
如果你正在处理进程卡死、磁盘 IO 阻塞方面的问题,或者需要一台配置合适的机器先把环境跑起来,可以先看看这几家的报价和技术说明:vmiss(服务器与主机,虚拟主机到专用服务器一站式);lightlayer(服务器与主机,独立物理服务器)
D 状态的全称是 uninterruptible sleep,中文一般翻译成「不可中断睡眠」。它的关键特征是:这个进程正在等一个几乎无法被中断的 I/O,信号递进去没反应,连 kill -9 都杀不掉。我当时的第一反应是「进程死锁了」,想重启服务了事,结果 systemctl restart 一直卡在那儿不返回,折腾了七八分钟才放弃事后证明这是我这晚犯的最贵的错。

第一步:先确认它到底在等什么
D 状态最容易被误判成死锁,但它跟死锁完全是两回事。死锁是进程等不到对方主动让步,而 D 状态是进程已经交给内核了,内核说「这个 I/O 我拿不到,你去等」,然后进程就进入一种叫 task_uninterruptible 的等待。它等的对象通常是四种:磁盘/NVMe 的物理 I/O、网络文件系统(NFS、CIFS)上的读写、块设备层的请求,以及页缓存回写。
确认方法很简单,先看进程栈:
cat /proc/<pid>/stack(需要 root,很多内核版本要开 CONFIG_STACKTRACE),或者用 cat /proc/<pid>/wchan 看它睡在哪个函数上。我的那个 worker 的 wchan 显示的是 nfs_wait_bit_killable这一眼就基本锁定了方向:NFS,不是本地盘。
同时看内核日志,dmesg 里的 task ... blocked for more than 120 seconds 那几条 hung task 记录能把阻塞点和栈打出来,这是最直接的证据。如果没开 hung task 检测,可以临时打开:
echo 120 > /proc/sys/kernel/hung_task_timeout_secsecho 1 > /proc/sys/kernel/hung_task_panic

第二步:区分是「本地盘慢」还是「网络文件系统挂死」
这一步别跳过。我一开始怀疑的是云盘底层抖动,因为 iostat -x 1 里 %util 一直贴着 95 以上。但 iostat 看的是本地块设备,如果进程等的是 NFS,读写根本没落到本地盘上,%util 自然不会有什么异常变化。
真正的判据是这几条:
其一,看 /proc/<pid>/io 里的 read_bytes / write_bytes 这两个「真正落到块设备」的计数,如果它们几乎不动,而 rchar / wchar(系统调用层读写的字节数)在涨,说明数据卡在协议层,压根没到磁盘。
其二,用 mount | grep nfs 看挂载选项。我这台机器上的挂载是这样的:
10.0.0.8:/data /mnt/data nfs defaults,_netdev,vers=3 0 0
注意 soft 和 hard 这对选项的默认值。默认挂载是 hard,意思是服务端不响应就无限期重试、进程就一直 D 状态挂着好处是数据不丢,坏处是卡住的服务端会把客户端一起拖死。我出问题那台机器上,恰好有一批任务改成了 soft。
其三,挂载点本身的状态。stat /mnt/data 如果返回 stat: /mnt/data: Transport endpoint is not connected,说明服务端重启过、或者网络断了,客户端这边挂着的是已经失效的旧挂载。

第三步:为什么改个挂载参数就能救回来
根因找到了:那台 NFS 服务端在磁盘故障期间重启了几次,客户端的挂载没做 hard 保护也没配 intr/timeo,于是每一次读写都在无限重试。所有需要访问 /mnt/data 的 worker 一个接一个进 D 状态,进程表越堆越多,最后连 nginx 主进程想接管都接管不了因为 worker 的 fd 还卡在 NFS 的 RPC 调用上,主进程等它退出,主进程就等。
我的处理分两步:先止损,再修根。
止损是把挂载改成带超时的软挂载并允许中断,让请求能失败返回而不是无限等:
umount -l /mnt/datamount -t nfs -o soft,timeo=100,retrans=2,vers=3 10.0.0.8:/data /mnt/data
这里 timeo=100 的单位是百分之一秒,也就是单次 RPC 最多等 1 秒,retrans=2 表示重试两次,累计 3 秒左右就放弃返回错误。umount -l 是 lazy umount,先从目录树摘掉挂载点,再等引用它的进程自己退出,这一步能避免直接 umount 卡住不动。
修根是给这类关键挂载换回 hard 加 intr 的组合,并且在 fstab 里写明 _netdev(保证网络起来之后才挂载)、nofail(挂载失败不阻塞开机)。另外加一条 systemd 的依赖,让依赖这个目录的服务在挂载就绪后再启动:
[Unit]
RequiresMountsFor=/mnt/data
After=remote-fs.target
最后别忘了 NFS 服务端那边也要有 rpc.statd、hard_mount 的一致配置,两端参数不对称是这类故障最常见的根因。

复盘:这类问题我下次会先做的三件事
第一,kill -9 对 D 状态无效,别在重启服务上浪费时间,先 wchan + dmesg 定位等待源。
第二,挂载参数要当成配置来管。生产环境的 mount 选项应该是 review 过的,而不是随手一个 defaults。soft 挂载看似"更安全",实际上是把数据一致性的风险从服务端转移到了业务代码里,业务代码没做重试和降级就会直接把错误抛给用户。
第三,监控里一定要加 hung_task 和 NFS 客户端错误这两项。D 状态进程数量、NFS 重传计数,这俩指标在故障扩大之前就能报警。我这次是被客户电话叫醒的,其实 nfsstat -c 里的重传计数早就在涨了,只是没人看。
如果你的机器上已经出现类似的 D 状态进程堆积,先别急着重启。进程删不掉往往不是进程的问题,是它下面那块存储的问题,顺着 wchan 往下查一层,通常就能找到真正该修的地方。



喜欢
顶
难过
囧
围观
无聊





