CPU 冲到 100% 是运维最常遇到的告警之一,也是最容易手忙脚乱的一种:网站打不开、接口全超时、SSH 敲命令都卡。这时候最忌讳的就是直接重启——重启确实能止血,但它同时销毁了所有证据,下次大概率还会再来一遍。
这篇文章讲的是怎么在几分钟内回答三个问题:是谁在吃 CPU、它在干什么、该怎么处理。内容包括 top 各 CPU 分项的准确含义、进程级到线程级的定位方法、短命进程的抓取手段、Java 应用特有的线程栈分析套路、六类高 CPU 成因的特征与对策,以及生产环境的临时止损手段。
一、先分清是哪一种"100%"
top 头部那行 CPU 数据把 100% 拆成了若干分量,不同分量指向完全不同的原因。这是定位的第一步,也是最容易被跳过的一步——很多人只看总使用率,然后就开始猜。
top
# 进入后按 1 可以展开每个逻辑核的使用率
# 按 P 按 CPU 排序,按 M 按内存排序,按 H 切换线程视图
各字段的准确含义与对应原因:
| 字段 | 含义 | 升高时的典型原因 | 下一步 |
|---|---|---|---|
| us | 用户态 CPU 占比 | 业务计算、加解密、压缩、正则、GC、挖矿 | perf top、top -H |
| sy | 内核态 CPU 占比 | 系统调用频繁、上下文切换多、短连接洪水 | vmstat 看 cs、pidstat -w |
| ni | 改变过优先级的进程占用 | 人为 nice 过的任务 |
确认是否有预期的批处理任务 |
| id | 空闲占比 | — | 越低说明越忙 |
| wa | 等待 IO 的空闲时间 | 磁盘/网络存储慢,加 CPU 无效 | iostat -x 1 |
| hi | 硬中断 | 硬件中断风暴,较罕见 | /proc/interrupts |
| si | 软中断 | 网络包量过大(小包洪水) | sar -n DEV 1 看 pps |
| st | 被虚拟机管理程序偷走 | 宿主机超卖,guest 内无法解决 | 联系云厂商或迁移实例 |
几个重要的判断:
wa 高不是 CPU 问题。 它表示 CPU 闲着但被 IO 阻塞的任务占着时间表。这时候加 CPU 核数毫无用处,要去查磁盘和网络存储。
st 高只有一个解释——宿主机把本该给你的 CPU 时间给了别人。这在超卖严重的共享型实例上常见,解决方案是换实例规格或换宿主机,在 guest 内部做任何优化都没用。
sy 高容易被误判成攻击者行为,但其实很多时候是自己的代码在频繁 malloc/free、做大量的小文件读写、或者开了过多线程导致上下文切换。用 vmstat 1 看 cs 列确认:每秒上下文切换达到几万到几十万,就是这个方向。
vmstat 1
# 重点看:r(运行队列)、cs(上下文切换/秒)、us/sy/id
# 参考:cs 在数千属正常,持续数万以上需要关注
二、进程级定位:谁在吃 CPU
有了方向,下一步是找到具体进程。根据工具是否可用,有三种由简到繁的方式。
# 方式一:top 交互式(最通用)
top -c # -c 显示完整命令行
top -o %CPU # 部分版本支持按字段排序
# 进入后按 P 排序,看 %CPU 列
# 方式二:ps 快照方式,适合写进脚本
ps -eo pid,ppid,user,%cpu,%mem,etime,comm,args --sort=-%cpu | head -20
# 方式三:pidstat 持续采样,能看到趋势
pidstat -u 1 5 # 每秒一次,共 5 次
pidstat -u 1 # 持续输出
pidstat -p PID 1 5 # 只看指定进程
三种方式有个关键区别必须知道:top 的 %CPU 和 ps 的 %CPU 计算口径不同。
top(默认模式)显示的是进程自启动以来的平均 CPU 使用率按刷新间隔采样,实际上它反映的是采样间隔内的占用(IRIX 模式开启时,多核机器上单个进程可以超过 100%,300% 表示用了 3 个核)。ps的 %CPU 是进程生命周期内的平均值,对于刚启动的进程可能很高,跑了三天的进程会被摊薄到很低。
所以排查当前问题时用 top 或 pidstat,不要拿 ps 的数值下结论。pidstat 是最推荐的,因为它给出的是采样区间内的真实占用,且能持续观察趋势。
安装这些工具:
sudo dnf install -y sysstat procps-ng perf # RHEL、Rocky、CentOS
sudo apt install -y sysstat procps linux-tools-common linux-tools-$(uname -r) # Debian、Ubuntu
etime 字段经常被忽略但很有价值:它显示进程已运行的时长。一个启动了 5 秒却占了 90% CPU 的进程,很可能是被反复拉起的异常任务;一个跑了 30 天的进程突然 CPU 高,更可能是触发了某条异常分支或者中了木马。
三、线程级定位:多线程应用必看
现代服务端程序几乎都多线程,top 默认的进程视图会把所有线程的 CPU 加起来,你只能看到"Java 占了 400%",但不知道是哪段代码。
# 查看某进程的所有线程 CPU 占用
top -H -p PID
ps -Lp PID -o pid,tid,%cpu,stat,comm,args --sort=-%cpu | head -20
pidstat -tu -p PID 1 3 # -t 显示线程
注意 ps -Lp 里 tid 列是线程的内核 ID,在 Linux 上线程就是轻量级进程,每个 tid 独立参与调度。top -H 里的 PID 列显示的也是 tid。
Linux 线程 ID 与 JVM 线程栈的对应关系是关键技巧,下面在 Java 章节详述。
如果发现是某个工作线程池里的线程全部打满,通常说明线程池太小导致请求在队列里积压(伴随 CPU 也可能不高),或者是每个线程都在做重计算。前者要调线程池大小,后者要看代码。
四、短命进程的抓取方法
最棘手的一类情况:top 里 CPU 很高,但每次刷新进程名都不一样,或者刷新一次那个高 CPU 进程就消失了。这说明CPU 是被大量频繁创建又快速退出的短命进程消耗的——常见于 CGI 程序、频繁的 shell 脚本调用、cron 里的脚本、攻击者的反弹扫描等。
这类进程在 top 里几乎抓不住,需要专门的工具:
# 方式一:pidstat 高频采样(1 秒一次,仍可能漏掉更短的)
pidstat -u 1 60
# 方式二:perf top,看到的是函数级别的热点,不受进程生命周期影响
sudo perf top
sudo perf top -g # 带调用图
# 方式三:execsnoop(BCC/eBPF 工具),能捕获每一次 exec 系统调用
sudo /usr/share/bcc/tools/execsnoop # Debian 系常见路径
sudo /usr/libexec/bcc/execsnoop # RHEL 系部分版本
# 输出会包含 PID、PPID、返回码、启动的命令,短命进程一览无余
安装 BCC 工具集:
sudo dnf install -y bcc bcc-tools # RHEL、Rocky、CentOS
sudo apt install -y bpfcc-tools # Debian、Ubuntu(包名可能随版本变化)
execsnoop 的输出包含时间戳、PID、PPID 和完整命令行,是捕获"谁在不断fork进程"最直接的工具。发现某个脚本被每秒拉起几十次,顺着 PPID 往上找父进程就能找到源头(经常是 crond 或者某个守护进程)。
还有一个轻量替代:用 forkstat(apt install forkstat),它监听进程 fork/exec/exit 事件,开销比 eBPF 方案小一些,兼容性也更好。
perf 的另一种用法是采样记录,适合需要保存现场的场景:
sudo perf record -F 99 -ag -- sleep 30 # 采样 30 秒,-a 全系统 -g 调用图
sudo perf report # 交互式查看
sudo perf script > /tmp/perf.out # 导出原始采样,可以生成火焰图
风险提示:perf record -a 在高负载机器上有可观开销(采样会写大量数据到磁盘),生产环境尽量缩短采样时间(10~30 秒),并确保 /tmp 或当前目录有足够空间。
五、Java 进程的专项定位
Java 应用 CPU 高在服务端场景特别常见,而且有一套非常成熟的定位套路,核心是把操作系统线程 ID 转换成十六进制,再和 jstack 输出的线程 nid 对应。
# 第一步:找到 Java 进程的 PID
jps -l
ps -eo pid,%cpu,comm,args --sort=-%cpu | grep java | head
# 第二步:找出该进程内 CPU 最高的线程 tid
top -H -p JAVA_PID
# 记下高 CPU 行的 PID 列(这就是 tid,十进制)
# 第三步:把 tid 转成十六进制
printf '%x\n' 12345 # 例如输出 3039
# 第四步:抓线程栈并搜索 nid
jstack -l JAVA_PID > /tmp/jstack.txt
grep -A 30 'nid=0x3039' /tmp/jstack.txt
jstack 输出的每一行线程信息里,nid 就是该线程的内核线程 ID 的十六进制表示。匹配上之后,你就能看到这个线程当前执行到哪个类的哪一行,后面跟着完整的调用栈。
判读经验:
- 多个线程栈顶都在同一个业务方法 → 该处有重计算或死循环。
- 大量线程是
RUNNABLE且栈顶都在java.util.regex.Pattern→ 正则 catastrophic backtracking,典型特征是输入稍微变长 CPU 就指数级上升。 - 栈顶频繁出现
GC task thread、VM Thread→ GC 过于频繁,通常是堆太小或存在内存泄漏。用jstat -gcutil PID 1000确认,如果 FGC(Full GC 次数)每秒都在涨,基本确定。 - 栈顶大量是
socketRead、socketWrite→ 不是 CPU 计算问题,是外部依赖慢或网络问题,CPU 高是大量线程堆积的表象。
# GC 情况快速确认
jstat -gcutil JAVA_PID 1000 10
jmap -heap JAVA_PID # 堆配置与使用情况(会 STW,慎用)
jcmd JAVA_PID Thread.print # jstack 的等价写法,JDK 8+ 推荐
jmap -heap 和 jmap -dump 会触发 Stop-The-World,在生产高峰执行可能造成秒级到数十秒的业务停顿,务必在低峰期操作或先摘流量。JDK 8 之后推荐用 jcmd 系列命令替代。
六、六类常见成因与对策
| 成因 | 特征 | 快速验证 | 处理建议 |
|---|---|---|---|
| 死循环 / 算法复杂度失控 | us 极高,单核或多核 100%,持续不降 | perf top、jstack、py-spy |
修代码,临时重启 + 限流 |
| 正则回溯 | 输入变长时 CPU 突增,栈顶在正则相关函数 | jstack / py-spy / perf | 改写正则,避免嵌套量词 |
| GC 频繁 | GC 线程 CPU 高,FGC 次数快速增长,响应周期性卡顿 | jstat -gcutil |
调堆大小、换 GC 器、查内存泄漏 |
| 加解密/压缩/序列化 | us 高且有明确业务触发时点(如大批量导出) | 代码 review、perf |
异步化、批处理、换算法或加硬件指令 |
| 上下文切换过多 | sy 偏高,vmstat 的 cs 每秒数万 |
pidstat -w 1 |
减少线程数、改长连接、批量处理 |
| 挖矿等恶意程序(安全方向) | 陌生进程、异常外连、CPU 常年高位 | ss -antp、ps auxf、查定时任务与 SSH key |
按安全事件处置:断网取证、清理落盘文件与持久化项、改口令密钥、重装系统最彻底 |
最后一条要特别说明:挖矿木马属于安全事件,不属于性能问题。它的表现确实像 CPU 100%,但如果只盯着"杀掉那个进程",往往过几分钟又起来——因为木马通常有多重持久化(crontab、systemd service、SSH authorized_keys、动态库劫持 LD_PRELOAD、替换系统命令)。正确做法是按入侵响应流程走:先隔离网络、保留证据(ps auxf、ss -antp、/proc/PID/exe 指向、定时任务清单)、清理持久化项、轮换所有密钥和口令、最后考虑重装系统。相关排查细节可以参考本系列的恶意程序排查篇。
判断是否木马的简单三问:这个进程的命令行我能解释吗?它的可执行文件路径正常吗(/proc/PID/exe 是否被删除或指向 /tmp)?它的外连目的地址我认识吗? 三问中有一个答不上来,就按安全事件处理。
# 快速核实进程来源
ps -eo pid,ppid,user,%cpu,etime,comm,args --sort=-%cpu | head
ls -l /proc/PID/exe # 可执行文件真实路径
ls -l /proc/PID/cwd # 当前工作目录
sudo cat /proc/PID/environ | tr '\0' '\n' | head -30 # 环境变量有无异常
# 核实外连与持久化
sudo ss -antp | grep PID
sudo crontab -l -u root && sudo ls -la /etc/cron.d/ /var/spool/cron/crontabs/
sudo systemctl list-unit-files --state=enabled
七、临时止损的正确姿势
找到原因之后如果修复需要时间,先止血。原则是按影响面从小到大,优先不动线上的手段。
第一档:限制资源配额。 用 cgroup 给肇事进程上枷锁,让它不影响其他服务。
# systemd 管理的服务(推荐,cgroup v2 推荐做法)
sudo systemctl set-property --runtime 服务名 CPUQuota=80%
sudo systemctl set-property --runtime 服务名 CPUQuota=150% # 多核可超过 100%
# --runtime 表示重启后失效,去掉该参数会写入 /etc/systemd/system.control/ 持久化
# 查看当前限制
systemctl show 服务名 | grep -i cpu
# cgroup v1 手工方式
cd /sys/fs/cgroup/cpu/ && mkdir -p limited
echo 50000 > /sys/fs/cgroup/cpu/limited/cpu.cfs_quota_us # 50% 单核
echo 100000 > /sys/fs/cgroup/cpu/limited/cpu.cfs_period_us
echo PID > /sys/fs/cgroup/cpu/limited/tasks
第二档:在流量入口限流。 Nginx 侧限制请求速率,把异常流量挡在外面,保护后端。
# /etc/nginx/nginx.conf 的 http 块
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
server {
location /api/ {
limit_req zone=perip burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}
改完先 nginx -t 校验语法再 systemctl reload nginx(reload 不断连接,比 restart 安全)。
第三档:摘流量重启。 单机的情况先从负载均衡摘掉,让请求不再进来,再重启进程。集群场景务必保留一台故障机不做重启,作为取证和根因分析的现场。
关于 kill -9:只对 R/S 状态的进程有效,D 状态进程杀不掉(参考本系列 Load 篇)。另外 kill 之前先用 kill -3(Java)抓取线程栈,或者 gcore 抓转储,保留排查材料。
常见问题
ps 里看到某个进程 1800% CPU,是不是显示错了?
没显示错。在 IRIX 模式(top 默认开启)下,%CPU 是按单个逻辑核归一化的,一个占用 18 个核的多线程进程就会显示 1800%。用 top 时按 I 可以切换 IRIX 模式,关闭后所有进程会按总核数归一化,加起来不超过 100%。判断多核占用建议配合 top 里的每核视图,按 1 展开。
top 显示 CPU 很高,但就是这个进程名看不到,怎么办?
大概率是短命进程。用 execsnoop(BCC 工具集)捕获 exec 调用,或者 forkstat,能看到每一次进程创建。也可以先看 ps -eo ppid 找出高频出现的父进程,从父进程找源头。
为什么 CPU 100% 了网站还能响应,就是慢?
因为还是有部分时间片能分配给处理请求的线程。CPU 100% 不等于完全不可用,而是所有请求的处理时间被拉长,表现为超时增多、排队变长。真正完全不可用通常是因为进程崩溃、连接池耗尽或者被 OOM Killer 杀掉。
perf 显示的是一堆看不懂的函数地址,怎么变成人话?
先安装对应内核版本的 debug 符号包(kernel-debuginfo,RHEL 系用 dnf install -y kernel-debuginfo,需要开启 debug 仓库;Debian 系可以用 apt install linux-image-$(uname -r)-dbg)。对应用层,perf top 显示 ??? 通常是因为缺少符号表,Java 还需要 perf-map-agent 才能翻译 JIT 代码,Python 需要装了 py-spy 之类的工具才好用。
可以给生产 CPU 打满是renice 调低优先级解决吗?
能缓解但不能根治,且容易误伤。renice 只影响调度优先级,高 CPU 进程被降级后会让位给别的进程,但它的计算需求还在,反而会让整体吞吐更低。正确的做法是先找到原因,是异常行为(木马、死循环)就修或杀,是正常计算需求就扩容或优化。
总结
- 定位 CPU 100% 的第一步是拆分
top的 CPU 分量而不是看总数:us 高查应用计算、sy 高查系统调用与上下文切换、wa 高查 IO(加 CPU 无效)、si 高查网络包量、st 高是虚拟机宿主机超卖(guest 内无解)。 - 进程级用
pidstat -u 1持续采样(推荐),不要用ps的 %CPU 下结论,因为它是进程生命周期平均值,长跑进程会被摊薄。ps的etime字段有助于判断进程是"突然变坏"还是"刚被拉起"。 - 多线程应用必须下沉到线程级:
top -H -p PID或ps -Lp PID -o pid,tid,%cpu找出高 CPU 的 tid。Java 用printf '%x\n' tid转十六进制后在jstack输出里搜索对应 nid,直接定位到代码行。 - 抓不到进程时用面向短命进程的工具:
execsnoop(BCC/eBPF)捕获 exec 事件、forkstat监听 fork/exec/exit、perf top看函数级热点。perf record有可观开销,生产环境采样控制在 10~30 秒。 - 六大成因各有特征指纹:正则回溯在栈顶暴露 Regex、GC 频繁表现为
jstat的 FGC 秒级增长、上下文切换过多在vmstat的 cs 列暴露、加解密与压缩则表现为 us 高且有明确的批量触发时点。 挖矿木马属于安全事件,只杀进程会复活,必须按入侵响应流程清理持久化并轮换密钥。 - 止损按影响从小到大:先用
systemctl set-property CPUQuota限资源配额,再在 Nginx 侧limit_req入口限流(nginx -t校验后 reload),最后才考虑摘流量重启。重启会销毁现场,之前务必保存jstack、perf、dmesg、ss -antp的输出。
企业QQ咨询




