服务器CPU占用率高怎么解决

结论: 服务器 CPU 占用率高要先分清是哪一类占用:用户态 us 高多半是应用计算密集或死循环,用 top → pidstat -u → perf top 逐级找到热点函数;内核态 sy 高要看系统调用、上下文切换与网络包量;wa 高说明瓶颈其实在磁盘 IO,优化 CPU 没用;si(软中断)高通常是网卡小包风暴。定位到线程后用 jstack 或 arthas 抓 Java 线程栈,用 perf record 抓 C/C++ 热点,再针对性优化而非简单重启。

为什么 CPU 占用率高要先分类再看

很多人的第一反应是"CPU 满了,加配置吧"。但 CPU 占用率高(High CPU Usage)从来不是一种病,而是一组症状。同样的 90% 占用,背后可能是完全不同的事情:

  • 业务真的增长了,计算量就是这么多——此时加配置是对的;
  • 某个正则回溯导致死循环——加配置只是把崩溃时间推后;
  • 磁盘慢导致大量进程在等 IO——换 CPU 毫无意义,要换 SSD;
  • 网卡被小包打爆,CPU 全在处理软中断——这是网络问题,不是算力问题。

看懂分类只需要一个命令输出:top 第三行那串 Cpu(s): 40.0 us, 8.0 sy, 0.0 ni, 45.0 id, 6.0 wa, 0.0 hi, 1.0 si, 0.0 st。这几个百分比决定了后续完全不同的处置路线,因此排查 CPU 的第一步永远是看这四个值而不是急着杀进程。

另一个容易忽略的点是平均值的欺骗性。监控上 15 分钟平均 60% 看起来很健康,但可能每分钟都有一波 2 秒的 100% 尖峰导致请求排队。top 默认刷新 3 秒一轮,要看瞬时峰值请用 top -d 0.5 或直接用 sar 数据回溯。

第一步:用 top / htop / pidstat 锁定进程与线程

结论先行:从机器到进程用 top,从进程到线程用 top -Hp 或 pidstat -tu,这是 CPU 排查固定的两级套路。

bash
# 基础查看,按 1 展开每个逻辑核,按 P 按 CPU 排序
top

# 更友好的交互界面(需安装)
htop

# 只看 1 次快照,便于脚本采集
top -bn1 | head -20

# 每核 CPU 使用率(看是否存在单核打满)
mpstat -P ALL 1 5

# 按线程维度看某个进程内部谁在吃 CPU
top -Hp 12345

# pidstat 更精确:每 2 秒采样,连续 5 次,含线程
pidstat -u -p 12345 2 5
pidstat -tu -p 12345 2 5       # -t 展开线程

四个典型场景的判定要点:

  • 单核 100% 但总占用只有 12%(8 核机器):典型的单线程瓶颈,如 Node.js 单进程、Nginx 单 worker 死循环、MySQL 单条慢 SQL。加核数没用,要解决串行问题或用多实例 + 负载均衡。
  • 多核均匀高:正常计算密集,考虑扩容或优化算法。
  • 进程数暴涨导致负载高:看 Tasks 行的 running 数与 load average,大量进程在 fork 会让负载远超核数。
  • st(steal)值持续高于 5%:云服务器被超卖了,宿主机上的邻居在抢你的 CPU,应考虑迁移实例或更换实例规格族。

拿到 PID 后,立刻把线程栈留痕,为后续分析保存现场:

bash
PID=12345
# 保存此刻各线程 CPU 占用
ps -Lp $PID -o tid,pcpu,comm --sort=-pcpu | head -15

# Java 进程:抓线程栈(连抓 3 次,间隔 3 秒)
for i in 1 2 3; do jstack -l $PID > /tmp/jstack-$PID-$i.log; sleep 3; done
# 或用 jcmd(JDK 8+ 推荐)
jcmd $PID Thread.print > /tmp/thread-$PID.log

CPU 占用类型决策表

结论先行:这张表是整个排查过程的核心,先按 top 的 us/sy/wa/si 四个值定位方向,再按对应行执行动作。

类型全称与含义典型阈值常见原因处置动作
ususer,用户态程序执行持续 > 70%应用计算密集、正则回溯、序列化/加密开销、GC 频繁perf top 找热点函数;Java 用 async-profiler;考虑算法优化或扩容
sysystem,内核态持续 > 20%频繁系统调用、上下文切换、大量短连接、网络包量高vmstat 看 cs/in;strace -c 统计系统调用;优化连接复用或调大包大小
ninice,低优先级用户态一般接近 0手工 renice 的批处理任务可用 renice 限制后台任务优先级
waiowait,等待 IO持续 > 20%慢磁盘、数据库全表扫描、日志同步写、swap本质是 IO 问题,用 iostat -x 1 看 %util 与 await,换 SSD 或优化 SQL
hihardware irq,硬中断通常 < 1%网卡、磁盘控制器中断极少见,多为驱动异常
sisoftirq,软中断持续 > 5%小包网络风暴(UDP/DDoS)、大流量下网卡无多队列cat /proc/softirqs;开 RPS/RFS 或用多队列网卡;接入高防
ststeal,被宿主机抢占持续 > 5%云服务器超卖、宿主机繁忙保留 sar 数据作为证据,联系厂商迁移或换实例

配套命令:

bash
# 综合看上下文切换与中断
vmstat 1 5
# 关键列:r(运行队列) b(不可中断睡眠) cs(上下文切换/秒) us sy wa st

# 全局上下文切换速率
pidstat -w 1 3

# 软中断分布,看 NET_RX 是否异常增长
watch -n1 'cat /proc/softirqs'

# 磁盘 IO 是否成为瓶颈(wa 高时用)
iostat -xzm 1 5

第二步:用 perf 挖到函数级热点

结论先行:知道哪个进程还不够,要知道哪个函数在吃 CPU。Linux perf(perf_events)是原生的采样分析器(Sampling Profiler),按 CPU 周期采样,能把占用精确到函数名和调用栈。

bash
# 安装(不同内核包名略有差异)
sudo apt install -y linux-tools-common linux-tools-$(uname -r)     # Ubuntu
sudo dnf install -y perf                                          # RHEL/Rocky

# 实时看系统级热点函数(类似 top,但粒度到函数)
sudo perf top -g

# 针对某个进程采样 30 秒并生成报告
sudo perf record -F 99 -g -p 12345 -- sleep 30
sudo perf report --stdio | head -50

# 生成火焰图所需的折叠栈
sudo perf script | ./stackcollapse-perf.pl > out.folded
./flamegraph.pl out.folded > cpu.svg

perf 在容器里常见问题:

  • 报 Permission denied:宿主机执行 sysctl -w kernel.perf_event_paranoid=1 或 -1,容器需 --cap-add SYS_ADMIN --security-opt seccomp=unconfined。
  • 报 [stack] / unknown:目标程序没编译帧指针,需要带 -fno-omit-frame-pointer 重新编译,或用 --call-graph dwarf。
  • 虚拟机里 perf 拿不到硬件计数器:改用 -e cpu-clock 软件事件采样。

第三步:Java / Node.js / Python 的语言级定位

结论先行:perf 对解释型语言和 JIT 语言效果有限,Java 应用应改用 jstack 抓线程栈或 arthas / async-profiler;Node.js 用 --cpu-prof;Python 用 py-spy。

Java 排查是最高频的场景,jstack 的标准用法:

bash
# 1) 找到吃 CPU 的线程 ID(十进制)
PID=$(pgrep -f 'java.*app.jar' | head -1)
top -Hp $PID
# 假设得到 线程 TID=14567 占 98%

# 2) 转成十六进制,jstack 里线程号是十六进制
printf '%x\n' 14567     # => 38e7

# 3) 在线程栈里找 nid=0x38e7
jstack -l $PID > /tmp/stack.log
grep -A 30 'nid=0x38e7' /tmp/stack.log

如果线程栈顶反复出现 Pattern$Curly.match、HashMap.getNode、JSON.toJSONString、或者一段不断自我调用的同一个方法,基本就是元凶:正则回溯、哈希碰撞、超大对象序列化、死循环递归。

Arthas 更省事(阿里开源,截至 2026 年仍是 Java 在线诊断首选工具):

bash
# 下载并 attach 到目标进程
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar      # 交互选择要诊断的 Java 进程

# 进入 arthas 控制台后:
thread -n 3                    # 直接列出 CPU 占用最高的 3 个线程及栈
thread --all | grep -i BLOCKED # 查锁竞争
dashboard                      # 实时面板,含线程、内存、GC
profiler start                 # 采样 CPU
profiler stop --format flamegraph --file /tmp/cpu.html   # 生成火焰图
trace com.example.OrderService query '#cost>100'         # 追踪慢方法

其他语言的等价做法:

bash
# Node.js:生成 cpuprofile 后用 Chrome DevTools 打开分析
node --cpu-prof --cpu-prof-dir=/tmp/prof app.js

# Python:无需改代码的采样分析器
pip install py-spy
py-spy top --pid 12345
py-spy record -o /tmp/py.svg --pid 12345

# PHP:安装 tideways/xhprof 扩展做函数级统计

常见误区 / 排错提示

  1. 一看 CPU 高就重启服务:重启会销毁现场,问题必然复现且下次更难抓。正确顺序是留痕(top/ps/jstack/perf 输出)→ 定位 → 优化,实在来不及则用脚本自动保留现场后再重启。
  2. 把 wa 高当成 CPU 不足去扩容:iowait 本质是 CPU 空闲等着磁盘返回,加固态硬盘或优化 SQL 才有效,加 CPU 核心数几乎零收益。
  3. 忽略 load average 与 CPU 率不一致:CPU 只有 20% 但负载高达 30,说明大量进程卡在不可中断状态(D 状态),通常是存储故障、NFS 挂死或磁盘坏块,看 ps aux | awk '$8 ~ /D/'。
  4. 只看整机不看单核:8 核机器上单线程死循环只有 12.5% 的占用率,监控大盘完全看不出来,但业务已经卡死。必须 top 按 1 看每核。
  5. 容器里看到的是宿主机核数:top 在容器里显示宿主机所有 CPU,而 CPU limits 只限制了配额。判断要以 cgroup 的 cpu.max 或 Kubernetes metrics 为准。
  6. 把 GC 当正常开销放着不管:Java 进程 GC 线程长期占 CPU 20% 以上,说明堆设置不合理或存在内存泄漏,应抓 heap dump 分析而不是简单调大堆内存。
  7. perf 直接跑生产而不限制时长:perf record 采样会产生较大文件,应加 -- sleep 30 限制时长并把输出写到空间充足的分区。

常见问题(FAQ)

CPU 占用多少算高,需要马上处理吗?

不是数字本身,而是持续时间和影响。短时(1 分钟内)冲到 90% 不必紧张,业务有波峰很正常;持续 5 分钟以上超过 85%、或已经导致请求超时、排队上涨,就必须处理。建议按业务设阈值:核心接口场景警戒线 70%,离线批处理可放宽到 90%。

wa(iowait)高但磁盘看起来不忙怎么办?

先看 iostat -x 1 的 %util 与 await:%util 接近 100% 且 await 超过 20 ms(SSD 正常应低于 5 ms),说明设备确实被压满,这是真实的磁盘瓶颈;若 %util 很低而 wa 依然高,说明进程等待的不是本地磁盘,而是网络存储(NFS / Ceph / 云盘)的往返延迟或元数据锁,要去查挂载参数、网络质量与存储服务端状态。第三种常见原因是少数进程做同步阻塞写——比如日志逐行 flush、单线程 rsync——它们占不满 %util,却会让发起者长时间卡在 IO 上。判断依据是用 ps aux | awk '$8 ~ /D/' 找出处于不可中断睡眠的进程,再用 iotop -o 定位具体是谁在 IO。确认后优先改缓冲写、换 SSD 或优化 SQL,加 CPU 核数对 iowait 几乎零收益。

云服务器 CPU 一直 100% 但 top 里进程加起来不到 20%?

看 top 里的 st(steal)值,若持续高于 5% 说明宿主机资源被抢占(超卖)。另一可能是看的是容器内的 top,实际消耗在宿主机或其他容器。处理办法是保存 sar -u 1 60 数据作为证据,联系云厂商迁移实例或升级到独享型规格。

一个 8 核的机器,为什么单核 100% 就卡死了?

因为单线程任务只能跑在一个核上。典型场景是 Nginx 单个 worker 处理慢请求、Node.js 单进程事件循环被同步代码阻塞、MySQL 单条慢 SQL 只能用一个线程。解决办法是横向多实例(起 8 个 Node 进程用 PM2 cluster 模式)+ 负载均衡,而非升级 16 核。

perf 抓不到 Java 的函数名只有地址怎么办?

因为 Java 方法由 JVM 在运行时动态编译产出,perf 找不到对应的符号表(symbol table),只能显示十六进制地址。解决办法有三档:一是在 JVM 启动参数里加 -XX:+PreserveFramePointer,让 JIT 保留帧指针,perf 才能展开完整调用栈;二是安装 perf-map-agent,为运行中的进程生成 /tmp/perf-.map 符号映射文件,perf 读取后即可还原 Java 方法名;三是直接用 Arthas 或 async-profiler——它们内建 JVM 支持,profiler start 与 thread -n 3 能直接出火焰图和热点线程,对绝大多数 Java 应用来说这是成本最低的选择。需要注意的是,perf-map-agent 要求目标进程拥有写 /tmp 的权限,在容器里使用还要确认 PID 命名空间与宿主机一致,否则生成的映射文件路径对不上。

临时降 CPU 占有什么最快手段?

先用 renice 19 -p 降低非核心任务优先级保住关键服务;若是 Web 类可在 Nginx 层对该接口做限流(limit_req);杀掉爬虫/攻击流量可用 firewalld 临时封 IP。这些都是治标手段,事后必须用决策表回溯真正原因。