订阅更新

Load 平均 200 但 top 看 CPU 都不到 10%——load 到底是什么?

Linux手册 第 16 题 约 9 分钟读完

本文目录
  1. 先说结论
  2. Load 到底怎么算的——最容易被问穿的一道题
  3. 场景还原:那次 NFS hang 的 200 load
  4. Load 高的 5 种常见根因
  5. 面试高频追问
  6. 实战踩坑:那次 NFS hang 之后我们改了什么
  7. 总结

先说结论

第一条:Linux 的 load average 不是 CPU 使用率,是 R + D 状态进程的数量——运行中的(R)加上不可中断睡眠(D,主要是 IO 等待)之和。

第二条:Load 高但 CPU 低,100% 是 IO 问题(磁盘慢 / NFS hang / 内核锁等待),要去查 D 状态线程,不是去查 CPU 热点。

第三条:1/5/15 分钟三个值要联合看——只有 1 分钟高是瞬时抖动,三个都高才是持续问题,如果 1 > 5 > 15 说明 load 在上升,趋势比绝对值更重要。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

某短剧出海公司一次线上事故:报警说某台 Flink 机器的 1 分钟 Load 飙到 200+,运维火急火燎上去 top 一看——CPU 占用加起来不到 10%,内存也够,磁盘也正常。监控页面红得发紫,机器看起来却像在「睡觉」。这种「load 炸了但 CPU 不高」的场景,是 Linux 面试里最能区分「看过监控」和「懂原理」的一道题。

我当时上去 5 分钟就定位了——是 NFS checkpoint 目录 hang 住,几十个 Flink 线程全部卡在 D 状态(uninterruptible sleep),这些线程不吃 CPU,但会算进 load average。Load 是「正在运行 + 不可中断等待」的总和,不是「CPU 使用率」。

Load 到底怎么算的——最容易被问穿的一道题

uptime 输出里那三个数字,几乎每个人都看过,但真正能讲清它怎么算出来的人不到 20%:

uptime
# 15:30:22 up 120 days, 6 users, load average: 0.50, 1.20, 2.10
#                                              ↑     ↑     ↑
#                                              1min  5min  15min
 
# 内核每 5 秒采样一次 '运行队列长度'
# 运行队列 = 当前 CPU 上跑的 + Runnable 但没排上 CPU 的 + D 状态进程
# 然后用指数衰减平均(EMA)算 1/5/15 分钟的平滑值
 
# 关键公式(内核源码 kernel/sched/loadavg.c):
# load(t) = load(t-5s) * exp(-5/60) + active(t) * (1 - exp(-5/60))
# 这就是为什么叫 'average'——不是算术平均,是带记忆的指数平均
 
# cat /proc/loadavg 看得更全
cat /proc/loadavg
# 0.50 1.20 2.10 3/850 12345
#                  ↑    ↑
#                  当前可运行进程/总进程数   最近创建的PID

最关键的认知颠覆:'load=2' 在单核机器上是 '满载+还有 1 个在排队',但在 32 核机器上是 '只用了 6%'。Load 的数字必须除以 CPU 核数才有意义。一台 32 核机器 load 到 32 才叫满载,到 200 就是 6 倍超载——全都挤在等 IO。

场景还原:那次 NFS hang 的 200 load

某短剧出海公司那台 Flink 机器 load 冲到 200+,我 ssh 上去先跑这三板斧:

# 第一板斧:看当前运行队列和阻塞队列
vmstat 1 5
# procs -----------memory---------- ---swap-- -----io----
#  r  b   swpd   free   buff  cache   si   so    bi    bo
#  1 52  0    2000  80   30000  0    0    200   50
#  2 58  0    1998  80   30000  0    0    180   40
# ↑ b 列是 'blocked on IO' 的进程数,长期 50+
# ↑ 这就是 load 200 的根源:50+ 个线程在等 IO
 
# 第二板斧:直接找 D 状态的进程/线程
ps -eLo pid,tid,stat,comm | awk '$3 ~ /D/' | head -20
# PID    TID    STAT  COMM
# 12345  12346  Dl    java
# 12345  12347  Dl    java
# 12345  12348  Dl    java
# ...一堆 java 线程都在 D
 
# 第三板斧:看这些线程 stack 卡在哪
cat /proc/12345/task/12346/stack
# [<ffffffff>] rpc_wait_bit_killable+0x24/0xb0
# [<ffffffff>] __rpc_execute+0x1f5/0x470
# [<ffffffff>] nfs4_handle_exception+0x75/0x130
# ← 卡在 NFS RPC 等待,checkpoint 目录是个 NFS mount

10 分钟定位:NFS server 那端因为带宽被另一个作业打爆,checkpoint 写请求全部在 RPC 层面排队等响应。Flink 那边看不出任何 CPU 问题,但因为 checkpoint 涉及的线程数量很多,几十个线程同时卡 D,load 瞬间冲到 200。

Load 高的 5 种常见根因

类型vmstat r/b典型症状排查命令常见根因
CPU 饱和r 高 b 低top CPU 100%top -H + jstack热点代码 / GC
IO 阻塞r 低 b 高CPU 不高但 load 高ps D + iostat磁盘慢 / NFS hang
线程暴涨r 高进程数几千ps -eLf | wc -l线程池没限制
锁竞争r 中等多进程轮流饥饿perf lock内核/用户锁
僵尸进程过程计数高ps 看到 <defunct>ps -eo stat父进程没 wait

面试高频追问

追问一:D 状态和 S 状态有什么区别?为什么 D 状态 kill -9 都杀不掉?

S 是可中断睡眠(interruptible sleep),线程在等事件,比如 sleep、epoll_wait——收到信号立刻醒来响应,kill -9 能直接干掉。D 是不可中断睡眠(uninterruptible sleep),通常是在等硬件 IO(磁盘、网络)或内核锁释放,这种状态下信号会被挂起到等待结束再处理。kill -9 对 D 状态无效,因为内核为了数据一致性不允许中途打断——你必须等底层 IO 完成或超时。这就是为什么 NFS hang 时那些 java 线程 kill -9 打不掉,最后只能重启机器或 umount -f。

追问二:负载均衡器前面挂了 10 台机器,load 都在 20 左右,你觉得算高吗?

不能凭绝对值判断,要先问两个问题:一是每台机器的 CPU 核数,32 核和 8 核的 load 20 含义完全不同;二是业务在什么场景,批处理任务 load 高是正常(CPU 打满算正常),但在线服务 load 高意味着 P99 延迟会飙。我的经验值是:CPU 密集型服务 load/核数 保持在 0.7 以下有弹性,在线服务保持 0.5 以下,批处理任务可以接近 1.0。超过 1.0 就意味着有排队,排队就意味着延迟。

追问三:你怎么把 D 状态线程的 stack 采集下来做事后分析?

最快的方式是 cat /proc//task//stack,这个文件是内核实时生成的线程调用栈(只在 CONFIG_STACKTRACE 开启时有,生产内核基本都开)。批量采集可以写个循环:for tid in $(ls /proc//task/); do echo $tid; cat /proc//task/$tid/stack; done > stacks.txt。更系统化的做法是开 perf probe 或 eBPF 的 stackcount 捕获 D 状态转换事件,能看到是哪个系统调用把线程带进 D 的。面试时能说出 /proc//task//stack 这个点,面试官就知道你真的排查过类似问题。

实战踩坑:那次 NFS hang 之后我们改了什么

某短剧出海公司那次事故后做了三件事:第一,所有 Flink 作业的 checkpoint 目录从 NFS 迁到对象存储(OSS/S3)+ 本地盘双写,彻底消除 NFS 单点;第二,监控加了「D 状态线程数 > 20 持续 5 分钟」告警,比等 load 冲到 200 提前得多;第三,制定「load/核数 > 3」必须立即上机排查的响应规范。

bash - D 状态数监控脚本

#!/bin/bash
# /data/monitor/check_d_state.sh  每分钟一次
set -euo pipefail
 
D_COUNT=$(ps -eLo stat | awk '/^D/' | wc -l)
LOAD=$(awk '{print $1}' /proc/loadavg)
CORES=$(nproc)
NORM_LOAD=$(echo "$LOAD / $CORES" | bc -l)
 
if [ $D_COUNT -gt 20 ]; then
  curl -sS -X POST $WEBHOOK -d "{\"text\":\"D 状态线程=$D_COUNT load=$LOAD\"}"
  # 顺便抓一次 D 状态栈存档,便于事后排查
  ps -eLo pid,tid,stat | awk '$3 ~ /D/ {print $1,$2}' | head -10 \
    | while read pid tid; do
        echo "--- $pid/$tid ---" >> /data/d_stacks/$(date +%F-%H%M).txt
        cat /proc/$pid/task/$tid/stack >> /data/d_stacks/$(date +%F-%H%M).txt 2>/dev/null
      done
fi

效果:上线后半年,凡是 load 异常的事故都能在 5 分钟内自动拿到 D 状态快照和栈,定位时间从平均 30 分钟缩到 5 分钟内。这种「问题发生时主动采集现场」的思路比事后回溯有效得多。

总结

Load average 是 Linux 最容易被误解的指标:它不是 CPU 使用率,是 R + D 状态进程的指数移动平均;它必须除以核数才有意义;load 高而 CPU 低 100% 是 IO 问题。面试时把「D 状态」「vmstat r/b」「load/核数」这三组关键词讲清楚,面试官就知道你看过真正的生产告警,不是光会背 uptime 命令。

下一篇预告:vmstat 敲下去 si so 一直在涨——swap 被触发的完整排查。

你遇到过最离谱的 load 异常是什么?是 CPU 爆了还是 IO hang?评论区聊聊。

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器