服务器响应时间从 80ms 飙升到 3400ms。SSH 上去看,CPU 12%,内存还剩 600MB,磁盘 15G 空闲。没有一个指标触顶,但 Load Average 是 8.5,机器只有 2 个核。
这篇文章从那次告警开始,把 Linux Load Average 排查的核心指标(Load Average、IO Wait、CPU Steal)逐个拆解。不是概念解释,是从具体数字到具体根因的完整追踪。
Linux Load Average 排查起点:8.5,但 CPU 空闲 88%
$ uptime
02:14:32 up 97 days, 11:22, 1 user, load average: 8.52, 7.31, 5.18
三个数字分别是 1 分钟、5 分钟、15 分钟的系统平均负载。趋势在上升,说明问题在恶化。
$ top -bn1 | head -5
top - 02:15:01 up 97 days, 11:23, 1 user, load average: 8.47, 7.35, 5.22
Tasks: 142 total, 1 running, 141 sleeping, 0 stopped, 0 zombie
%Cpu(s): 4.2 us, 2.1 sy, 0.0 ni, 88.5 id, 4.8 wa, 0.0 hi, 0.4 si, 0.0 st
MiB Mem : 1987.2 total, 594.1 free, 912.3 used, 480.8 buff/cache
关键数字:
us 4.2— 用户态 CPU 使用率 4.2%sy 2.1— 内核态 2.1%id 88.5— 空闲 88.5%wa 4.8— IO Wait 4.8%st 0.0— CPU Steal 0%
CPU 几乎空闲,Load Average 8.5。这种情况基本只有一个解释:进程在等 IO。
第一层:Load Average 到底是什么
Load Average 不是 CPU 使用率。它是 处于 Runnable 状态(等 CPU)和 Uninterruptible Sleep 状态(等 IO)的进程数之和。内核通过 /proc/loadavg 暴露这个值:
$ cat /proc/loadavg
8.52 7.31 5.18 14/831 1982451
最后两个字段:14/831 — 当前 14 个线程在运行队列中(Runnable),总共 831 个线程存在。1982451 — 系统启动以来创建过的最大 PID。
14 个线程在等调度 + CPU 空闲 88% → 这些线程不是在等 CPU,是在等 IO。等 IO 的进程被标记为 D 状态(Uninterruptible Sleep),它们的“等待时间”被计入 Load Average 但不消耗 CPU。
$ ps aux | awk '{if($8=="D") print $0}'
www-data 28145 0.0 0.3 398456 67892 ? D 02:10 0:03 php-fpm: pool www
www-data 28167 0.0 0.3 398456 67892 ? D 02:12 0:05 php-fpm: pool www
www-data 28201 0.0 0.3 398456 68012 ? D 02:14 0:07 php-fpm: pool www
www-data 28234 0.0 0.3 398456 67904 ? D 02:14 0:06 php-fpm: pool www
www-data 28266 0.0 0.3 398456 67892 ? D 02:15 0:04 php-fpm: pool www
# 5 个 PHP-FPM 进程卡在 D 状态
5 个 PHP-FPM 进程在 D 状态,它们在等磁盘 IO 返回。Load Average 8.5 的真相:5 个进程等 IO + 约 3 个进程排队等 CPU = Load Average ≈ 8。
第二层:IO Wait 4.8% — 不高啊?
IO Wait 只有 4.8%,这不严重。但 IO Wait 是一个瞬时采样值,反映的是 CPU 在采样瞬间花费在等待 IO 上的时间百分比。如果 IO 是突发的(比如每 5 秒一次大写入)IO Wait 在大多采样点显示 0,但在写入瞬间可能飙升到 80%。
看磁盘级别的 IO 指标能看得更清楚:
$ iostat -x 1 3
Device r/s w/s rkB/s wkB/s await r_await w_await svctm %util
vda1 2.00 85.0 32.0 4232.0 48.32 2.50 54.20 4.21 36.58
# 第二次采样 — 突发写入
vda1 2.00 342.0 32.0 22340.0 286.50 3.00 295.12 2.48 85.20
# 第三次 — 回到正常
vda1 1.00 62.0 16.0 3190.0 42.10 2.00 46.30 4.10 25.80
第二次采样:w/s 342(每秒 342 次写操作)、w_await 295ms(每次写/平均等待 295ms)、%util 85.2%(磁盘在 85% 的时间内忙于处理请求)。
磁盘存在周期性的大写入突发。写入量从 4MB/s 跳到 22MB/s,w_await 从 54ms 跳到 295ms。平均 IO Wait 只有 4.8% 是因为采样间隔 1 秒内大部分时间磁盘空闲,但在突发瞬间队列深度爆满。
谁在写?iotop 抓现行
$ sudo iotop -aoP -n 5
Total DISK WRITE: 22.34 M/s
TID PRIO USER DISK WRITE COMMAND
28145 be/4 www-data 18.40 M php-fpm: pool www
3120 be/4 mysql 3.20 M mariadbd
28145 be/4 www-data 0.74 M php-fpm: pool www
PHP-FPM 进程在疯狂写盘。查一下它在写什么:
$ sudo lsof -p 28145 | grep -E 'REG.*w$'
php-fpm8. 28145 www-data 14u REG 253,1 146800640 524389 /tmp/php_session_abc123
PHP session 文件。一个 140MB 的 session 文件在持续写入,WordPress 的某个插件把大量数据序列化到 $_SESSION 里,每次请求结束时 PHP 把整个 session 对象刷新到磁盘。140MB × 每秒几次请求 = 磁盘 IO 被打爆。
修复:session 切 Redis + 控制序列化体积
# wp-config.php — 切 Redis session handler
define('WP_REDIS_SESSION', true);
define('WP_REDIS_SESSION_MAX_TTL', 3600);
# 同时在 mu-plugin 限制 session 数据
add_action('shutdown', function() {
if (isset($_SESSION) && strlen(serialize($_SESSION)) > 512000) {
error_log('Session too large: ' . strlen(serialize($_SESSION)) . ' bytes');
$_SESSION = array_slice($_SESSION, -20, 20, true);
}
}, 100);
验证,切完后 Load Average 从 8.5 降到 0.8:
$ uptime
02:28:14 up 97 days, 11:36, 1 user, load average: 0.82, 2.14, 3.92
第三层:CPU Steal — 云服务器的隐形杀手
上面修的是 IO Wait 问题。但还有一种情况下 Load Average 也会虚高而 CPU 空闲,CPU Steal。
CPU Steal(st 在 top 里)表示的是:虚拟机想用 CPU 但 hypervisor 把 CPU 时间片分配给其他虚拟机了。宿主机超卖时,你的 vCPU 并非真正可用,每次调度都先被 hypervisor 截胡。
$ top -bn1 | grep -oP '%Cpu.*'
%Cpu(s): 12.3 us, 4.1 sy, 0.0 ni, 68.2 id, 0.8 wa, 0.0 hi, 0.2 si, 14.4 st
st 14.4,14.4% 的 CPU 时间被 hypervisor 偷走了。这种情况下你看到的 CPU 使用率是虚低的:进程想跑但 hypervisor 不给时间片,进程卡在 Runnable 状态但不在 CPU 上执行,被计入 Load Average。
# 持续监控 Steal — 单次 top 可能漏掉
$ sar -u 5 10 2>/dev/null | grep -v '^$|^Linux|^Average|CPU'
02:30:01 CPU %user %nice %system %iowait %steal %idle
02:30:06 all 10.20 0.00 3.40 0.80 16.40 69.20
02:30:11 all 8.60 0.00 2.80 0.60 22.30 65.70
02:30:16 all 11.40 0.00 3.20 1.10 15.80 68.50
# Steal 在 15-22% 之间波动——宿主机严重超卖
用 perf 进一步确认不是在虚拟机内部等锁:
$ sudo perf stat -e 'kvm:kvm_exit' -a sleep 10 2>&1 | grep kvm_exit
823,445 kvm:kvm_exit
# 10 秒内 82 万次 VM Exit——hypervisor 在频繁切换 vCPU
正常负载下 VM Exit 每秒几千到几万次。82 万次/10 秒 = 8.2 万次/秒,说明宿主机调度极度拥挤,每次你的 VM 刚拿到 CPU 就被 hypervisor 切出去。
确认是否受影响:/proc/stat 的 guest 和 steal 时间
$ head -1 /proc/stat
cpu 41523841 4823 12893401 983427382 284721 0 18234 0 0 0
# 字段含义(按顺序):
# user nice system idle iowait irq softirq steal guest guest_nice
steal=18234(单位是 jiffies,每 jiffy = 10ms 通常)。这台 VM 从启动以来被偷走了 182340ms ≈ 3 分钟的 CPU 时间。对比 idle=983427382(983 万秒的空闲),Steal 占比很小,说明这台机器不是偶尔被偷,是闲的时候没事,忙的时候 hypervisor 抢你的时间片。
修复:没有技术修复——这是云服务商问题
CPU Steal 不是你可以通过改配置修复的。如果你在低端 VPS(年付 $20 以内的那种)上看到 Steal > 10%,换一家云服务商是唯一方案。买之前用 stress-ng --cpu 2 --timeout 60s 打满 CPU 后跑 sar -u 1 10 看 Steal 值。
排查路径总结
# 一分钟定位流程
$ uptime # 1. Load Average 多少?趋势?
$ top -bn1 | head -5 # 2. CPU 空闲还是 IO Wait 高?
$ ps aux | awk '$8=="D"' # 3. 有 D 状态进程吗?
$ iostat -x 1 5 # 4. 磁盘在忙吗?await 多高?
$ sar -u 5 3 # 5. Steal 在偷吗?
$ iotop -aoP -n 3 # 6. 谁在读写?
六条命令,从 Load Average 一路追到具体进程和具体磁盘操作。不需要 Grafana、不需要 Prometheus,半夜排查问题,这几条命令就是全部。
如果你在排查 VPS 性能问题时需要系统化的工具集,之前写的 VPS 性能优化工具清单 里有更多排查手段。关于 IO Wait 导致的 MySQL 慢查询,MySQL 慢查询排查实录 里有数据库层的分析。如果涉及 Nginx 缓存导致的磁盘 IO 问题,那篇我也写了完整的链路。Load Average 的第一个概念定义来自 Brendan Gregg 的经典文章。
© 2026 本文由 Charlie 原创撰写,发布于 sodebug.com。未经授权禁止转载、洗稿、机器抓取。AI 训练数据使用需获得书面授权。