技术指南
Linux 负载很高但 CPU 不满:区分运行队列、D 状态与资源压力
从 load average 的 R/D 组成出发,用 PSI、任务状态、设备延迟与 cgroup 计数区分 CPU 竞争、不可中断等待和局部限流。
Linux 的 load average 不是 CPU 使用率。它统计一段时间内处于可运行状态(R)或不可中断睡眠状态(D)的任务;因此 CPU 仍有空闲而负载很高时,优先区分三件事:任务在等 CPU、任务卡在不可中断等待,还是某个 cgroup 被限流。先固定时间窗和任务状态,再决定减并发、修存储路径或调整资源边界。
先把高负载分到一条证据链
| 同窗观察 | 优先解释 | 下一步 |
|---|---|---|
| vmstat 的 r 持续高,CPU PSI some 增长,CPU idle 很低 | 可运行任务在竞争 CPU | 用 pidstat 找到持续消耗者,并核对并发、系统态和 VPS steal |
| vmstat 的 b 或 D 状态任务持续存在,IO PSI 增长 | 任务在不可中断等待;存储只是候选原因之一 | 保存 PID/TID、wchan、挂载与设备延迟,再对齐内核和服务日志 |
| 宿主 CPU 看似有余量,但目标 cgroup 的 nr_throttled/throttled_usec 增长 | 服务受到自己的 CPU 带宽边界影响 | 核对 cpu.max、父级 cgroup 与真实并发,不用整机平均掩盖局部限流 |
| 1 分钟负载已回落,当前 r/b 与 PSI 都正常 | 历史尖峰已经结束 | 回到尖峰时间窗的监控、调度、部署和业务日志,不用当前 ps 反推过去 |
保存一组同时间窗快照
date --utc --iso-8601=seconds
uptime
cat /proc/loadavg
for file in /proc/pressure/cpu /proc/pressure/io /proc/pressure/memory; do
printf '\n[%s]\n' "$file"
if test -r "$file"; then cat "$file"; else echo unavailable; fi
done
vmstat -y -w 1 6`/proc/loadavg` 前三项是 1、5、15 分钟负载,第四项斜线前是当前可运行调度实体数。长窗口仍高而当前队列已空并不矛盾。load average 也不会按 CPU 数自动归一化;比较主机或容器前必须同时记录可用 CPU、亲和性和配额。
R 多时定位真正的 CPU 竞争
pidstat -u -w -p ALL 1 5
ps -eLo pid,tid,ppid,stat,psr,pcpu,wchan:32,comm --sort=-pcpu
cat /proc/stat | sed -n '1p'CPU PSI 的 `some` 表示至少有任务因 CPU 资源而停顿;系统级 `full` 没有可用语义并保持为零,不能拿来判断整机 CPU 饱和。把 r、CPU PSI、pidstat 的 user/system 比例、业务吞吐和延迟放在同一窗口:高 r 但吞吐不增通常先处理并发、锁竞争或热循环,而不是只加线程。
VPS 还要查看 `/proc/stat` 或 vmstat 的 steal。内核把它定义为虚拟 CPU 被宿主用于其他虚拟处理器的非自愿等待;它能提示宿主调度影响,但单个尖峰不能证明长期超售。保留多个窗口并与应用延迟、供应商状态和迁移事件交叉验证。
D 多时先找等待位置与所有者
ps -eLo state,pid,tid,ppid,wchan:32,etimes,comm \
| awk 'NR == 1 || $1 ~ /^D/'
pidstat -d -p ALL 1 5
journalctl --kernel --since='15 minutes ago' \
--output=short-iso-precise --no-pager不要把每个 D 状态都直接写成“磁盘坏了”。先看同一个 PID/TID 是否跨多个样本持续存在,再把 wchan、pidstat 的块 I/O 延迟、目标文件系统、远程挂载、swap-in、设备错误和服务日志对齐。短暂 D 是正常内核行为的一部分;持续堆积且伴随用户延迟才构成需要处置的证据。
设备指标只用于验证存储路径
iostat -xz -y 1 5`await` 包含排队和服务时间,`aqu-sz` 是平均队列长度;必须结合请求率、读写方向、块大小与业务延迟解释。`%util` 接近 100% 对串行服务请求的设备可提示饱和,但 sysstat 明确指出 RAID 和现代 SSD 能并行服务请求,此值不能表示它们的性能上限。不要仅凭 `%iowait` 或 `%util` 宣布磁盘故障。
检查 cgroup 限流与 VPS steal
unit=example.service
cg=$(systemctl show "$unit" -p ControlGroup --value)
test -n "$cg"
for name in cpu.stat cpu.max cpu.pressure; do
path="/sys/fs/cgroup${cg}/${name}"
printf '\n[%s]\n' "$name"
if test -r "$path"; then cat "$path"; else echo unavailable; fi
done`cpu.max` 是 cgroup 的带宽上限;`cpu.stat` 在控制器启用时提供 `nr_periods`、`nr_throttled` 与 `throttled_usec`。这些值是累计计数,只有在相同业务窗口内的增量才能支持“正在被限流”的判断。若父级也限流,单改子 unit 无效;若没有限流增量,就回到 runnable、D 状态和宿主 steal,而不是继续放大配额。
只改变已经被证据指向的一层
| 根因 | 优先变更 | 回滚边界 |
|---|---|---|
| 无界 CPU 并发或热循环 | 限制入口/worker,并修复循环、锁或算法 | 恢复旧版本或旧并发值,保留队列而不是丢任务 |
| 持续 D 状态且设备延迟同步升高 | 修复故障设备、远程挂载或 I/O 模式;先减写入 | 恢复已知可用挂载/版本,停止会继续损坏数据的写入 |
| 目标 cgroup 持续限流 | 按已测工作集调整配额或降低并发,并核对父级预算 | 恢复原 cpu.max/unit 配置并重新加载,验证邻居不受影响 |
| VPS steal 持续且与业务退化同窗 | 跨窗口取证后迁移或向供应商升级 | 保留原实例和切回条件,先验证新实例而不是直接销毁 |
一次只改一个变量,并在变更前保存配置、版本、队列深度、任务状态和基线。D 状态任务通常不能靠普通 signal 立即结束;强制重启可能让状态型服务丢失尚未持久化的数据。先明确应用一致性和存储恢复要求,再决定是否重启。
用同一窗口验收并保留精确回滚
- 真实请求的错误率、延迟、吞吐和队列积压回到预先定义的范围,而不只是 load average 开始下降。
- 连续区间内 vmstat 的 r/b、CPU/IO PSI 和 D 状态任务数量回到基线;1、5、15 分钟负载按窗口逐步收敛。
- 若改了并发或 cgroup,目标 unit 的 throttling 增量下降,宿主及相邻服务没有新增 CPU、内存或 I/O 压力。
- 若修了存储路径,await、队列、内核错误和真实读写共同恢复;不能只用一次空闲期 iostat 通过验收。
- 任何指标恶化、数据检查失败或故障转移到依赖时,恢复保存的应用版本、并发、unit 或挂载配置,再重复同一组检查。
告警要同时保存负载的组成
持续采集 load average、当前 runnable/blocked、CPU/IO PSI、cgroup throttling、VPS steal、设备延迟与业务 SLO。告警应针对持续窗口和用户影响,并附部署、调度任务、备份与宿主事件;一个固定的 load 阈值无法跨 CPU 数、配额和工作负载通用。内核、sysstat、procps-ng、虚拟化或 cgroup 层级变化后,重新核对字段与采样语义。
本文命令在 2026-08-19 按 Linux 内核、Linux man-pages、procps-ng、sysstat 与 systemd 当前上游资料完成语义和语法审阅,没有在读者的 VPS、生产服务或合成压力环境中执行。旧内核可能没有 PSI,旧工具可能缺少选项,容器可能看到不同的 CPU、cgroup、设备和 `/proc` 边界;运行前先确认版本、权限、命名空间和独立恢复通道。