2024 年,我盯着一台服务器的监控,心里发慌:它的 load average 是 48。这是一台 8 核的机器,我一直记得一个说法——load 超过核数就说明过载了,48 对 8,这不是过载,这是要爆炸了。我赶紧上去用 top 看 CPU,准备抓那个吃满 CPU 的进程。可 top 一开,我愣住了:CPU 的 us(用户态)才 6%,sy(内核态)才 3%,id(空闲)高达 80% 多——CPU 闲得很。我当时整个人是懵的:load 48,CPU 却闲着 80%,这两个数字怎么可能同时成立?load 不就是 CPU 的繁忙程度吗?我把这两个数字反复看了好几遍,确认自己没看错,然后才意识到:我对 load average 这个词,从一开始就理解错了。我一直以为 load 衡量的是"CPU 有多忙",可它衡量的根本不是这个。我盯着 top 里那一个我平时从来不看的字段 wa 看了很久,它写着 11.0。这件事逼着我把 Linux 的 load average、运行队列、D 状态进程、iowait 这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,8 核服务器,一个 Java 服务 + 一些定时任务
事故现象:
- 监控告警:load average 飙到 48(8 核机器)
- ★ top 看 CPU:us 6% sy 3% id 80%+ —— CPU 闲得很
- 服务响应变慢,但 CPU 根本没吃满
现场排查:
# 1. 看 load 和 CPU
$ uptime
load average: 48.05, 45.20, 30.10 # ★ load 极高
$ top
%Cpu(s): 6.0 us, 3.0 sy, 0.0 ni, 80.2 id, 11.0 wa # ★ id 高 wa 也高
# 2. ★ load 这么高,到底是谁在排队
$ ps -eo state,pid,cmd | grep '^[RD]'
R 9400 java ...
D 9521 java ... # ★ 一堆 D 状态进程
D 9522 cp /data/big /backup/
D 9530 java ...
... # ★ D 状态的进程几十个
# 3. ★ 数一下 R 和 D 状态各有多少
$ ps -eo state | grep -c R
3 # ★ 真正在跑/可跑的才 3 个
$ ps -eo state | grep -c D
44 # ★ D 状态有 44 个!
# 4. ★ 看磁盘 IO 忙不忙
$ iostat -x 2
Device ... %util
vda ... 99.8 # ★ 磁盘 util 接近 100%,IO 打满了
根因(后来想清楚的):
1. ★ load average 不是"CPU 繁忙度"。它是【运行队列
长度】—— 同时处于 R(可运行)和 D(不可中断
睡眠)状态的进程数的平均值。
2. R 状态:在用 CPU 或排队等 CPU。
3. ★ D 状态:进程在等一件【无法被打断】的事 ——
绝大多数情况,就是在【等磁盘 IO】完成。
4. 我的机器:磁盘被一个大文件拷贝 + 一堆服务的读写
打满了,几十个进程全卡在"等磁盘"上 = 几十个 D。
5. ★ 几十个 D 状态进程,把 load 顶到了 48,可它们
全在睡觉等 IO,【根本没用 CPU】—— 所以 CPU 闲着。
load 高 ≠ CPU 忙。load 高也可能是【一堆进程在等磁盘】。
修复 1:load average 到底是什么——不是 CPU 繁忙度
# === ★ 先纠正最核心的误解:load 不等于 CPU 使用率 ===
# === 我以为的 load vs 真实的 load ===
# 我以为:load average 就是 CPU 的繁忙程度,load 是 8
# 就说明 8 个核都在满负荷转,load 48 就是严重过载。
# ★ 真相:load average 衡量的是【系统有多少进程在
# "等着被处理"】,它统计的是【运行队列的长度】,
# 不是 CPU 用了百分之多少。
# === ★ load 到底统计了哪些进程 ===
# Linux 的 load average,统计的是这两类进程的总数:
# - R 状态(Running / Runnable):正在用 CPU,
# 或者已经准备好、正排队等 CPU。
# - ★ D 状态(Uninterruptible Sleep):进程在等一件
# 【不可被中断】的事 —— 几乎总是【等磁盘 IO】。
# load = (R 的数量 + D 的数量) 的一段时间平均值。
# === ★ 关键:D 状态的进程,不用 CPU,却算进 load ===
# 一个 D 状态的进程,它在干嘛?它在【睡觉】,
# 躺在那等磁盘把数据读完/写完。它一点 CPU 都不占。
# ★ 但它【被算进 load】。所以会出现我看到的怪象:
# 几十个进程在等磁盘(几十个 D)-> load 几十,
# 可它们都没用 CPU -> CPU 显示空闲。
# load 高和 CPU 闲,在这种情况下【一点都不矛盾】。
# === 三个数字:1 分钟、5 分钟、15 分钟 ===
$ uptime
load average: 48.05, 45.20, 30.10
# ★ 三个数分别是【最近 1 / 5 / 15 分钟】的平均值。
# - 1 分钟 > 15 分钟:负载在【上升】(我这次就是)
# - 1 分钟 < 15 分钟:负载在【回落】
# 看趋势,比看单个数字有用。
# === ★ load 多高算高?要除以核数 ===
$ nproc
8
# ★ load 要对照【CPU 核数】看:
# - load ≈ 核数:每个核都有活干,刚好饱和。
# - load < 核数:有空闲。
# - load > 核数:有进程在排队。
# 但 ★ 排队的可能是【等 CPU】,也可能是【等磁盘】——
# 光看 load 这个数,分不清是哪种。得往下查。
# === 一句话认知 ===
# ★ load 高 = 有一堆进程"等着被处理"。
# 至于它们在等 CPU 还是等磁盘,load 自己不告诉你。
修复 2:R 状态 vs D 状态——两种"排队"
# === ★ load 高,先分清:是 R 多,还是 D 多 ===
# === 进程的几种状态 ===
# ps 的 STAT/S 列,常见值:
# - R:Running / Runnable —— 在用 CPU 或等 CPU。
# - ★ D:Uninterruptible Sleep —— 等 IO,不可中断。
# - S:Interruptible Sleep —— 普通睡眠(等事件),
# ★ 不算进 load。
# - T:停止。 Z:僵尸。
# ★ 只有 R 和 D 计入 load average。S 再多也不影响 load。
# === ★ 看当前 R 和 D 各有多少 ===
$ ps -eo state | grep -c R
3
$ ps -eo state | grep -c D
44
# ★ 这两个数,直接决定了你该往哪查:
# - R 多、D 少 -> 进程在抢 CPU -> 是【CPU 瓶颈】
# - ★ D 多 -> 进程在等磁盘 -> 是【IO 瓶颈】
# - R 和 D 都多 -> CPU 和 IO 一起紧张
# === 把 R 和 D 状态的进程具体列出来 ===
$ ps -eo state,pid,wchan:24,cmd | awk '$1=="R"||$1=="D"'
D 9521 io_schedule java ... # ★ wchan = io_schedule
D 9522 io_schedule cp /data/big ... # = 正卡在 IO 调度上
R 9400 - java ...
# ★ wchan 这一列,是进程"睡在内核哪个函数上"。
# D 状态进程的 wchan 多半带 io / blk / wait ——
# 一眼能确认它就是在等磁盘。
# === ★ 为什么 D 状态"不可中断" ===
# 进程发起一次磁盘读,内核让它睡下,等磁盘控制器
# 把数据搬完再叫醒它。这个等待【不能被信号打断】——
# 因为 IO 操作进行到一半,中途叫醒它会出乱子。
# ★ 所以 D 状态的进程,你 kill -9 都杀不掉(它要等
# 那次 IO 真正结束)。这也是 D 状态最磨人的地方。
# === 持续观察 R/D 数量 ===
$ for i in $(seq 10); do
echo "R=$(ps -eo state|grep -c R) D=$(ps -eo state|grep -c D)"
sleep 1
done
# ★ D 一直维持在几十 = IO 瓶颈坐实了。
修复 3:top 里的 wa(iowait)——CPU 在"空等磁盘"
# === ★ 看懂 top 那一行 CPU,尤其是 wa ===
# === top 的 CPU 那行,每个字段什么意思 ===
$ top
%Cpu(s): 6.0 us, 3.0 sy, 0.0 ni, 80.2 id, 11.0 wa, 0.0 hi, 0.0 si
# - us :用户态代码占的 CPU(你的程序在算东西)
# - sy :内核态占的 CPU(系统调用、内核在忙)
# - id :idle,CPU 空闲
# - ★ wa:iowait —— CPU 空闲,且【在等磁盘 IO 完成】
# - ni/hi/si:nice 进程 / 硬中断 / 软中断,通常很小
# === ★ wa(iowait)到底是什么 ===
# wa 不是"CPU 在干活"。wa 是一种【特殊的空闲】:
# CPU 没事干(本可以 idle),但系统里【有进程正卡在
# 等磁盘】—— 内核就把这段空闲记成 wa,而不是 id。
# ★ 所以 wa 高的本质是:CPU 闲着,但闲着不是因为
# 没活,而是因为活儿都【卡在磁盘那边出不来】。
# === ★ id 高 + wa 高,说明什么 ===
# 我这次:id 80% + wa 11%。
# - id 高:CPU 确实有大量空闲 -> 不是 CPU 瓶颈。
# - ★ wa 不为 0 且明显:有进程在等磁盘 -> IO 在拖后腿。
# ★ wa 是"load 高但 CPU 闲"这个谜题的【关键证据】:
# 它直接告诉你,瓶颈在磁盘 IO,不在 CPU。
# === wa 高 vs wa 低,怎么判断 ===
# - wa 持续在 10%、20% 以上:IO 压力明显,要查磁盘。
# - ★ wa 偶尔跳一下、平时接近 0:正常,别紧张。
# - wa 长期很高、且 D 状态进程一直多:IO 瓶颈实锤。
# === ★ 注意一个坑:wa 高 ≠ 一定是磁盘"坏了" ===
# wa 高只说明"有进程在等磁盘",可能是:
# - 磁盘本身慢/故障
# - ★ 也可能磁盘是好的,只是被【过量的 IO 请求】
# 打满了(我这次就是:大文件拷贝 + 服务读写挤一起)
# wa 高是"果",还要往下查这些 IO【是谁发出来的】。
# === 用 vmstat 也能看 ===
$ vmstat 2
r b ... wa
3 44 ... 11 # ★ b 列 = 等待 IO 被阻塞的进程数
# ★ vmstat 的 b 列,就是 D 状态进程数,和 wa 一起看。
修复 4:揪出是谁在打满磁盘 IO
# === ★ 确认是 IO 瓶颈后:定位 IO 是谁发的 ===
# === 第一步:确认磁盘到底有多忙 ===
$ iostat -x 2
Device r/s w/s rkB/s wkB/s await %util
vda 120 850 4800 210000 95.0 99.8
# ★ 重点看两个:
# - %util:磁盘有百分之多少时间在忙。接近 100 = 打满。
# - await:一次 IO 请求平均耗时(毫秒)。几十上百
# 毫秒 = 磁盘已经很吃力,IO 在排队。
# === ★ 第二步:用 iotop 看是哪个进程在狂读写 ===
$ iotop -oP
PID DISK READ DISK WRITE COMMAND
9522 0 B/s 198 M/s cp /data/big /backup/ # ★ 元凶!
9400 2 M/s 30 M/s java ...
# ★ iotop -oP:-o 只显示真的在 IO 的,-P 按进程聚合。
# 一眼看出谁是 IO 大户。我这次是一个 cp 大文件
# 的进程,瞬间把磁盘写带宽吃掉一大半。
# === 第三步:没有 iotop 时,用 /proc 看 ===
$ cat /proc/9522/io
read_bytes: 0
write_bytes: 21000000000 # ★ 这个进程累计写了 21G
# ★ /proc//io 里的 read_bytes / write_bytes,
# 是进程真正落到磁盘的读写量。隔几秒看增量,
# 增得最快的就是 IO 大户。
# === ★ 第四步:看 D 状态进程都在等什么 ===
$ ps -eo state,pid,cmd | awk '$1=="D"'
D 9521 java ... # 业务进程被 IO 拖住了
D 9530 java ...
D 9522 cp /data/big ... # ★ 始作俑者自己也在 D
# ★ 这里能看出"受害范围":一个 cp 把磁盘打满,
# 结果一堆无辜的 java 业务进程也跟着卡进 D 状态 ——
# IO 是【共享资源】,一个进程吃满,全员遭殃。
# === 第五步:确认是不是磁盘本身的问题 ===
$ dmesg -T | grep -iE 'error|i/o error|ata' | tail
# ★ 如果有 I/O error,可能是磁盘真坏了;
# 没有报错,那就是【IO 被打满】,不是磁盘故障。
# === ★ 小结:IO 排查三件套 ===
# iostat -x 看磁盘忙不忙(%util / await)
# iotop -oP 看 IO 是哪个进程发的
# ps D 状态 看哪些进程被 IO 拖住了
修复 5:正确解法——降 IO、隔离、对症
# === ★ 解法:先止血,再根治,别再盯着 CPU ===
# === ★ 第一步(止血):限制/暂停那个 IO 大户 ===
# 我这次的元凶是一个前台 cp 大文件。最快的止血:
$ kill -STOP 9522 # ★ 先暂停它,让磁盘喘口气
# 业务恢复后,再用【限速】的方式重新做这个拷贝:
$ ionice -c2 -n7 cp /data/big /backup/ # 调低 IO 优先级
# 或者直接用带限速的工具:
$ rsync --bwlimit=20000 /data/big /backup/ # 限到 20MB/s
# ★ 大文件拷贝、备份这类批量 IO,就该限速 / 降优先级,
# 不能让它和在线业务【平等地】抢磁盘。
# === ★ 第二步:用 ionice 给进程分 IO 优先级 ===
# ionice 之于磁盘 IO,就像 nice 之于 CPU。
$ ionice -c1 -p 9400 # class1 实时:在线业务给高优先级
$ ionice -c3 -p 9522 # class3 idle:批量任务只在空闲时 IO
# ★ class:1=实时 2=尽力(默认)3=空闲。
# 把备份/日志归档丢进 class3,它就只在没人用磁盘
# 时才动手,不会再挤占业务。
# === 第三步:根治——减少不必要的磁盘 IO ===
# - ★ 错峰:大文件拷贝、数据库备份、日志压缩,
# 挪到业务低峰期(深夜)跑,别和高峰挤。
# - 业务侧:检查是不是有进程在【疯狂写日志】,
# 或频繁刷盘 —— 该加缓冲的加缓冲,该降日志级别的降。
# - 读多:加内存做缓存,让热数据不落盘。
# === ★ 第四步:物理隔离 —— 别让备份和业务同盘 ===
# 根本的办法:让批量 IO 和业务 IO【不共享同一块盘】。
# - 备份目标盘,单独挂一块盘 / 一个独立的存储。
# - 数据库的数据盘和日志盘分开。
# ★ IO 是共享资源,最干净的隔离就是物理上分开。
# === 第五步:用 cgroup 给进程的 IO 设硬上限 ===
# 想彻底框住某个进程的 IO,用 cgroup v2 的 io 控制器:
# io.max 里写 "设备号 wbps=20971520" 限写带宽 20MB/s
# ★ systemd 服务可直接在 unit 里设 IOWriteBandwidthMax,
# 给它的磁盘写带宽一个不可逾越的天花板。
# === ★ 第六步:验证 ===
$ uptime # load 应明显回落
$ top # wa 回到接近 0
$ iostat -x 2 # %util 不再贴着 100
$ ps -eo state | grep -c D # D 状态进程数回到个位数
# ★ 四个一起回归正常,才算真修好了。
修复 6:load 排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ load average 不是 CPU 使用率 ===
# load = R 状态 + D 状态进程数的平均,D 不用 CPU 也算。
# === 2. ★ load 高,先分清是 R 多还是 D 多 ===
$ ps -eo state | grep -c R # R 多 = 抢 CPU
$ ps -eo state | grep -c D # D 多 = 等磁盘
# === 3. ★ top 的 wa(iowait)是关键证据 ===
# id 高 + wa 高 = CPU 闲着,但有进程卡在等磁盘。
# === 4. D 状态进程 kill -9 也杀不掉,它在等不可中断的 IO ===
# === 5. ★ IO 瓶颈用 iostat -x 看 %util 和 await ===
$ iostat -x 2 # %util 贴 100 = 磁盘打满
# === 6. ★ 用 iotop -oP 揪出 IO 大户 ===
$ iotop -oP # 谁在狂读写,一目了然
# === 7. 批量 IO 要限速/降优先级,别和在线业务平等抢盘 ===
$ ionice -c3 -p PID # 备份归档丢进 idle 优先级
# === 8. 排查"load 高"的步骤链 ===
$ uptime # ① load 多高,趋势升还是降
$ top # ② 看 us/sy/id/wa,wa 高吗
$ ps -eo state|grep -c D # ③ D 状态多不多
$ iostat -x 2 # ④ 磁盘是不是打满了
$ iotop -oP # ⑤ IO 大户是谁
$ ionice/错峰/隔离 # ⑥ 限速 + 错峰 + 物理隔离
# 按这个顺序,load 高基本能定位、能根治。
命令速查
需求 命令
=============================================================
看 load average uptime 或 cat /proc/loadavg
看 CPU 各项(含 wa) top 然后看 %Cpu(s) 那行
看 CPU 核数 nproc
数 R 状态进程 ps -eo state | grep -c R
数 D 状态进程 ps -eo state | grep -c D
列出 R/D 进程及 wchan ps -eo state,pid,wchan:24,cmd
看磁盘繁忙度和延迟 iostat -x 2
看 IO 大户进程 iotop -oP
看某进程累计读写量 cat /proc/PID/io
给进程调 IO 优先级 ionice -c3 -p PID
看是否有磁盘硬件报错 dmesg -T | grep -i 'i/o error'
口诀:load 不是 CPU 使用率,是 R+D 进程数;id 高 wa 高就是有进程在等磁盘
D 状态进程数多就查 IO,iostat 看 util,iotop 揪大户,批量 IO 要限速错峰
避坑清单
- load average 不是 CPU 使用率,它是运行队列长度,即 R 状态加 D 状态进程数的平均值
- D 状态进程在等磁盘 IO,完全不用 CPU,却照样算进 load,所以 load 高和 CPU 闲不矛盾
- load 多高算高要除以 CPU 核数,但排队的可能是等 CPU 也可能是等磁盘,光看 load 分不清
- load 高先用 ps 分清是 R 多还是 D 多,R 多是 CPU 瓶颈,D 多是 IO 瓶颈
- top 里的 wa 是 iowait,是一种特殊空闲,CPU 没活干是因为活儿卡在磁盘那边
- id 高加 wa 高是 load 高 CPU 闲这个谜题的关键证据,直接指向磁盘 IO 瓶颈
- D 状态进程不可中断,kill -9 都杀不掉,因为它必须等那次 IO 真正结束
- IO 瓶颈用 iostat -x 看 %util 和 await,用 iotop -oP 揪出狂读写的 IO 大户
- 磁盘 IO 是共享资源,一个进程打满磁盘会拖垮一堆无辜业务进程一起卡进 D 状态
- 大文件拷贝备份等批量 IO 要用 ionice 降优先级或限速,错峰跑,最好物理隔离不同盘
总结
这次"load 飙到 48、CPU 却闲着 80%"的事故,纠正了我一个埋得很深、深到我从来没意识到它是个"理解"、而一直把它当"常识"的误解。在我的脑子里,load average 这个东西的意思,简单得不能再简单:它就是 CPU 的繁忙程度。load 是 1,就是一个核满载;load 是 8,就是 8 个核都转满了;load 是 48,那就是远远超出了这台 8 核机器的能力,严重过载。这个理解如此地顺理成章,以至于我从来没有停下来想过它对不对——我把 load 和 CPU 使用率,在心里画上了一个等号。正因为有这个等号,所以当监控告诉我 load 是 48 的时候,我的全部反应,都是冲着 CPU 去的:我打开 top,我的眼睛直直地盯着 CPU 那一行,我准备抓那个把 CPU 吃满的罪魁祸首。可 top 告诉我的事实,把我那个等号砸得粉碎:CPU 的空闲率高达 80%。在我那个"load 等于 CPU 繁忙度"的世界观里,这是一个彻头彻尾的悖论,一个不可能成立的事实——一个东西怎么可能既"忙到 48"又"闲着 80%"?我对着这个悖论懵了很久,因为我没有任何工具去理解它,我的认知模型里根本没有给这个现象留位置。复盘到根上,我才明白,我错就错在那个等号上。load average 衡量的,从来都不是"CPU 有多忙",而是"有多少进程在等着被处理"——它统计的是一个队列的长度。而站在这个队列里的,不只有"在等 CPU 的进程",还有另一种我以前压根不知道它存在的进程:它们处在一种叫 D 的状态里,它们在等待一件无法被打断的事情完成,而这件事,几乎总是磁盘 IO。这种 D 状态的进程,它此刻在做什么?它什么也没做,它在睡觉,它安安静静地躺在那里,等着磁盘把数据搬运完毕。它一丝一毫的 CPU 都没有占用。可是,它被算进了 load。这就是那个悖论的全部答案:我的机器上,一个拷贝大文件的进程把磁盘带宽吃干抹净,几十个无辜的业务进程想读写磁盘,却全都被堵在了门外,一个接一个地陷入 D 状态,睡死在那里等磁盘。这几十个沉睡的、不耗 CPU 的进程,把 load 这个"队列长度"顶到了 48;而因为它们谁都没在用 CPU,CPU 自然就闲着——它不是没活干,它是被告知"活儿都卡在磁盘那边,你等着吧",于是 top 用一个我从来不看的字段 wa,默默地、忠实地记录下了这种"被磁盘拖累的空闲"。load 高和 CPU 闲,在真相面前,不仅不矛盾,它们根本就是同一件事的一体两面。这次最大的收获,不是我学会了 iostat、iotop 这几个命令,而是我看清了一件更让我后背发凉的事:最危险的错误认知,不是那些我"知道自己不懂"的东西,而是那些我"以为自己懂、并且从未怀疑过"的东西。我对很多复杂的技术心怀敬畏,会去查文档、会去验证;可恰恰是 load average 这种我"一眼就懂"的、最最基础的概念,我连一秒钟的怀疑都没有给过它。那个错误的等号,在我脑子里大概安安静静地待了好几年,从来没有被检验过,直到它在一个真实的事故现场,把我引向了完全错误的方向,让我对着 CPU 白白排查了半天。所以下一次,当一个监控数字和我的预期发生剧烈冲突、冲突到像是个"悖论"的时候,我会强迫自己,先不要去怀疑那个数字、不要去怀疑那台机器,而是回过头来,极其严肃地、像第一次见到它一样,去盘问我自己:我用来理解这个数字的那个定义,到底从哪来的?它是我真的读过、验证过、想明白了的,还是仅仅因为它听起来太顺、太像常识,我就想当然地接受了、并且再也没回头看过一眼?一个监控指标,只有当你真正、精确地知道它在数的是什么的时候,它才是你的眼睛;否则,它只是一个会把你领进沟里的、你以为你看得懂的数字。
—— 别看了 · 2026