周一早上九点半,我刚打开电脑,监控大盘就红了一片:某台应用服务器 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 之类的网络存储?
三、排查路线图
四、动手:分清“磁盘慢”还是“存储挂”
第 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'
61ps 的 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.jarpidstat 的 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.20server ... not responding, still trying 是 hard 挂载的标志性日志:内核在重试,业务线程在 D 状态里陪着等。到这里根因就清楚了:NFS 服务端(存储机)出问题,或者中间网络抖动。联系存储同学一查,服务端那晚正在做 RAID 重建,I/O 被压得半死。
五、根因:为什么一个存储挂了会把业务线程“全体传染”
因果链是这样的:
- 应用有一个功能——把导出文件、附件写到 NFS 共享目录(
/data/shared); - NFS 服务端响应变慢,客户端
hard挂载让写操作无限等待; - Tomcat 工作线程一旦调用到写文件,就进入 D 状态;每来一个相关请求,就吃掉一个线程;
- 线程池被逐个耗尽,其他不相关的接口也排队;同时 logrotate、df、监控采集脚本碰到挂载点也一起进 D 状态,load 蹭蹭涨到 63;
- CPU 完全没活干,所以
us、sy都很低——“机器很闲,服务很慢”。
一个额外的教训:把业务的关键路径放在网络存储上,等于把可用性押在了另一台机器的心情上。而且线程卡在 D 状态时,jstack 看到的线程状态往往是 RUNNABLE(Java 认为它在跑本地方法),栈顶停在 FileOutputStream.writeBytes 之类的 native 方法——如果只看 Java 栈,很容易被误导。这次要不是先看了 load 与 CPU 的反差,我大概会在 GC 上白白耗半小时。
六、止血与修复
止血
- 先让业务绕开慢存储:通过配置开关关闭“写共享目录”的功能,或者临时把写入目标切到本地盘;
- 别指望
kill -9能清掉 D 进程。NFS 的 hard 挂载有一个折中选项:intr/killable语义(现代内核里进程处于*_killable睡眠,可以被致命信号杀掉,这也是 wchan 名字里带 killable 的原因)——所以实际上可以试试kill -9,但nfs_wait_bit_killable之外的 D 状态(比如卡在磁盘驱动里)依旧杀不掉; - 存储恢复后,D 进程会自己陆续醒来,load 会按 1/5/15 分钟的指数衰减慢慢回落;load 回落有滞后,不要看到 1 分钟值还高就以为没好;
- 如果存储短期恢复不了,最后的手段是
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 与 b | Linux 的 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 字段(进程睡眠所在的内核函数)。
一起聊聊