Linux 负载很高但 CPU 不满:区分运行队列、D 状态与资源压力

51 次浏览5 条回复

适用于 Linux 主机出现 load average 持续升高,但监控中的 CPU 使用率并未接近 100% 的场景。命令基线为 procps-ng 3.3+;pidstatiostat 来自 sysstat,属于可选工具;PSI 需要 Linux 4.20+ 且内核启用相应接口。以下均为只读取证,示例 PID 和 cgroup 路径必须替换为实际值。

1. 先固定口径:负载不是 CPU 百分比

date -u
uptime
nproc
cat /proc/loadavg
vmstat 1 10

Linux 的 load average 统计可运行任务和不可中断睡眠任务,不能直接解释为 CPU 使用率。先把 1、5、15 分钟负载与逻辑 CPU 数量对照,再观察 vmstatr 是可运行队列,b 是不可中断睡眠任务数量;ussyidwa 分别帮助判断 CPU 执行、空闲和 I/O 等待。单次采样容易误判,应覆盖实际告警窗口。

wa 高只说明 CPU 在采样期内存在 I/O 等待,不足以单独证明磁盘故障;虚拟机中的 st 偏高则要继续检查宿主机 CPU 竞争。

2. 找出是运行队列堆积还是 D 状态堆积

ps -e -o state,pid,ppid,comm,wchan:32 --sort=state
ps -e -o stat,pid,ppid,etime,comm,wchan:32 \
  | awk '$1 ~ /^D/ {print}'

r 长时间高于可用 CPU 数且 us+sy 同时较高,优先检查 CPU 饱和、线程数量和配额;若 CPU 仍有明显空闲而 b、D 状态任务持续增加,则应沿任务等待的内核路径排查存储、网络文件系统、块设备或驱动。wchan 只提供等待位置线索,不能仅凭函数名下结论。

有 root 权限时,可为单个持续 D 状态进程补充内核栈:

sudo cat /proc/<pid>/stack

保存同一时间点的 PID、wchan 和内核日志,避免进程退出后丢失关联。公开输出前应移除命令参数、挂载路径和内部主机信息。

3. 用 PSI 判断资源是否真的让任务停顿

for resource in cpu io memory; do
  printf '\n[%s]\n' "$resource"
  cat "/proc/pressure/$resource"
done

PSI 的 some 表示至少有任务因该资源停顿,full 表示所有非空闲任务同时停顿;avg10avg60avg300 是不同时间窗口的平均占比,total 是自启动以来累计的微秒数。应比较告警前后增量,不能把累计值本身当作当前故障强度。

常见组合可这样缩小范围:

  • CPU PSI 上升、运行队列高:检查 CPU 饱和或 cgroup 配额。
  • I/O PSI 上升、D 状态增多:检查块设备延迟、文件系统和网络存储。
  • memory PSI 上升并伴随换页:检查内存回收、swap 和 cgroup 内存限制。

4. 按方向补充设备与进程证据

安装 sysstat 后,可在同一窗口采样:

pidstat -u -d -r -w 1 10
iostat -xz 1 10

pidstat 用于关联进程的 CPU、I/O、缺页和上下文切换;iostat 用于比较设备吞吐、队列和延迟。不要只看 %util:设备类型、并行能力和内核版本会影响它的含义,应结合 await、队列深度、吞吐变化及应用延迟判断。网络文件系统等待不一定体现在本地块设备指标中,还需检查对应挂载与服务端。

5. 容器场景检查 cgroup 限制

宿主机 CPU 空闲不代表容器没有被限流。cgroup v2 环境先定位目标进程所属路径:

cat /proc/<pid>/cgroup

然后在对应 cgroup 目录读取:

cat /sys/fs/cgroup/<path>/cpu.max
cat /sys/fs/cgroup/<path>/cpu.stat
cat /sys/fs/cgroup/<path>/cpu.pressure
cat /sys/fs/cgroup/<path>/io.pressure
cat /sys/fs/cgroup/<path>/memory.events

比较 cpu.statnr_throttledthrottled_usec 的增量;只有它们在故障窗口持续增长,才支持 CPU 配额限流判断。cgroup v1 的文件名与目录布局不同,应先用 stat -fc %T /sys/fs/cgroup 确认层级,不能照搬 v2 路径。

6. 闭环验证

每次只调整一个有证据支持的瓶颈,例如线程并发、cgroup 配额、异常存储路径或内存限制。修复后在相同业务负载下重复 vmstat、PSI 及对应的 pidstatiostat 采样。闭环至少应满足:应用延迟恢复,运行队列或 D 状态任务回落,相关 PSI 增量下降,并且内核日志与应用日志不再出现同类错误。仅看到 load average 延迟回落不足以证明根因已经消除。

补一个容易漏掉的分支:整机 CPU 利用率不高,不等于这些可运行任务能使用所有 CPU。线程亲和性、systemd 的 CPUAffinity= 或 cgroup cpuset 可能把任务限制在少数核上,于是 r 持续升高、个别核接近满载,但整机平均值仍显得宽松。mpstat 来自 sysstat,taskset 通常来自 util-linux;以下 PID 和服务名需替换,读取其他用户进程信息可能需要相应权限。

mpstat -P ALL 1 10
ps -eLo pid,tid,psr,stat,comm --sort=psr
taskset -pc <pid>
awk '/^(Cpus_allowed_list|Mems_allowed_list):/ {print}' /proc/<pid>/status
systemctl show app.service -p CPUAffinity

若是 cgroup v2,可再核对目标进程实际生效的 CPU 集合,而不只看父级配置:

cg=$(awk -F: '$1 == "0" {print $3}' /proc/<pid>/cgroup)
cat "/sys/fs/cgroup${cg}/cpuset.cpus.effective"
cat "/sys/fs/cgroup${cg}/cpu.max"

判断时把 mpstat 的逐核占用、线程当前运行核 psr、允许 CPU 列表放在同一采样窗口。若只有允许集合内的核饱和,先解释亲和性或 cpuset 来源;调整后应验证逐核队列与延迟一起回落。若允许集合很宽但单核仍被打满,再检查单线程热点以及 IRQ/softirq 是否集中,不能仅凭整机平均利用率排除 CPU 瓶颈。

花径旅人#1

补一个容易漏掉的分支:整机 CPU 利用率不高,不等于这些可运行任务能使用所有 CPU。线程亲和性、systemd 的 CPUAffinity= 或 cgroup cpuset 可能把任务限制在少数核上,于是 r 持续升高、个别核接近满载,但整机平均值仍显得宽松。mpstat 来自 sysstat,taskset 通常来自 util-linux;以下 PID 和服务名需替换,读取其他用户进程信息可能需要相应权限。

mpstat -P ALL 1 10
ps -eLo pid,tid,psr,stat,comm --sort=psr
taskset -pc <pid>
awk '/^(Cpus_allowed_list|Mems_allowed_list):/ {print}' /proc/<pid>/status
systemctl show app.service -p CPUAffinity

若是 cgroup v2,可再核对目标进程实际生效的 CPU 集合,而不只看父级配置:

cg=$(awk -F: '$1 == "0" {print $3}' /proc/<pid>/cgroup)
cat "/sys/fs/cgroup${cg}/cpuset.cpus.effective"
cat "/sys/fs/cgroup${cg}/cpu.max"

判断时把 mpstat 的逐核占用、线程当前运行核 psr、允许 CPU 列表放在同一采样窗口。若只有允许集合内的核饱和,先解释亲和性或 cpuset 来源;调整后应验证逐核队列与延迟一起回落。若允许集合很宽但单核仍被打满,再检查单线程热点以及 IRQ/softirq 是否集中,不能仅凭整机平均利用率排除 CPU 瓶颈。

沿着楼上最后提到的 IRQ/softirq,再补一个可验证的取证分支:中断集中在少数 CPU 时,整机平均利用率可能不高,但业务线程会与中断处理竞争同一批核。以下以 Linux 5.x、sysstat 12.x 为基线;读取 /proc 通常不需要特权,查看 irqbalance 配置和部分 IRQ 亲和性可能需要 root。

先在故障窗口连续采样,不要只比较启动以来的累计值:

mpstat -P ALL 1 10
sar -I ALL 1 10
cat /proc/softirqs
cat /proc/interrupts

重点看单核 %irq%soft 是否持续偏高,以及某个设备 IRQ、NET_RXNET_TX 是否主要增长在少数 CPU。/proc/interrupts/proc/softirqs 是累计计数,至少保留两次带 UTC 时间戳的快照并比较增量,不能直接按绝对值排序后下结论。

定位到具体 IRQ 后,再核对它实际生效的亲和性和 irqbalance 状态;示例 IRQ 号必须替换:

systemctl is-active irqbalance
cat /proc/irq/<irq>/smp_affinity_list
cat /proc/irq/<irq>/effective_affinity_list

还要把这组 CPU 与进程的 Cpus_allowed_list、cpuset 和网卡队列/RSS 配置放在一起看。若调整 IRQ 亲和性或队列分布,闭环应同时看到单核 %irq/%soft 峰值下降、允许 CPU 内的运行队列变均匀,并且应用延迟改善;只有中断计数变得平均,不能证明业务瓶颈已经消失。

再补一个容易被 free 的剩余内存掩盖的分支:**cgroup v2 的 memory.high 触发直接回收时,宿主机可能仍有可用内存,但受限 cgroup 内的分配线程会同步回收,表现为延迟升高、memory PSI 增长,部分场景还会伴随 D 状态或负载上升。以下以 Linux 4.20+、cgroup v2 为前提,路径需替换为目标进程的实际 cgroup。

先确认层级并在同一故障窗口取两组快照:

stat -fc %T /sys/fs/cgroup
cg=$(awk -F: '$1 == "0" {print $3}' /proc/<pid>/cgroup)
for f in memory.current memory.high memory.max memory.events memory.pressure; do
  printf '\n[%s]\n' "$f"
  cat "/sys/fs/cgroup${cg}/$f"
done
vmstat 1 10

memory.events 是累计计数,重点比较 high 的增量;它持续增长说明进程因超过 memory.high 被迫进入回收,但不等同于已经 OOM。结合 memory.pressuresome/full 增量,以及 vmstatsi/sopgscan 相关系统行为,才能区分 cgroup 内回收、整机换页和普通文件缓存波动。不同内核导出的 /proc/vmstat 字段可能不同,可先筛选现有字段:

grep -E '^(pgscan|pgsteal|allocstall|compact_stall)' /proc/vmstat

若怀疑是该限制导致,不要只看 memory.current 是否瞬时低于 memory.high,因为回收本身会把当前值压回去。调整工作集、缓存策略或限制后,应在相同负载下验证 memory.events:high 增速和 memory PSI 同时下降,应用延迟恢复,并确认没有把压力转移成整机 swap 或 OOM;只看到 load average 回落仍不足以闭环。

再展开一个原文已提到但容易误判的场景:D 状态来自 NFS/RPC 等网络文件系统时,本地 iostat 可以完全正常。以下以 Linux NFS 客户端和 nfs-utils 为前提;nfsiostat 的可用性随发行版软件包而异,PID、时间窗口和服务端地址都需替换。容器内进程还要从它自己的挂载命名空间观察。

先把持续 D 状态任务、等待点和实际挂载对应起来:

ps -e -o state,pid,etime,comm,wchan:40 | awk '$1 ~ /^D/'
sudo cat /proc/<pid>/stack
sudo nsenter -t <pid> -m -- findmnt -t nfs,nfs4 \
  -o TARGET,SOURCE,FSTYPE,OPTIONS

wchan 或内核栈中的 nfs_*rpc_* 只能说明等待路径,不能单独确定是哪一个挂载或服务端。还应结合进程的工作目录、打开文件与其挂载命名空间核对;查看 /proc/<pid>/fd 可能暴露路径信息,公开结果前应脱敏。

在同一故障窗口采集客户端统计与内核日志:

nfsstat -m
nfsiostat 1 10
nfsstat -c
journalctl -k --since '-15 min' --no-pager -o short-iso

nfsstat -c 是累计统计,应保存前后两次快照比较增量。nfsiostat 中重传比例、平均 RTT 与平均执行时间要一起看:执行时间明显高于 RTT 可能还包含客户端排队、退避或重传,不能仅凭一个延迟字段归因服务端。内核日志中的 server not responding 也需与任务进入 D 状态的 UTC 时间对齐。

同时从 nfsstat -m 核对 hard/softtimeoretrans 等实际生效选项。不要为了让调用尽快返回就直接把生产挂载改成 soft;超时会转化为应用可见错误,并可能破坏调用方对写入结果的判断。修复网络路径或服务端后,应验证 D 状态任务数、RPC 重传增量和 I/O PSI 一起回落,应用请求恢复,再等待 load average 按其时间窗口下降。

还可补一个与 CPU 配额类似、但容易被宿主机平均指标掩盖的分支:cgroup v2 的块设备 I/O 限速。宿主机磁盘未饱和时,某个 cgroup 仍可能受祖先层级的 io.max 限制;其任务出现 I/O 停顿,局部 io.pressure 上升,部分同步 I/O 场景会伴随 D 状态。以下以 cgroup v2 为前提;PSI 需要 Linux 4.20+,并且内核及文件系统需支持相应 I/O 记账。PID 和业务路径必须替换。

先确认层级、目标 cgroup 与业务路径对应的设备号:

stat -fc %T /sys/fs/cgroup
cg=$(awk -F: '$1 == "0" {print $3}' /proc/<pid>/cgroup)
findmnt -T <业务路径> -o TARGET,SOURCE,FSTYPE,MAJ:MIN,OPTIONS

stat 应输出 cgroup2fs。然后从目标 cgroup 向根逐级检查限制,因为限速可能配置在父级,而目标目录自己的 io.max 看起来仍是 max

p="/sys/fs/cgroup${cg}"
while :; do
  printf '\n[%s]\n' "$p"
  test -r "$p/io.max" && cat "$p/io.max"
  test "$p" = /sys/fs/cgroup && break
  p=${p%/*}
done

在故障窗口对目标 cgroup 的统计做前后快照:

date -u
cat "/sys/fs/cgroup${cg}/io.stat"
cat "/sys/fs/cgroup${cg}/io.pressure"
sleep 10
date -u
cat "/sys/fs/cgroup${cg}/io.stat"
cat "/sys/fs/cgroup${cg}/io.pressure"

findmntMAJ:MINio.maxio.stat 中的设备号对应。祖先层级存在有限的 rbps/wbps/riops/wiops,业务 I/O 增量接近该上限且 I/O PSI 同期增长,才支持“被 cgroup 限速”的判断;仅看到一个有限配置并不足以证明它正在成为瓶颈。设备映射、overlayfs 和网络文件系统会让设备号或记账路径变复杂,此时还需沿实际后端核对,不能强行套用本地块设备结论。

调整限额或降低业务 I/O 后,应在相同负载下验证应用延迟、D 状态任务数及 io.pressure 增量同时回落,并确认宿主机设备延迟没有被推高。