结论: 服务器 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 排查固定的两级套路。
# 基础查看,按 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 后,立刻把线程栈留痕,为后续分析保存现场:
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.logCPU 占用类型决策表
结论先行:这张表是整个排查过程的核心,先按 top 的 us/sy/wa/si 四个值定位方向,再按对应行执行动作。
| 类型 | 全称与含义 | 典型阈值 | 常见原因 | 处置动作 |
|---|---|---|---|---|
us | user,用户态程序执行 | 持续 > 70% | 应用计算密集、正则回溯、序列化/加密开销、GC 频繁 | perf top 找热点函数;Java 用 async-profiler;考虑算法优化或扩容 |
sy | system,内核态 | 持续 > 20% | 频繁系统调用、上下文切换、大量短连接、网络包量高 | vmstat 看 cs/in;strace -c 统计系统调用;优化连接复用或调大包大小 |
ni | nice,低优先级用户态 | 一般接近 0 | 手工 renice 的批处理任务 | 可用 renice 限制后台任务优先级 |
wa | iowait,等待 IO | 持续 > 20% | 慢磁盘、数据库全表扫描、日志同步写、swap | 本质是 IO 问题,用 iostat -x 1 看 %util 与 await,换 SSD 或优化 SQL |
hi | hardware irq,硬中断 | 通常 < 1% | 网卡、磁盘控制器中断 | 极少见,多为驱动异常 |
si | softirq,软中断 | 持续 > 5% | 小包网络风暴(UDP/DDoS)、大流量下网卡无多队列 | cat /proc/softirqs;开 RPS/RFS 或用多队列网卡;接入高防 |
st | steal,被宿主机抢占 | 持续 > 5% | 云服务器超卖、宿主机繁忙 | 保留 sar 数据作为证据,联系厂商迁移或换实例 |
配套命令:
# 综合看上下文切换与中断
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 周期采样,能把占用精确到函数名和调用栈。
# 安装(不同内核包名略有差异)
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.svgperf 在容器里常见问题:
- 报
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 的标准用法:
# 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 在线诊断首选工具):
# 下载并 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' # 追踪慢方法其他语言的等价做法:
# 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 扩展做函数级统计常见误区 / 排错提示
- 一看 CPU 高就重启服务:重启会销毁现场,问题必然复现且下次更难抓。正确顺序是留痕(top/ps/jstack/perf 输出)→ 定位 → 优化,实在来不及则用脚本自动保留现场后再重启。
- 把
wa高当成 CPU 不足去扩容:iowait 本质是 CPU 空闲等着磁盘返回,加固态硬盘或优化 SQL 才有效,加 CPU 核心数几乎零收益。 - 忽略
load average与 CPU 率不一致:CPU 只有 20% 但负载高达 30,说明大量进程卡在不可中断状态(D 状态),通常是存储故障、NFS 挂死或磁盘坏块,看ps aux | awk '$8 ~ /D/'。 - 只看整机不看单核:8 核机器上单线程死循环只有 12.5% 的占用率,监控大盘完全看不出来,但业务已经卡死。必须
top按1看每核。 - 容器里看到的是宿主机核数:
top在容器里显示宿主机所有 CPU,而 CPU limits 只限制了配额。判断要以 cgroup 的cpu.max或 Kubernetes metrics 为准。 - 把 GC 当正常开销放着不管:Java 进程 GC 线程长期占 CPU 20% 以上,说明堆设置不合理或存在内存泄漏,应抓 heap dump 分析而不是简单调大堆内存。
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- 符号映射文件,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。这些都是治标手段,事后必须用决策表回溯真正原因。
企业QQ咨询




