2024 年的一个下午,监控告警把我从工位上炸了起来:一台服务器的 load average 冲到了 40,服务响应慢得像是卡死了。40 这个数字,在我当时的认知里就等于"CPU 被榨干了"——这台机器是 8 核的,load 40 意味着 CPU 严重过载、有一大堆任务在排队抢 CPU。我深吸一口气登上机器,准备去抓那个吃满 CPU 的罪魁祸首,top 一打开,我整个人都懵了:CPU 那一行,id(空闲)赫然写着 90% 以上,8 个核基本都在闲着晒太阳。一台 CPU 几乎全空闲的机器,load average 怎么会是 40?这两个数字摆在一起,像是在公然嘲笑我对 Linux 负载的全部理解。这件事逼着我把 Linux 的 load average、CPU 使用率、进程状态、iowait 这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,8 核,一个读写都很重的服务
事故现象:
- load average 冲到 40,服务响应极慢
- 但 top 里 CPU 的 idle 高达 90%+,CPU 几乎是空闲的
- load 高 + CPU 闲,两个数字完全对不上
现场排查:
# 1. 看负载和 CPU
$ uptime
15:30:01 up 60 days, load average: 40.12, 38.50, 35.20
$ top
top - 15:30:05 load average: 40.12, 38.50, 35.20
%Cpu(s): 3.2 us, 1.5 sy, 0.0 ni, 90.1 id, 5.0 wa, ...
# ^^^^^^ ^^^^^^^ id 90%(空闲)
# us 才 3% —— CPU 真的在闲着 ^^^ wa 5%
# 2. ★ 看进程状态 —— 大量进程卡在 D 状态
$ ps -eo pid,stat,comm | awk '$2 ~ /D/'
8123 D myapp
8124 D myapp
8125 D myapp
... (几十个) # ★ 一大片 D 状态进程
# 3. D 是"不可中断睡眠",通常在等 IO。看磁盘
$ iostat -x 1 3
Device r/s w/s %util await
vda 12.0 980.0 99.8 350.2
# ^^^^^ 磁盘几乎 100% 忙 ^^^^^ 等待 350ms
根因(后来想清楚的):
1. ★ load average 算的【不只是】抢 CPU 的进程,
它还把【处于 D 状态(不可中断睡眠)】的进程也算进去。
2. 这台机器磁盘 IO 被打满了(%util 99.8%),
几十个进程全卡在"等磁盘返回"上,进入 D 状态。
3. 这些 D 状态进程,不消耗 CPU(所以 CPU idle 很高),
但它们【全部计入 load average】 -> load 飙到 40。
4. ★ 所以 load 高、CPU 闲,一点都不矛盾 ——
这恰恰是【磁盘 IO 瓶颈】的典型特征。
5. 真正该排查的不是 CPU,是磁盘:谁在狂写磁盘。
load average 不等于 CPU 使用率,它是个更宽的概念。
修复 1:读懂 top 第一屏——每个数字什么意思
# === top 打开后,最该看的是【最上面那几行】 ===
$ top
top - 15:30:05 up 60 days, load average: 40.12, 38.50, 35.20
Tasks: 320 total, 2 running, 280 sleeping, 38 stopped
%Cpu(s): 3.2 us, 1.5 sy, 0.0 ni, 90.1 id, 5.0 wa, 0.0 hi, 0.2 si, 0.0 st
MiB Mem : 16000 total, 1200 free, 9000 used, 5800 buff/cache
# === ★ load average 三个数:1 分钟、5 分钟、15 分钟均值 ===
# 看三个数的【趋势】比看绝对值更有意义:
# 1分 > 5分 > 15分 -> 负载正在【上升】(恶化中)
# 1分 < 5分 < 15分 -> 负载正在【下降】(在恢复)
# 这次 40 > 38 > 35,说明问题还在持续恶化。
# === ★ %Cpu(s) 那一行,每个字母什么意思(排查核心)===
# us 用户态:跑应用代码花的 CPU —— 业务计算重就高
# sy 内核态:跑系统调用花的 CPU —— 频繁 IO/上下文切换会高
# ni 跑 nice 值调过的低优先级进程
# id ★ 空闲 —— 这次它 90%,CPU 根本没忙
# wa ★ iowait —— CPU 空着,在【干等磁盘 IO 返回】
# hi/si 处理硬中断 / 软中断(si 高常和网络收包有关)
# st 被虚拟化层"偷"走的 CPU(云主机上要留意)
# === ★ 这次的关键解读 ===
# us 只有 3%,id 高达 90% —— CPU 没有过载,这是铁证。
# wa 有 5%,且配合后面看到的磁盘 %util ——
# 矛头清清楚楚指向【磁盘 IO】,而不是 CPU。
# === top 里几个实用快捷键 ===
# 按 1 :把"所有核的平均"展开成【每个核】单独显示
# 按 P :按 CPU 使用率排序 按 M:按内存排序
# 按 H :★ 切换到【线程】视图(下面排查热点线程要用)
# 按 x :高亮当前排序列
修复 2:load average 到底算的是什么——这次的认知盲区
# === ★ 这次最大的误解:load average ≠ CPU 使用率 ===
# 我一直以为 load = "有多少进程在抢 CPU"。错了一半。
# === load average 的真实定义 ===
# 它统计的是【处于以下两种状态的进程】数量的滑动平均:
# 1. R 状态:正在 CPU 上跑、或在【运行队列里等 CPU】
# 2. ★ D 状态:不可中断睡眠 —— 绝大多数情况是在【等磁盘 IO】
# ★ 关键就在第 2 点:D 状态进程【不消耗 CPU】,
# 却【计入 load】。这就是 load 高、CPU 闲的全部秘密。
# === 怎么"读"load average 这个数 ===
# 经验法则:load 和 CPU 核数比着看。
# load ≈ 核数 -> 满负荷,但还行
# load >> 核数 -> 过载,有任务在排队
# 这台 8 核,load 40,看起来是"过载 5 倍"——
# ★ 但前提是"过载"指 CPU。这次 CPU 没过载,
# 是几十个进程在【排队等磁盘】,把 load 撑起来了。
# === ★ load 高时,第一步要分清:是等 CPU,还是等 IO ===
# 看 top 的 CPU 行:
# us / sy 高、id 低 -> 真的是 CPU 忙 -> 查 CPU 热点(修复 3)
# id 高、但 wa 高 -> CPU 在等 IO -> 查磁盘(修复 4)
# id 高、wa 也不高 -> 查有没有大量 D 状态进程(修复 5)
# === 数一下当前 R 和 D 状态的进程各有多少 ===
$ ps -eo stat | grep -c '^R' # 在跑/等 CPU 的
$ ps -eo stat | grep -c '^D' # ★ 卡在 IO 上的
# D 的数量很大 -> load 高的元凶就是它们,病根在 IO。
# === vmstat:一眼看清"等 CPU"还是"等 IO" ===
$ 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
2 38 0 120000 ... 120 98000 3 2 90 5
# ★ r 列:等 CPU 的进程数 b 列:★ 等 IO 阻塞的进程数
# 这次 b=38 而 r=2 —— 38 个进程在等 IO,根本不是 CPU 问题。
修复 3:CPU 真高时——怎么揪出热点线程
# === ★ 这一节是"CPU 真的被打满"时的标准打法 ===
# (这次不是,但排查负载,这套必须会)
# === 第一步:top 找出吃 CPU 的进程 ===
$ top # 按 P 排序,看 %CPU 最高的进程
# 假设 PID 8123 的 CPU 占了 700%(8 核机器,最高 800%)
# === ★ 第二步:top -H 钻进进程内部,找热点【线程】 ===
$ top -H -p 8123
# -H 让 top 显示线程。看哪个【线程】CPU 最高。
# 假设热点线程 TID = 8200。
# === ★ 第三步:线程号转 16 进制(对接 jstack 的关键)===
$ printf '%x\n' 8200
2008
# Java 的 jstack 里,线程 nid 是 16 进制的 —— 必须转。
# === 第四步:抓 Java 线程栈,定位到代码 ===
$ jstack 8123 | grep -A 30 'nid=0x2008'
# ★ nid=0x2008 那一段栈,就是这个热点线程正在跑的代码 ——
# 一眼能看出它卡在哪个方法、是不是死循环。
# 建议隔几秒抓一次、抓 3~5 次对比:
# 每次都停在同一处 -> 死循环 / 热点代码实锤。
# === 非 Java 程序:用 perf 找 CPU 热点 ===
$ perf top -p 8123 # 实时看哪些函数最耗 CPU
$ perf record -p 8123 -- sleep 30 && perf report # 采样后看报告
# === pidstat:按进程/线程持续观察 CPU ===
$ pidstat -u 1 # 每秒刷新各进程 CPU
$ pidstat -t -p 8123 1 # -t 看该进程下各线程的 CPU
# === ★ CPU sy(内核态)特别高,换个思路 ===
# us 高 -> 查应用代码;sy 高 -> 查系统调用:
$ pidstat -w 1 # 看上下文切换次数(cswch/nvcswch)
$ vmstat 1 # cs 列:上下文切换;过高拖垮 CPU
# 频繁创建销毁线程、锁竞争激烈,都会让 sy 和 cs 飙高。
修复 4:iowait 高——磁盘 IO 瓶颈(这次的根因)
# === ★ wa(iowait)高 + 大量 D 进程 = 磁盘 IO 瓶颈 ===
# 这次就走到了这一步。主角是 iostat。
# === iostat -x:看每块磁盘的详细负载 ===
$ iostat -x 1 3
Device r/s w/s rkB/s wkB/s await r_await w_await %util
vda 12.0 980.0 480.0 410000 350.2 8.0 354.0 99.8
# ★ 排查磁盘,死死盯住这几列:
# %util ★ 磁盘繁忙百分比 —— 接近 100% 就是【磁盘打满了】
# await ★ 每个 IO 平均耗时(ms)—— 正常几 ms,这次 350ms!
# w/s 每秒写次数 r/s 每秒读次数
# wkB/s 每秒写入的数据量
# 这次:%util 99.8% + await 350ms + w/s 980 ——
# 磁盘被【写】操作彻底压垮了。
# === ★ 第二步:揪出是哪个进程在狂写磁盘 ===
$ iotop -oP
# -o 只显示真正在做 IO 的 -P 按进程聚合
# Total DISK WRITE 那一行看总量,下面列出各进程的读写速率 ——
# 哪个进程的 DISK WRITE 最高,它就是元凶。
# === pidstat 也能看进程级 IO ===
$ pidstat -d 1
PID kB_rd/s kB_wr/s Command
8123 0.0 395000.0 myapp # ★ myapp 每秒写近 400MB
# === 定位到进程后,再看它在写什么文件 ===
$ ls -l /proc/8123/fd | grep -v socket # 它打开的文件
$ lsof -p 8123 | grep -E 'REG.*w'
# ★ 这次发现:某个 debug 日志开关被误开,
# 服务在疯狂往一个日志文件里写 —— 关掉开关,IO 立刻回落。
# === 几种典型 IO 瓶颈成因 ===
# - 日志打太猛(debug 日志、大量异常栈)
# - 大查询导致数据库疯狂读盘 / 落临时文件
# - 没加索引的全表扫描
# - swap 在疯狂换入换出(看 vmstat 的 si/so)
# - 备份 / 大文件拷贝挤占了磁盘带宽
修复 5:进程状态——R / S / D / Z / T 各是什么
# === ★ ps 的 STAT 列,是排查负载绕不开的一课 ===
$ ps -eo pid,stat,comm
# STAT 第一个字母,就是进程状态:
# R Running/Runnable —— 正在 CPU 上跑,或在运行队列里等 CPU
# S Sleeping —— 可中断睡眠,在等某个事件(最常见、正常)
# D ★ Uninterruptible Sleep —— 不可中断睡眠,通常在等磁盘 IO
# ★ D 状态的进程:kill -9 都杀不掉!只能等 IO 完成。
# 大量 D 进程 = IO 出问题了。
# Z Zombie —— 僵尸进程:已结束,但父进程没回收它
# T Stopped —— 被暂停(收到 STOP 信号 / 在被调试)
# === 后面还可能跟修饰符 ===
# s 是会话首进程 l 多线程 + 在前台进程组 N 低优先级
# === ★ 揪出所有 D 状态进程(IO 排查第一步)===
$ ps -eo pid,stat,wchan,comm | awk '$2 ~ /D/'
# wchan 列:进程正卡在内核的哪个函数里等 —— 能提示在等什么。
# === ★ 看一个 D 进程具体卡在哪 ===
$ cat /proc/8123/stack # 它当前的内核调用栈
$ cat /proc/8123/wchan # 它在等的内核函数
# 大多会看到 IO 相关的函数,印证"在等磁盘"。
# === 僵尸进程 Z:本身不占资源,但占着进程号 ===
$ ps -eo pid,ppid,stat,comm | awk '$3 ~ /Z/'
# ★ 僵尸的根源在它【父进程】没调 wait() 回收。
# 解法是处理父进程(让它回收,或重启父进程)——
# 你 kill 僵尸本身是没用的,它已经死了。
# === ★ D 状态进程为什么 kill -9 也杀不掉 ===
# 进程在 D 状态时,正陷在内核里等一个【不可被信号打断】的操作
#(典型就是磁盘 IO)。信号要等它从内核返回用户态才能被处理,
# 而它返回不了 —— 所以 kill -9 也得排队等 IO 结束。
# ★ 结论:看到一堆杀不掉的 D 进程,别跟它较劲,去修 IO。
修复 6:负载与 CPU 排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ load 高,先分清是等 CPU 还是等 IO ===
$ top # 看 CPU 行:id 低是 CPU 忙,wa 高是等 IO
$ vmstat 1 # r 列大=等 CPU,b 列大=等 IO
# load average ≠ CPU 使用率,别一看 load 高就扑向 CPU。
# === 2. ★ load 高但 CPU 空闲,九成是磁盘 IO ===
$ ps -eo stat | grep -c '^D' # D 状态进程一大堆 -> 实锤 IO 问题
$ iostat -x 1 # %util 接近 100、await 很大 -> 磁盘打满
# === 3. CPU 真高,要钻到【线程】粒度 ===
$ top -H -p PID # 找热点线程
$ printf '%x\n' TID # 转 16 进制,对接 jstack
# 别停在"进程 CPU 高",要定位到具体哪个线程、哪段代码。
# === 4. 分清 us 高还是 sy 高 ===
# us 高 -> 查应用代码(死循环、重计算)
# sy 高 -> 查系统调用、上下文切换、锁竞争
# === 5. 大量 D 进程别硬 kill,去修 IO 源头 ===
# D 进程 kill -9 也杀不掉,它在等 IO。
# 用 iotop 找出狂做 IO 的进程,从源头治。
# === 6. load 看趋势:三个数递增是恶化,递减是恢复 ===
# 1分 > 5分 > 15分 还在变坏;反过来在变好。
# === 7. 排查负载的命令链 ===
$ uptime # ① load 多高、趋势如何
$ top # ② CPU 各项占比,是忙还是等
$ vmstat 1 # ③ r/b 列:等 CPU 还是等 IO
$ iostat -x 1 # ④ 磁盘 %util / await
$ iotop -oP # ⑤ 谁在狂做 IO
$ top -H -p PID / jstack # ⑥ CPU 真高时定位热点线程
# 按这个顺序,负载问题基本能定位。
命令速查
需求 命令
=============================================================
看 load average 和趋势 uptime
看 CPU 各项占比 top(看 %Cpu 行)
每核单独看 CPU top 里按 1
看进程内各线程的 CPU top -H -p PID
线程号转 16 进制 printf '%x\n' TID
看是等 CPU 还是等 IO vmstat 1(r 列 vs b 列)
看磁盘繁忙度和 IO 延迟 iostat -x 1
揪出狂做 IO 的进程 iotop -oP
看进程级 IO 速率 pidstat -d 1
找出所有 D 状态进程 ps -eo pid,stat,comm | awk '$2 ~ /D/'
口诀:load 高先分清等 CPU 还是等 IO -> CPU 空闲就查磁盘
load 算上了 D 状态进程 -> 大量 D 进程就是 IO 瓶颈
避坑清单
- load average 不等于 CPU 使用率,它还把 D 状态进程算了进去
- load 高但 CPU idle 高,不矛盾,这恰是磁盘 IO 瓶颈的典型特征
- top 的 CPU 行:id 低是 CPU 忙,wa 高是 CPU 在干等磁盘 IO
- vmstat 的 r 列是等 CPU 的进程,b 列是等 IO 阻塞的进程
- CPU 真高要钻到线程粒度,top -H 找热点线程再 jstack 定位
- jstack 里线程 nid 是 16 进制,要用 printf '%x' 转换
- us 高查应用代码,sy 高查系统调用和上下文切换、锁竞争
- D 状态进程 kill -9 也杀不掉,它在等 IO,要修 IO 源头
- iostat 看磁盘要盯 %util 和 await,接近 100% 和延迟大就是打满
- load 看三个数趋势:1分大于5分大于15分是恶化,反之在恢复
总结
这次"load 飙到 40 但 CPU 几乎空闲"的事故,纠正了我一个流传极广、我也深信不疑的认知:我一直把 load average 直接等同于"CPU 的繁忙程度"——在我脑子里,load 这个数字,就是"此刻有多少个任务在抢 CPU";load 是 40,而机器是 8 核,那意味着 CPU 严重过载、有五倍于处理能力的任务在排队。正是这个根深蒂固的等式,让我在登上机器、看到 top 里 CPU 的空闲率高达 90% 时,陷入了彻底的认知崩塌——一台 CPU 闲得发慌的机器,load 怎么可能是 40?在我那个"load 就是 CPU 负载"的世界观里,这是一个绝不可能出现的画面。复盘到根上,我才终于弄清楚了 load average 真正的、完整的定义。它统计的,从来都不只是"正在运行或等待 CPU 的进程"(也就是 R 状态的进程);它的统计口径里,还包含了另一类我从前完全忽略的进程——处于 D 状态、也就是"不可中断睡眠"状态的进程。而一个进程会进入 D 状态,最典型、最普遍的原因,就是它发起了一次磁盘 IO 操作,然后陷在内核里,死死地等着磁盘把数据返回来。这就是整个谜题的钥匙:D 状态的进程,它【不消耗任何 CPU】——它只是在等,所以 top 里的 CPU 空闲率才会那么高;但它【会被完整地计入 load average】。我那台机器当时的真实处境是:它的磁盘 IO 被彻底打满了,iostat 显示磁盘的 %util 高达 99.8%,每一次 IO 的平均等待时间长达 350 毫秒;于是几十个服务进程,全部发起了磁盘写操作,然后全部卡死在 D 状态,排着长队等那块不堪重负的磁盘。这几十个一动不动、不烧一点 CPU 的进程,把 load average 这个数字,生生顶到了 40。所以"load 高、CPU 闲"这件看似自相矛盾的事,不仅不矛盾,它本身就是一个极其清晰、极其经典的诊断信号——它在向你大喊:别看 CPU 了,你的瓶颈在磁盘 IO。想通这一层,正确的排查路径瞬间就清晰了:面对一个高 load,我要做的第一件事,不再是急着去 top 里抓 CPU 大户,而是先冷静地分清——这个 load,究竟是由"等 CPU"的进程撑起来的,还是由"等 IO"的进程撑起来的。top 的 CPU 那一行就能给出答案:如果是 us 和 sy 高、id 低,那是 CPU 真的忙,我该顺着 top -H 钻到线程粒度去抓热点代码;如果是 id 很高、却伴随着 wa(iowait)不低,或者干脆是一大片 D 状态的进程,那病根就在磁盘,我该掉头去用 iostat 看磁盘的 %util 和 await、用 iotop 揪出那个在疯狂读写磁盘的进程。这次的结局也确实如此:真凶是一个被误开的 debug 日志开关,让服务在疯狂地往日志文件里灌数据,把磁盘活活压垮——关掉那个开关,磁盘 IO 立刻回落,那 40 的 load,也随之烟消云散。这次从一个"不可能"的监控画面出发,我最大的收获,是亲手拆掉了脑子里"load = CPU 负载"那个过度简化的等式,换上了一个更准确的理解:load average 是一个比 CPU 使用率宽得多的概念,它衡量的是"系统里有多少进程在被某种东西卡住而无法推进",而卡住它们的,既可能是稀缺的 CPU,也可能是一块跟不上趟的磁盘。读懂了这一点,一个高 load 就不再是一句含糊的"系统很忙",而变成了一个需要你进一步追问"忙在哪里"的、信息量丰富的起点。
—— 别看了 · 2026