运维知识
悠悠
2026年10月8日

Linux 负载飙到 100,CPU 却很空闲:别急着扩容,先找出 D 状态进程

凌晨的告警突然响起:一台 16 核 Linux 节点的 Load Average 已经超过 100,接口延迟持续上升,部分请求开始超时。

值班同事登录服务器后却发现,CPU Idle 仍有 80% 左右。既没有进程持续占满 CPU,内存也没有明显耗尽。扩容一台机器后,故障依旧没有消失。

更反常的是,几个业务进程执行 kill -9 后迟不退出。重启服务同样卡住,Load Average 还在继续上涨。

遇到这种现场,我不会先扩容,也不会反复杀进程,而是先确认系统中有没有大量处于 D 状态的任务。因为负载很高而 CPU 很闲,往不是“算不过来”,而是大量任务被堵在了内核等待路径上。

高负载只是排队的结果,D 状态才可能指向等待现场:先找到任务在等什么,再决定是否扩容。

Load Average 到底在统计什么

很多人看到 Load Average 为 100,下意识会认为 CPU 已经严重过载。这个判断并不完整。

Linux 的平均负载不仅包含正在运行或等待 CPU 的任务,也包含处于不可中断睡眠状态的任务。后者通常在 ps 中显示为 D,对应内核中的不可中断等待状态。

这类任务可能正在等待:

  • 本地磁盘或云盘返回 I/O;
  • NFS、Ceph 等远端存储响应;
  • 块设备、SCSI、NVMe 或多路径故障恢复;
  • 文件系统、设备驱动或某条内核等待路径;
  • 极少数情况下的内核缺陷或硬件异常。

需要特别注意:D 状态经常与 I/O 有关,但不能直接等同于“磁盘坏了”。只有继续查看等待通道、内核栈、设备指标和系统日志,才能知道它究竟被什么阻塞。

D 状态任务不持续占用 CPU,因此完全可能出现“Load Average 很高,CPU Idle 也很高”的组合。kill -9 对它也不一定立即生效:信号可以被挂起,但任务要先从不可中断等待中返回,才有机会处理退出。

所以,看到这种现场时,连续执行 kill -9 没有诊断价值,还可能让恢复过程变得更混乱。

别急着动服务,先保存十分钟现场

排查前先记录绝对时间、主机名、内核版本、CPU 数量和工具版本。不同内核、发行版和 sysstat 版本的字段可能存在差异,后面分析时需要这些上下文。

下面均为只读检查:

date -Is
hostnamectl
uname -r
cat /etc/os-release
getconf _NPROCESSORS_ONLN
ps --version | head -n 1
iostat -V 2>&1 | head -n 1

如果系统没有 hostnamectl,可以改用 hostname。如果没有 iostat、pidstat 或 mpstat,通常需要安装 sysstat,但故障期间不要为了安装工具随意修改生产节点,先使用系统已有的 /proc、ps、vmstat 和日志完成基础采证。

接着确认负载变化和 CPU 使用情况:

uptime
cat /proc/loadavg
vmstat 1 10

uptime 和 /proc/loadavg 展示最近 1、5、15 分钟的平均负载。负载不会在故障解除后瞬间归零,因此判断恢复时要结合趋势,而不是只看某一个数字。

vmstat 中重点看这些字段:

  • r:正在运行和等待 CPU 的任务数量;
  • b:被阻塞的任务数量,可用于交叉判断阻塞是否严重;
  • us、sy:用户态与内核态 CPU 占用;
  • id:CPU 空闲比例;
  • wa:I/O wait 比例;
  • si、so:换入、换出活动。

如果 r 不高、b 很高,同时 CPU 的 id 仍然较高,就应该把注意力从 CPU 容量转向阻塞任务。

但不要把 wa 较低当作“没有 I/O 问题”。I/O wait 的统计语义有限,远端文件系统、设备超时和部分内核等待未必会表现为很高的 wa。它只能作为证据之一,不能单独排除存储问题。

如果节点支持 PSI,可以继续读取压力信息。PSI 自 Linux 4.20 引入,但是否可用还取决于内核配置;没有相应文件就跳过。

cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io

io 中的 some 表示至少有任务因 I/O 停顿,full 表示所有非空闲任务同时被 I/O 卡住。avg10、avg60、avg300 是对应时间窗口内的停顿时间占比,不是单次 I/O 延迟。

PSI 更适合观察“业务因为资源压力停了多久”,但它仍然不能告诉我们是哪一个设备或挂载点出了问题。

找到 D 状态后,沿着等待路径往下追

ps aux 只看进程级状态,容易漏掉多线程程序内部被卡住的线程。我更习惯直接查看线程:

ps -eLo state,pid,tid,ppid,wchan:32,comm |
awk 'NR == 1 || $1 ~ /^D/'

还可以单独统计 D 状态线程数量:

ps -eLo state= |
awk '$1 ~ /^D/ {n++} END {print n+0}'

这里的 PID 是线程组对应的进程号,TID 是具体线程号,wchan 是内核等待位置。重点关注三个问题:

  • D 状态是否集中在同一个业务进程;
  • 多个线程的 wchan 是否相同;
  • 异常线程是否都访问了同一个设备、文件系统或远端存储。

如果大部分任务集中在 nfs、rpc、块设备或文件系统相关等待路径,方向就比较明确。如果 wchan 显示为 0 或 -,可能是权限、内核配置或安全限制导致,不能据此认为没有阻塞。

有 root 权限时,可以在受控范围内读取一个代表性线程的内核栈:

sudo cat /proc/<PID>/task/<TID>/wchan
sudo cat /proc/<PID>/task/<TID>/stack

这是只读操作,但内核符号可能暴露系统实现细节,输出应存放在受控位置,脱敏后再进入工单或复盘。不同内核版本的函数名也可能不同,不要只凭某个函数名就宣布根因。

如果怀疑本地块设备,继续检查设备与文件系统:

lsblk -o NAME,TYPE,SIZE,FSTYPE,MOUNTPOINTS
findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS
iostat -xz 1 10

iostat 的第一组数据通常是开机以来的累计平均值,重点分析后续采样。需要关注吞吐量、await、队列长度和 %util 的变化,并与健康时基线对比。

await 明显上升且队列不断增长,说明请求完成时间和排队情况正在恶化。但 %util 接近 100% 不等于所有设备都已经达到理论吞吐上限,尤其在并行能力较强的 NVMe、存储阵列和虚拟块设备上,不能只靠这个字段做容量结论。

如果怀疑 NFS,先确认挂载来源与参数:

findmnt -t nfs,nfs4 -o TARGET,SOURCE,FSTYPE,OPTIONS
nfsstat -m

系统安装了 NFS 客户端统计工具时,可以限时采样:

nfsiostat 1 5

重点看 RPC 积压、重传、往返时间和操作完成时间是否持续恶化。字段名称会随工具版本变化,应结合当前输出表头判断。

内核日志是另一条关键证据:

journalctl -k --since "2026-10-08 13:40:00" \
  --until "2026-10-08 14:20:00"

查询窗口要覆盖告警前几分钟,并重点检查 not responding、timed out、I/O error、reset、blocked for more than、nvme、scsi、nfs 等信息。

日志量较大时应先限定时间,不要在故障节点上无边界扫描所有历史日志。

一个负载 94、CPU 仍空闲的典型现场

下面用一个脱敏后的典型场景说明,时间、地址、实例数量和指标均为示例数据,不对应具体公司或客户。

14:07,某业务集群开始出现请求超时。24 个应用副本中,有 7 个集中在同一台 16 核节点上。该节点的负载为:

load average: 94.31, 61.08, 25.47

但 vmstat 连续采样显示,CPU Idle 仍在 80% 左右,等待 CPU 的 r 并不高,b 却在 60~75 之间波动。

初步判断曾指向 CPU 不足,但这个假设很快被否定:CPU 没有繁忙,运行队列也没有持续堆积。内存 PSI 没有异常,本地块设备的 iostat 与健康节点基本一致。

线程级检查发现,有 71 个线程处于 D 状态,它们属于同一组 Java 工作进程,wchan 集中在 NFS/RPC 相关等待路径。代表性线程的内核栈同样指向远端文件读取等待。

随后,内核日志出现了类似信息:

nfs: server 10.0.18.24 not responding, still trying

nfsiostat 显示该挂载点的 RPC 重传持续增加,平均往返时间显著高于故障前基线。与之对照,另一可用区运行相同版本应用的节点没有 D 状态堆积,访问同一业务数据的耗时正常。

这时,证据链已经比较完整:

  • 高负载来自大量不可中断等待任务,而不是 CPU 运行队列;
  • D 状态线程集中在读取共享目录的工作进程;
  • 等待通道、内核栈和日志均指向 NFS/RPC;
  • 本地磁盘、内存和健康节点对照没有相同异常;
  • NFS 重传与接口超时在同一时间窗口上升。

进一步核对变更记录后发现,故障前十分钟调整过该可用区到存储网段的路由策略。回滚这项策略后,NFS 连接逐步恢复,D 状态线程陆续返回,业务超时停止。Load Average 因为是滑动平均值,又过了一段时间才恢复到日常范围。

真正的根因不是“Linux 负载太高”,也不是“Java 线程太多”,而是存储网络路径异常导致远端文件访问被阻塞。单纯扩容计算节点,只会让更多实例走进同一条故障路径。

止损可以快,但每一步都要能退回来

如果业务有冗余容量,临时止损通常是把异常节点从流量路径中摘除,避免新请求继续进入阻塞现场。具体操作取决于负载均衡器和调度平台,执行前必须确认健康节点能够承接流量。

在 Kubernetes 环境中,先确认集群上下文、节点上的工作负载和 PodDisruptionBudget:

kubectl config current-context
kubectl get node <node> -o wide
kubectl get pod -A --field-selector spec.nodeName=<node> -o wide
kubectl get pdb -A

保存节点和 Pod 清单后,再把节点设为不可调度:

kubectl get node <node> -o yaml > node-before.yaml
kubectl get pod -A --field-selector spec.nodeName=<node> \
  -o wide > pods-before.txt
kubectl cordon <node>

cordon 只阻止新 Pod 调度,不会驱逐现有 Pod,回退方式是:

kubectl uncordon <node>

如果需要迁移现有工作负载,应先确认剩余容量、PDB、无控制器管理的 Pod、emptyDir 数据和本地持久卷。可以先预览:

kubectl drain <node> \
  --ignore-daemonsets \
  --dry-run=client

确认风险后才执行受限的驱逐:

kubectl drain <node> \
  --ignore-daemonsets \
  --timeout=10m

这里故意不使用 --force 和 --delete-emptydir-data。前者可能删除没有控制器接管的 Pod,后者可能丢失本地临时数据。D 状态还可能导致 Pod 无法按时终止;一旦超时,应停止强制操作,重新评估,而不是继续扩大破坏范围。

已经被驱逐的 Pod 不会因为 uncordon 自动搬回原节点。uncordon 只恢复节点的调度资格,因此它不是完整的业务回滚。完整回退还要结合工作负载副本、流量入口和发布系统执行。

根因修复也要遵守同样原则:

  • NFS 故障应优先恢复服务端或网络路径,不要直接对繁忙挂载点执行强制卸载;
  • 不要为了让进程“能被杀掉”就把 NFS 从 hard 改成 soft,错误的软挂载策略可能把长时间等待变成应用错误,特定场景下还存在数据完整性风险;
  • 不要在没有厂商指导和冗余验证的情况下删除块设备、重置控制器或分离云盘;
  • 重启节点只能作为证据保存后的兜底止损,且要提前确认业务冗余、文件系统恢复方案和带外控制能力。强制重启不可真正回滚,还可能带来文件系统或数据损坏。

修复后不要立即全量恢复流量。先在一个节点或一小部分流量上验证:

uptime
vmstat 1 10
ps -eLo state= |
awk '$1 ~ /^D/ {n++} END {print n+0}'
cat /proc/pressure/io
iostat -xz 1 10

同时观察业务错误率、延迟分位数、存储重传、设备队列和内核新日志。若错误率重新上升、D 状态再次累积,或者存储超时仍在增加,应立即停止放量,把节点重新摘除或 cordon,并恢复上一版网络、挂载或设备配置。

这份排查清单,建议直接收藏

  • 不要看到 Load Average 高就立即扩容。 先对照 CPU Idle、运行队列和阻塞任务,否则可能把实例扩进同一个故障依赖。
  • 务必按线程维度检查 D 状态。 多线程程序可能只有部分线程被卡住,仅看进程级状态容易漏掉关键现场。
  • 不要把 D 状态直接定性为磁盘故障。 NFS、云盘、文件系统、驱动和设备恢复路径都可能产生不可中断等待。
  • 不要反复执行 kill -9。 任务未从内核等待返回前通常不会立即退出,连续发送信号不能修复底层依赖。
  • 务必记录绝对时间和近期变更。 没有统一时间窗,就无法把负载、内核日志、存储指标和网络变更串成证据链。
  • 不要只看 wa 或 %util 一个指标。 单一字段不足以证明或排除 I/O 故障,必须结合队列、延迟、PSI、日志和健康节点对照。
  • 禁止无边界抓取日志、堆栈或全盘扫描。 故障节点资源已经紧张,大范围采集可能进一步放大影响,也可能泄露路径和业务信息。
  • 驱逐、卸载、设备重置和重启前必须确认冗余。 这些操作可能中断服务或损坏数据,执行前要保存证据、明确停止条件和回退动作。
  • 恢复流量必须从小范围开始。 D 状态数量、业务延迟和存储错误稳定后再逐步放量;异常复现就立即退回隔离状态。
  • 复盘时要保留被证伪的方向。 CPU、内存、本地磁盘为什么被排除,与最终根因同样重要,否则下次还会重复走弯路。

下次再遇到“负载高、CPU 闲、进程杀不掉”,可以立即做一件事:运行线程级 ps 命令,确认 D 状态数量及其 wchan 是否集中。只要先把“谁在等、等在哪里”说清楚,后面的止损与根因修复才不会靠猜。

如果这套排查路径对你有帮助,欢迎关注、点赞、在看,并转发给需要处理 Linux 生产故障的同事。

公众号:耕云躬行录

个人博客:躬行笔记

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码