2024 年,一台跑得好好的服务器,某天开始变得又慢又卡——上去敲个命令都要顿一下,服务响应肉眼可见地拖沓。我条件反射地敲了 top,准备抓那个吃满 CPU 的罪魁祸首。可 top 的画面让我愣住了:CPU 使用率不高,几个核都闲着,可那个 load average,赫然是 38——这台机器只有 8 个核。一边是悠闲的 CPU,一边是高得吓人的负载,这两个数字像是来自两台不同的机器,凑在一张屏幕上互相打脸。我一直以为 load average 高,就是 CPU 忙不过来了,可眼前的 CPU 分明没忙。这个矛盾逼着我去重新搞懂一件我自以为早就懂了的事:load average 这个数字,到底是用什么算出来的。这件事逼着我把 Linux 的负载、iowait、磁盘 IO 排查这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,8 核服务器,跑一个有大量读写的服务
事故现象:
- 服务器整体又慢又卡,敲命令都卡顿
- top 看 CPU 使用率不高,核都挺闲
- ★ load average 高达 38(机器才 8 核)
- CPU 闲 + load 爆表,两个数字自相矛盾
现场排查:
# 1. top 看一眼:CPU 闲,load 却爆表
$ top
load average: 38.5, 36.2, 30.1 # ★ 8 核机器,load 38
%Cpu(s): 5.0 us, 3.0 sy, 0.0 ni, 12.0 id, 80.0 wa
# ^^^^^^^ ★ wa 80%!
# 2. ★ %wa(iowait)高达 80% —— 关键线索
# CPU 不是在干活,是在【干等磁盘 IO】
# 3. 看是不是有进程卡在 D 状态(不可中断睡眠)
$ ps -eo state,pid,comm | grep '^D'
D 9001 java
D 9005 java
D 9210 mysqld # ★ 一堆 D 状态进程
# ★ D 状态 = 进程在等 IO,卡死动不了
# 4. ★ iostat 看磁盘到底忙成什么样
$ iostat -x 1 3
Device ... r/s w/s await %util
vda ... 1200 3400 220.5 99.8 # ★ %util 99.8%!await 220ms!
# ★ 磁盘利用率打满 100%,单次 IO 平均要等 220 毫秒
根因(后来想清楚的):
1. ★ load average 算的不只是"等 CPU 的进程",
它还算上了"等磁盘 IO 而卡在 D 状态的进程"。
2. ★ 所以 load 高有【两种】原因:CPU 不够用,
或者 —— IO 太慢,一堆进程在排队等磁盘。
3. 这次是后者:磁盘 %util 打到 100%、await 220ms,
磁盘已经是瓶颈,大量进程卡在 D 状态等它。
4. CPU 闲,是因为它无事可做 —— 它在 iowait,
在干等那个慢吞吞的磁盘把数据吐出来。
5. ★ 我盯着 CPU 找凶手,方向全错:凶手是磁盘 IO。
load 高 + CPU 闲 + wa 高,几乎一定是磁盘 IO 瓶颈。
修复 1:load average 到底算了什么——不只是 CPU
# === ★ 先纠正最核心的误解:load average ≠ CPU 使用率 ===
# === load average 是什么 ===
$ uptime
... load average: 38.5, 36.2, 30.1
# 1分钟 5分钟 15分钟
# load average = 系统在过去一段时间里,
# 平均有多少个进程【处于"想干活"的状态】。
# === ★ 关键:"想干活"包含【两种】进程 ===
# 1. 正在用 CPU、或在排队等 CPU 的进程(R 状态)
# 2. ★ 卡在【不可中断睡眠】等 IO 的进程(D 状态)
# ★ 第 2 种,正是大多数人(包括过去的我)漏掉的!
# 在 Linux 里,等磁盘 IO 等到卡死的进程,
# 也被算进 load —— 它们也是"系统的负担"。
# === ★ 于是 load 高,有两种完全不同的病 ===
# 病 A:CPU 不够用 -> 一堆进程在排队抢 CPU
# 特征:load 高,且 CPU 使用率(us+sy)也高
# 病 B:★ IO 太慢 -> 一堆进程卡在 D 状态等磁盘
# 特征:load 高,但 CPU 使用率低、iowait 高
# ★ 这次就是病 B。光看 load 数字,分不出是哪种病。
# === 怎么快速分辨是哪种病 ===
$ top
%Cpu(s): 5.0 us, 3.0 sy, ..., 12.0 id, 80.0 wa
# us+sy 高、wa 低 -> 病 A,CPU 瓶颈
# us+sy 低、wa 高 -> ★ 病 B,IO 瓶颈(这次)
# ★ load 是"有多少进程在等",至于在等 CPU 还是等磁盘,
# 要靠 us/sy/wa 这几个百分比来区分。
# === load 多高算高 ===
# 粗略基准:load 持续 > CPU 核数,就说明系统在过载。
# 8 核机器 load 38 -> 严重过载,平均每核背着近 5 个进程。
修复 2:iowait 与 D 状态——CPU 在"干等"的证据
# === ★ %wa(iowait):CPU 闲,但闲得"不情愿" ===
$ top
%Cpu(s): 5.0 us, 3.0 sy, 0.0 ni, 12.0 id, 80.0 wa, ...
# us 用户态 CPU sy 内核态 CPU
# id ★ 真正空闲(无事可做)
# wa ★ iowait:CPU 没活干,但【是因为在等 IO 完成】
# ★ id 和 wa 都表示"CPU 没在算",但意义天差地别:
# id 高 = 系统真闲;wa 高 = 系统被磁盘拖住了,
# CPU 想干活却干不了,只能干等磁盘。
# === ★ iowait 高,本质是"磁盘喂不饱 CPU" ===
# 进程要读数据 -> 数据在磁盘上 -> 磁盘慢吞吞地找 ->
# 这期间进程只能睡着等 -> CPU 也跟着没活干 -> 计入 wa。
# wa 高 = 你的瓶颈在磁盘,不在 CPU。加 CPU 没用。
# === ★ D 状态进程:被 IO 卡死的进程 ===
$ ps -eo state,pid,ppid,comm | awk '$1=="D"'
D 9001 1 java
D 9210 1 mysqld
# 进程状态(state)的几个值:
# R 运行中 / 可运行(在用或在等 CPU)
# S 可中断睡眠(正常等事件,如等网络)
# D ★ 不可中断睡眠 —— 通常就是【在等磁盘 IO】
# Z 僵尸进程
# ★ D 状态的进程,连 kill -9 都杀不动 —— 它在等内核
# 把 IO 做完,谁也打断不了。大量 D 状态 = IO 出大问题。
# === 持续盯 D 状态进程的数量 ===
$ watch -n1 "ps -eo state | grep -c '^D'"
# 这个数字一直很高 -> 系统持续被 IO 卡住。
# === ★ 把这次的链路串起来 ===
# 磁盘慢 -> 进程读写时卡在 D 状态 -> D 状态进程计入 load
# -> load 飙高;同时 CPU 在等磁盘 -> iowait 飙高、
# CPU 使用率却低。
# load 38 + CPU 闲 + wa 80%,讲的是【同一个】故事:磁盘。
修复 3:iostat——看磁盘到底忙不忙
# === ★ 确认是 IO 问题后,用 iostat 量化磁盘有多忙 ===
# iostat 来自 sysstat 包:yum install sysstat
# === 看磁盘的详细 IO 指标 ===
$ iostat -x 1 3
# -x 显示扩展指标 1 每秒刷新 3 共采 3 次
Device r/s w/s rkB/s wkB/s await r_await w_await %util
vda 1200 3400 48000 136000 220.5 180.2 235.1 99.8
# === ★ 这几个字段,每一个都是排查关键 ===
# r/s, w/s 每秒读 / 写多少次 IO(IOPS)
# rkB/s,wkB/s 每秒读 / 写多少 KB(吞吐量)
# ★ await 单次 IO 平均耗时(毫秒)——【从发起到完成】
# 普通 SSD 个位数 ms,机械盘十几 ms。
# 220ms = 磁盘已经被压垮,IO 在严重排队。
# ★ %util 磁盘繁忙度 —— 这块盘有多少时间在处理 IO。
# 接近 100% = 磁盘【满负荷】,这就是瓶颈。
# === ★ 怎么判读 ===
# %util 接近 100% + await 远高于正常 -> 磁盘是瓶颈,实锤。
# %util 不高 -> 磁盘没满,瓶颈在别处。
# (注意:%util 对 SSD/多队列盘会有点失真,要和 await 一起看)
# === 看每个磁盘分区/整体 IO 概览 ===
$ iostat # 不带 -x,看简要
$ iostat -x 2 # 每 2 秒持续刷,观察趋势
# === ★ 还要看是不是"内存不够导致的 swap IO" ===
$ vmstat 1 5
procs ---memory--- ---swap-- ---io--- -system- ----cpu----
r b ... si so bi bo ... us sy id wa
2 35 ... 120 340 ... ... 80
# ★ b 列 = 阻塞(等 IO)的进程数,这里 35,很高。
# ★ si/so = swap in/out,如果不为 0 -> 内存不足在
# 疯狂换页,这本身就会制造大量磁盘 IO!
# 排查 IO 高,别忘了回头看一眼是不是内存不足引起的。
修复 4:揪出是哪个进程在狂读写——iotop 与 pidstat
# === ★ 确认磁盘是瓶颈后,要找出"是谁在狂读写" ===
# === iotop:像 top 一样,但看的是 IO ===
$ iotop -o
# -o 只显示【真正在产生 IO】的进程
Total DISK READ: 45 M/s | Total DISK WRITE: 130 M/s
TID PRIO USER DISK READ DISK WRITE COMMAND
9210 be/4 mysql 2 M/s 88 M/s mysqld # ★ 写大户
9001 be/4 app 40 M/s 5 M/s java # ★ 读大户
# ★ 一眼看出 mysqld 在狂写、java 在狂读 —— 锁定嫌疑进程。
# iotop 来自 iotop 包,需要 root 跑。
# === ★ pidstat:统计每个进程的 IO(不用装 iotop 时)===
$ pidstat -d 1 5
# -d 看磁盘 IO 1 每秒 5 次
UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
27 9210 2048 90112 0 mysqld
1000 9001 40960 5120 0 java
# kB_rd/s 每秒读 kB_wr/s 每秒写 —— 和 iotop 信息类似。
# === 看某个具体进程的 IO 明细 ===
$ pidstat -d -p 9210 1
# 持续观察 mysqld(9210)的读写速率。
# === ★ 再往下:这个进程在读写【哪些文件】===
$ ls -l /proc/9210/fd # 它打开了哪些文件
$ cat /proc/9210/io # ★ 该进程累计 IO 统计
read_bytes: 10854203392
write_bytes: 88243507200 # ★ 累计写了 88G
# /proc/PID/io 里 read_bytes / write_bytes 是
# 这个进程【实际落盘】的字节数,很有参考价值。
# === 用 strace 看进程具体在对哪个文件做 IO(谨慎用)===
$ strace -f -e trace=read,write -p 9210 2>&1 | head
# ★ strace 会拖慢目标进程,生产上短暂采样即可,别长期挂。
修复 5:定位到进程之后——往哪个方向治
# === ★ 找到"谁在狂 IO"之后,要分清是"正常的多"还是"异常" ===
# === 方向 1:是不是日志/调试输出在疯狂写盘 ===
# 经典坑:某次把日志级别开成了 DEBUG,
# 程序疯狂往磁盘写日志 -> 磁盘 IO 打满。
$ cat /proc/9001/io | grep write_bytes # 看它写了多少
# 把日志级别调回 INFO/WARN,IO 立刻下来 —— 这种最常见。
# === 方向 2:数据库在大量刷盘 ===
# mysqld 狂写,常见于:大批量写入、大事务、
# 慢查询导致临时文件落盘、binlog/redo 刷盘频繁。
$ mysql -e "SHOW ENGINE INNODB STATUS\G" | grep -A5 'FILE I/O'
# ★ 治理思路:批量写改小批次、优化慢查询、
# 调整 innodb_flush_log_at_trx_commit 等(需评估)。
# === 方向 3:★ 是不是内存不足,在疯狂 swap ===
$ free -h
$ vmstat 1 3 # 看 si/so
# si/so 持续不为 0 -> 内存不够,系统在拿磁盘当内存用,
# 解法是加内存 / 降低内存占用,而不是治磁盘。
# === 方向 4:磁盘本身能力不够(硬件层)===
# 如果 IO 量是【合理的业务量】,只是磁盘扛不住:
# - 机械盘换 SSD,IOPS 和 await 差一个数量级
# - 云盘升级到更高 IOPS 规格
# - 把读写热点分散到多块盘
# === ★ 临时降低 IO 影响:ionice 给进程降优先级 ===
$ ionice -c3 -p 9001
# -c3 = idle 级别:只在磁盘空闲时才让这个进程做 IO,
# 适合给"不紧急的批处理/备份进程"降级,
# 把宝贵的 IO 让给在线服务。
# === 一句话 ===
# 找到 IO 大户后,先问"它该不该产生这么多 IO":
# 不该 -> 治程序(日志级别/批量/查询);
# 该 -> 治硬件(换盘/升配)或错峰(ionice)。
修复 6:磁盘 IO 排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ load 高,先分清是 CPU 瓶颈还是 IO 瓶颈 ===
$ top
# us+sy 高 -> CPU 瓶颈;wa 高、CPU 闲 -> IO 瓶颈
# === 2. ★ load 算的是 R + D 状态进程,不止 CPU ===
# 等磁盘卡在 D 状态的进程,一样计入 load。
# === 3. iowait 高 = CPU 在干等磁盘,加 CPU 没用 ===
$ ps -eo state | grep -c '^D' # D 状态进程多 = IO 卡
# === 4. ★ 用 iostat -x 量化磁盘 ===
$ iostat -x 1 3
# %util 接近 100% + await 远超正常 = 磁盘是瓶颈
# === 5. 用 iotop / pidstat -d 揪出 IO 大户进程 ===
$ iotop -o
$ pidstat -d 1
# === 6. ★ 别忘了排查内存:swap 也会制造大量 IO ===
$ vmstat 1 3 # 看 si/so
# === 7. 排查 IO 问题的命令链 ===
$ uptime / top # ① load 和 wa,先定性
$ ps -eo state | grep -c '^D' # ② D 状态进程多不多
$ iostat -x 1 3 # ③ 磁盘 %util / await
$ iotop -o / pidstat -d 1 # ④ 谁在狂读写
$ cat /proc/PID/io # ⑤ 该进程累计 IO 量
$ vmstat 1 3 # ⑥ 排除内存不足引发的 swap IO
# 按这个顺序,IO 瓶颈基本能定位。
命令速查
需求 命令
=============================================================
看 load average uptime / top
看 CPU 各项占比(含 wa) top 看 %Cpu 行
数 D 状态(等 IO)进程 ps -eo state | grep -c '^D'
看磁盘详细 IO 指标 iostat -x 1 3
看哪个进程在狂读写 iotop -o
按进程统计 IO pidstat -d 1
看某进程累计 IO 字节 cat /proc/PID/io
看阻塞进程数和 swap vmstat 1 3
给进程的 IO 降优先级 ionice -c3 -p PID
口诀:load 高先看 top 的 wa,wa 高 CPU 闲就是磁盘 IO 瓶颈
iostat 看 %util 打没打满、await 高不高,iotop 揪 IO 大户
避坑清单
- load average 不等于 CPU 使用率,它算的是 R 加 D 状态的进程数
- 等磁盘 IO 卡在 D 状态的进程也计入 load,所以 load 高未必是 CPU 忙
- load 高先看 top 的 %wa,wa 高且 CPU 使用率低就是磁盘 IO 瓶颈
- iowait 高说明 CPU 在干等磁盘,这种情况加 CPU 核数毫无帮助
- D 状态是不可中断睡眠,通常在等 IO,连 kill -9 都杀不动
- iostat -x 看 %util 接近 100% 加 await 远超正常,即磁盘是瓶颈
- iotop -o 或 pidstat -d 揪出在狂读写的进程,/proc/PID/io 看累计 IO
- 内存不足引发的 swap 会制造大量磁盘 IO,vmstat 看 si/so 别漏排
- 日志级别误开成 DEBUG 是 IO 打满的经典原因,先查日志输出量
- IO 量合理但磁盘扛不住就换 SSD 升配,不紧急进程可用 ionice 降级
总结
这次"CPU 明明很闲、load 却高到 38"的事故,纠正了我一个用了很多年的、几乎已经成为肌肉记忆的错误等式:load average 等于 CPU 的繁忙程度。在我过去的认知里,load 这个数字的含义是不言自明的——它就是衡量 CPU 累不累的那根指针。load 高,意思就是 CPU 忙不过来了,有一堆进程在排队等着抢 CPU;要解决,无非是优化掉那个吃 CPU 的大户,或者干脆加几个核。正是这个等式,让我在 top 的画面前彻底卡壳:一个高达 38 的 load,和几个明明闲着的 CPU 核,如果 load 真的只衡量 CPU,那这两个数字就是公然地、不可调和地互相矛盾。它们不可能同时为真——可它们偏偏就在同一块屏幕上,同时为真。复盘到根上,我才真正理解了 load average 这个数字的本义,而它的本义,比我以为的要宽得多。load 衡量的,从来不是"有多少进程在等 CPU",而是"有多少进程处于'想干活'的状态"。这是一个关键的、被我忽略了多年的区别。一个"想干活"的进程,确实可能是在等 CPU——它万事俱备,只差一个空闲的核心,这是我熟知的那一种。但它也可能是另一种我从未纳入考量的状态:它要读一块数据,而这块数据躺在慢吞吞的磁盘上,于是它发起了一个 IO 请求,然后就卡死在那里,动弹不得,连 kill -9 都打不断它——这就是所谓的 D 状态,不可中断的睡眠。这样一个卡在 D 状态苦等磁盘的进程,它没有占用一丝一毫的 CPU,可它同样是"系统的一个负担",同样是一个"想干活却干不成"的进程,因此,它一样被堂堂正正地计入了 load。这就是矛盾的解药:我那台机器上,真相不是 CPU 忙不过来,而是磁盘慢得离谱——%util 死死地贴在 100%,单次 IO 的 await 高达 220 毫秒。磁盘成了那个真正的瓶颈,大量的进程发起读写后,一个接一个地卡进了 D 状态,排着长队等待这块被压垮的磁盘。是这条长长的 D 状态队列,把 load 顶到了 38。而 CPU 之所以闲,恰恰是因为它无能为力——它不是没活干,是它要算的数据还在磁盘里没出来,它只能干等,这份"不情愿的空闲",就老老实实地体现在了 top 里那个高达 80% 的 iowait 上。load 38、CPU 闲、wa 80%,这三个我一度以为互相打架的数字,其实从头到尾都在异口同声地讲述同一个故事:磁盘扛不住了。这次从一个自相矛盾的 top 画面里走出来,我最大的收获,是把"load = CPU 忙"这个用了多年的错误等式,郑重地擦掉了。load 高,只是系统在喊"我背着很多想干活的进程",至于这些进程到底卡在哪里——是卡在 CPU 前,还是卡在磁盘前——load 这个数字本身,一个字都没说。要听清它们究竟卡在哪,得去看 iowait,去数 D 状态的进程,去用 iostat 量一量那块盘的 %util。一个高高的 load,是一个问题的开始,而绝不是一个答案。
—— 别看了 · 2026