服务器CPU使用率100%怎么解决?定位高占用进程的实用技巧

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 的输出。