服务器与 Linux 排查 列表全部文章
服务器与 Linux 排查

load average 飙到 60,CPU 却在摸鱼:一场由 D 状态进程引发的“假繁忙”

监控里 load 飙到几十,top 里 CPU 却几乎空闲。原来 Linux 的 load 也统计 D 状态进程。用 vmstat、iostat -x、pidstat -d、iotop 和 ps 找出是磁盘慢还是 NFS 卡死,并讲清 await 与 %util 怎么读。

更多

周一早上九点半,我刚打开电脑,监控大盘就红了一片:某台应用服务器 load average 63.4。8 核的机器,load 63,按老经验这就是“每个核排着 8 个人等 CPU”,应该已经烧得冒烟了。

我心一沉,登上机器 top 一看:CPU 空闲 92%。这台机器不但没冒烟,简直在午休。接口倒是确实慢,但日志里没有异常。一台“很闲”的机器,load 却高得离谱——这就是本篇的主题:load 并不等于 CPU 忙。

一、现象:load 高,CPU 闲

先把现场留下来(后面要对比“好的时候长什么样”):

$ uptime
 09:34:12 up 87 days,  2:41,  2 users,  load average: 63.42, 41.10, 18.77

$ top -bn1 | head -12
top - 09:34:20 up 87 days,  2:41,  2 users,  load average: 63.31, 42.05, 19.20
Tasks: 412 total,   2 running, 268 sleeping,   0 stopped,   0 zombie
%Cpu(s):  3.1 us,  1.9 sy,  0.0 ni, 88.6 id,  6.2 wa,  0.0 hi,  0.2 si,  0.0 st
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 8231 app       20   0 8123456   3.1g  24812 S   4.3 19.6  41:33.12 java
 ...

三个数字凑一起有点奇怪:load 60+、id 88%、wa 6%。load 的三个值 63 → 41 → 18 是 1/5/15 分钟均值,呈明显上升趋势,说明这事是最近十几分钟才开始恶化的,不是长期状态。

二、先搞懂:load average 到底在数什么

很多人(包括以前的我)以为 load 是“CPU 排队长度”。这在 BSD/Solaris 上大致成立,但Linux 的 load average 数的是两类任务:正在运行/等待运行的(R 状态),以及处于不可中断睡眠的(D 状态,TASK_UNINTERRUPTIBLE)。Brendan Gregg 在《Linux Load Averages: Solving the Mystery》里追溯过历史:这个行为来自 1993 年的一个补丁,理由是“正在换页或等待‘快’的不可中断 I/O 的进程同样在消耗系统资源”。所以在 Linux 上,磁盘或 NFS 卡住时,即使 CPU 空着,load 也会涨。

ps 手册里对 D 的定义是“uninterruptible sleep (usually IO)”,也就是不可中断睡眠,通常是在等 I/O。这种状态下进程不能被信号打断,连 kill -9 都不管用——因为它在内核里等一个必须完成的 I/O 操作,要是中途被杀,可能会留下内核态不一致。这也是为什么“D 进程杀不掉”是运维经典恐怖故事。

所以公式很简单:load ≈ R 状态数 + D 状态数。load 高 + CPU 闲,几乎可以直接断言:有一堆进程/线程卡在 D 状态,在等 I/O。接下来的问题就是:等的是本地磁盘,还是 NFS 之类的网络存储?

三、排查路线图

load 高,top 里 id 很高先怀疑 D 状态进程 vmstat 1 看 b 列 / wab = 不可中断睡眠的进程数 ps 找 D 状态 + wchan谁在等?卡在哪个内核函数 wchan / 挂载类型判断去向nfs_* / rpc_* 还是 io_schedule / jbd2 本地盘慢:iostat -x 1 → await 高、aqu-sz 高pidstat -d / iotop 找谁在狂读写 网络存储挂了:iostat 本地盘很闲nfsstat / mount / 存储端与网络排查

四、动手:分清“磁盘慢”还是“存储挂”

第 1 步:vmstat 1,确认有多少进程被 I/O 堵住

$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1 58      0 412300 210044 5210112    0    0     8  1204  980 1650  3  2 89  6  0
 0 61      0 412288 210044 5210112    0    0     0   312  870 1500  2  1 90  7  0
 1 63      0 412100 210044 5210120    0    0     0   256  910 1580  2  1 91  6  0
 0 63      0 412100 210044 5210120    0    0     0   240  905 1570  3  1 90  6  0

最关键的是前两列:r(等 CPU 的可运行进程数)只有 0~1,b(处于不可中断睡眠的进程数)高达 58~63,正好对得上 load 的 63。真相大白:load 里几乎全是 D 状态。wa 6% 不算高,别被它迷惑——wa 只是“CPU 空闲且有 I/O 未完成”的时间占比,进程卡得再多,只要还有别的核在空闲,wa 就不会很夸张;判断这类问题请以 b 列和 D 进程数为准,而不是 wa。

第 2 步:把 D 状态进程抓出来,看它们卡在哪

$ ps -eo pid,stat,wchan:32,comm | awk '$2 ~ /^D/' | head
 8590 D    nfs_wait_bit_killable           java
 8611 D    nfs_wait_bit_killable           java
 8623 D    nfs_wait_bit_killable           java
 9002 D    rpc_wait_bit_killable           df
 9107 D    nfs_wait_bit_killable           logrotate
 ...

$ ps -eo stat | grep -c '^D'
61

ps 的 wchan 字段是“进程正在睡眠的内核函数名”。这里全是 nfs_wait_bit_killable / rpc_wait_bit_killable——关键词是 nfs 和 rpc,说明进程们是在等 NFS 服务端的响应。顺便留意 df 也在里面:当 NFS 挂载点失联时,任何碰到那个挂载点的命令(df、ls、find)都会跟着挂住,这是很多人“敲 df 敲到终端假死”的原因。

如果 wchan 显示的是别的,比如 io_schedule、jbd2_log_wait_commit(ext4 日志提交)、wait_on_page_bit,则说明是本地盘的 I/O 队列或文件系统日志卡住。

没有 wchan 权限或输出为 - 时,可以用 cat /proc/PID/stack(需要 root)看内核栈。同时确认这个业务用的挂载到底是什么:

$ mount | grep -E 'nfs|cifs'
10.0.8.20:/export/shared on /data/shared type nfs4 (rw,relatime,vers=4.1,hard,timeo=600,retrans=2,...)

$ findmnt -t nfs,nfs4

看到 hard 就要警惕了:hard 挂载在服务端不响应时会无限重试,进程永远停在 D 状态;soft 挂载会超时返回错误,但有数据损坏风险。这是 NFS 的经典取舍。

第 3 步:iostat -x 1,确认本地盘有没有喊救命

为了排除“其实本地盘也慢”,看 iostat 的扩展统计(sysstat 包):

$ iostat -x 1 3
Device   r/s   w/s  rkB/s  wkB/s  r_await  w_await  aqu-sz  %util
vda     2.00 15.00   32.0  240.0     0.50     1.20    0.02   1.8
vdb     0.00  0.00    0.0    0.0     0.00     0.00    0.00   0.0

本地盘 vda 的 await 才 1 毫秒左右、%util 不到 2%,几乎无事可做。本地盘很闲,D 进程却一大堆——矛头指向网络存储。

这里顺便把 iostat -x 几个关键列讲清楚(依据 iostat 手册):

  • r_await / w_await:读/写请求的平均耗时(毫秒),包含排队时间和服务时间。机械盘几十毫秒开始就该警觉,SSD 超过几毫秒就有问题;老版本 sysstat 里合并成一列 await;
  • aqu-sz(旧版叫 avgqu-sz):平均队列长度,持续大于 1 说明有请求在排队;
  • %util:设备有 I/O 请求在处理的时间占比。手册明确说明:对 RAID、现代 SSD 这类可并行处理请求的设备,这个数字并不反映性能极限——所以“%util 100% = 盘打满”只对老式串行设备成立,判断瓶颈要结合 await 和 aqu-sz;
  • iostat 只能看到本机块设备,NFS 是网络文件系统,不会出现在 iostat 的磁盘列表里,这本身就是一个佐证。

第 4 步:如果是本地盘慢,谁在狂读写?

假设第 3 步看到的不是空闲,而是 vda 的 w_await 200ms、%util 100%,那下一个问题就是“谁在写”。pidstat 和 iotop 各有所长:

$ pidstat -d 1 3
Linux 5.10.0 (demo)   09/29/2026   _x86_64_   (8 CPU)

09:41:02  UID   PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
09:41:03  1001  8231      0.00  86532.00      0.00     412  java
09:41:03     0  9310  20480.00      0.00      0.00      35  tar

$ sudo iotop -oPa      # 只显示真正在做 I/O 的进程,累计值
Total DISK READ: 19.53 M/s | Total DISK WRITE: 84.51 M/s
  PID  PRIO  USER   DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
 8231 be/4  app      0.00 B   4.02 G      0.00 %  96.40 %  java -jar order.jar

pidstat 的 kB_wr/s 一眼看出是 java 在猛写,iodelay 是该进程等块 I/O 的时钟滴答数;iotop 的 IO> 列则是进程花在等 I/O 上的时间百分比。找到元凶后再去看它为什么写(日志级别被改成 DEBUG?定时导出?备份 tar 和业务抢盘?)。

第 5 步:确认 NFS 端的状态

回到这次的场景,问题在 NFS。客户端侧可以看:

$ dmesg -T | tail
[Mon Sep 29 09:22:41 2026] nfs: server 10.0.8.20 not responding, still trying
[Mon Sep 29 09:24:12 2026] nfs: server 10.0.8.20 not responding, still trying

$ nfsstat -c | head       # 客户端 RPC 重传次数
$ ping -c 3 10.0.8.20

server ... not responding, still trying 是 hard 挂载的标志性日志:内核在重试,业务线程在 D 状态里陪着等。到这里根因就清楚了:NFS 服务端(存储机)出问题,或者中间网络抖动。联系存储同学一查,服务端那晚正在做 RAID 重建,I/O 被压得半死。

五、根因:为什么一个存储挂了会把业务线程“全体传染”

因果链是这样的:

  1. 应用有一个功能——把导出文件、附件写到 NFS 共享目录(/data/shared);
  2. NFS 服务端响应变慢,客户端 hard 挂载让写操作无限等待;
  3. Tomcat 工作线程一旦调用到写文件,就进入 D 状态;每来一个相关请求,就吃掉一个线程;
  4. 线程池被逐个耗尽,其他不相关的接口也排队;同时 logrotate、df、监控采集脚本碰到挂载点也一起进 D 状态,load 蹭蹭涨到 63;
  5. CPU 完全没活干,所以 us、sy 都很低——“机器很闲,服务很慢”。

一个额外的教训:把业务的关键路径放在网络存储上,等于把可用性押在了另一台机器的心情上。而且线程卡在 D 状态时,jstack 看到的线程状态往往是 RUNNABLE(Java 认为它在跑本地方法),栈顶停在 FileOutputStream.writeBytes 之类的 native 方法——如果只看 Java 栈,很容易被误导。这次要不是先看了 load 与 CPU 的反差,我大概会在 GC 上白白耗半小时。

六、止血与修复

止血

  1. 先让业务绕开慢存储:通过配置开关关闭“写共享目录”的功能,或者临时把写入目标切到本地盘;
  2. 别指望 kill -9 能清掉 D 进程。NFS 的 hard 挂载有一个折中选项:intr/killable 语义(现代内核里进程处于 *_killable 睡眠,可以被致命信号杀掉,这也是 wchan 名字里带 killable 的原因)——所以实际上可以试试 kill -9,但 nfs_wait_bit_killable 之外的 D 状态(比如卡在磁盘驱动里)依旧杀不掉;
  3. 存储恢复后,D 进程会自己陆续醒来,load 会按 1/5/15 分钟的指数衰减慢慢回落;load 回落有滞后,不要看到 1 分钟值还高就以为没好;
  4. 如果存储短期恢复不了,最后的手段是 umount -f -l /data/shared(懒卸载),并重启相关应用实例。这一步会中断未完成的写入,要先和业务确认。

根治

# 1. NFS 挂载选项:给出超时与有限重试,避免无限挂死线程
#    /etc/fstab 示例(是否用 soft 要按业务对数据一致性的要求权衡)
10.0.8.20:/export/shared  /data/shared  nfs4  rw,soft,timeo=100,retrans=3,noatime  0 0

# 2. 应用层:写共享目录必须走独立线程池 + 超时 + 熔断,别占用 Tomcat 工作线程
# 3. 监控:把 D 状态进程数和 load 一起监控
$ ps -eo stat | grep -c '^D'     # 采集成 node_procs_blocked 指标告警
# node_exporter 里对应 node_procs_blocked,node_load1 也要和 CPU 使用率一起看

还有个更根本的建议:不要让核心业务同步依赖 NFS。能换对象存储(OSS/S3/MinIO)就换,异步落盘、失败可重试。存储再挂,也只影响导出功能,而不是整个应用。

七、复盘

做法为什么
load 高先对照 CPU:vmstat 1 看 r 与 bLinux 的 load = R + D,r 高是 CPU 问题,b 高是 I/O/锁问题
用 ps -eo pid,stat,wchan:32,comm 找 D 状态进程wchan 直接告诉你卡在哪个内核函数,nfs/rpc 与 io_schedule 是两条路
iostat -x 1 看 await、aqu-sz,别只看 %util%util 对 SSD/RAID 这类并行设备不代表打满
本地盘忙用 pidstat -d、iotop -oPa 找元凶知道谁在写,才能决定是限流、错峰还是换盘
NFS 挂载慎用 hard,设置 timeo/retrans,应用层加超时和熔断hard 会让业务线程无限期停在 D 状态
监控 load、blocked 进程数、磁盘 await 三件套只盯 CPU 使用率会在“闲得发慌的故障”面前失明

一句话备忘:load 高 CPU 闲 → 十有八九是 D 状态;vmstat 看 b 列,ps 看 stat 和 wchan,iostat -x 判断本地盘,nfs/rpc 关键词就去查存储。

从那以后我对“load 高”的态度变了:它不是“机器累”,而是“机器里有一群人在排队等某件事”——至于等的是 CPU 还是那块不争气的存储,得看看队伍的样子才知道。毕竟,排队的人多,不一定是店火,也可能是收银台坏了。

参考资料

  • Linux Load Averages: Solving the Mystery(Brendan Gregg):解释 Linux load 同时统计 R 状态与不可中断状态任务,以及用 vmstat/pidstat/iostat/mpstat 分解 load 的方法。
  • iostat(1) 手册:r_await、w_await、aqu-sz、%util 字段定义,以及 %util 对并行设备不反映性能极限的说明。
  • ps(1) 手册:进程状态码(D 为不可中断睡眠,通常是 I/O)与 wchan 字段(进程睡眠所在的内核函数)。
— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人