结论: 服务器 CPU 占用过高(打满)时,正确顺序是先止血、再定位、后复盘:第一时间限流或重启单个异常服务恢复业务,再用 top → top -Hp PID → jstack/perf top 把问题锁定到具体线程与函数,最后修根因。切忌上来就重启整机——重启会抹掉现场,问题必然复现。
引言:CPU 打满和 CPU 占用率高,是两种不同的处置
很多教程把"CPU 占用率高"和"CPU 打满"当成一件事讲,其实两者的处置逻辑完全不同。占用率 60%~70% 属于优化问题,你有时间慢慢分析、做基准测试、灰度上线;而 CPU 占用过高(尤其是持续 95% 以上、load 超过核数 2 倍)属于故障事件,此时请求已经堆积、SSH 可能都卡得敲不动命令,首要目标是恢复业务而不是找出元凶。
本文聚焦后一种情况:CPU 已经被打满、业务已经受损时的应急处理流程。关于"占用率偏高时的通用排查思路"(us/sy/wa/si 四类占用的含义与决策树),请参考本站《服务器CPU占用率高怎么解决》。两篇是互补关系:那一篇教你分类,这一篇教你救火。
一个必须提前建立的认识:故障现场只存在一次。CPU 打满时最有价值的东西是"此刻哪个线程在烧 CPU",一旦你重启了,这个证据就永远消失了,而绝大多数高 CPU 问题的根因并不会因为重启而消失。所以本文的所有步骤都围绕一个原则设计——在最短时间内保住现场的前提下恢复业务。
第一步:黄金十分钟,先止血再定位
结论:CPU 打满的头十分钟目标只有一个——让业务恢复可用。定位工作可以等业务恢复后再做,但不能在止血前把现场破坏掉。
立刻确认三件事(30 秒内)
# 1) 看负载与 CPU 概览:如果 load average 远超核数,说明已严重排队
uptime
nproc # 逻辑核数,与 load 对比
# 2) 看是哪一类占用:us 高=应用计算,sy 高=内核/系统调用,wa 高=等磁盘,si 高=网络软中断
top -bn1 | head -12
# 3) 找出吃 CPU 的进程 TOP10
ps aux --sort=-%cpu | head -11这三条命令的输出要在第一时间截图或保存,这是后续复盘唯一的现场证据:
# 保存现场:进程快照 + 每核使用率 + 负载,全部落盘
mkdir -p /tmp/cpu-incident-$(date +%Y%m%d-%H%M%S)
cd /tmp/cpu-incident-$(date +%Y%m%d-%H%M%S)
ps aux --sort=-%cpu > ps-snapshot.txt
top -bn3 -d 1 > top-snapshot.txt
mpstat -P ALL 1 5 > mpstat.txt
uptime > uptime.txt止血手段对照表
| 止血手段 | 生效速度 | 业务影响 | 适用场景 | 是否破坏现场 |
|---|---|---|---|---|
| 网关限流(Nginx/QPS 限制) | 秒级 | 部分请求被拒 | 流量突增、爬虫、刷接口 | 否,推荐 |
| 封禁异常 IP / UA | 秒级 | 仅影响攻击源 | 明确的攻击流量、异常爬虫 | 否,推荐 |
| 重启单个异常服务 | 10~60 秒 | 该服务短暂中断 | 单进程死循环、单应用失控 | 部分破坏(该进程现场丢失) |
| 临时降级(关掉非核心功能) | 分钟级 | 功能降级 | 报表、推荐、搜索等高耗模块 | 否,推荐 |
| CPU 配额限制(cgroup) | 秒级 | 该进程变慢但不抢占整机 | 后台任务、批处理抢资源 | 否,推荐 |
| 临时扩容 / 加机器分流 | 分钟~十分钟 | 无 | 真实业务增长 | 否,但成本高 |
| 重启整机 | 1~5 分钟 | 全部中断 | 系统级异常、内核问题 | 完全破坏,最后手段 |
常用止血命令
# 用 systemd 对单个服务限 CPU(例:限制 nginx 最多用 50% 单核)
systemctl set-property nginx.service CPUQuota=50%
systemctl show nginx.service | grep CPUQuota
# 临时用 cpulimit 压制某个已失控的进程(不改配置,立即生效)
cpulimit -p 12345 -l 30 & # 限制该进程 CPU 上限 30%
# 给低优先级后台任务降调度优先级,避免抢占在线业务
renice +19 -p 12345
# 封禁异常来源 IP(先看访问来源,确认后再封)
tail -n 2000 /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20
iptables -I INPUT -s 203.0.113.55 -j DROP
# 重启单个服务而不是整机(保住其它进程现场)
systemctl restart myapp.serviceNginx 侧快速限流的配置片段(改完 nginx -s reload 秒级生效):
# 在 http{} 中定义限流区:按来源 IP 限制,单 IP 每秒 10 个请求
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
server {
location / {
limit_req zone=perip burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}什么时候必须重启整机:只有当系统级资源被耗尽(如内核线程 ksoftirqd 长期 100%、软中断把 CPU 吃光且无法定位来源)、或者 SSH 完全无法登录、或者出现内核 panic 征兆时,才考虑重启。重启前务必执行上面的"保存现场"脚本。
第二步:从进程定位到线程
结论:ps 只能告诉你哪个进程吃 CPU,真正的原因在线程里。定位链条固定为:进程 → 线程 → 线程栈/热点函数。
# 每 1 秒采样一次,持续观察哪个进程的 CPU 在涨
pidstat -u 1
# 展开某个进程内部的所有线程(LWP/TID 列即线程 ID)
top -Hp 12345
ps -Lp 12345 -o pid,tid,pcpu,stat,comm --sort=-pcpu | head -15记下占用最高的线程 TID(十进制),后续两类应用分别处理。
Java 应用:top -H 配合 jstack
结论:Java 进程 CPU 打满的标准做法是把 top -H 里的十进制线程号转成十六进制,再到 jstack 输出里搜对应的 nid。
PID=12345
# 1) 找出该进程内 CPU 最高的线程 TID(十进制)
top -Hp $PID -bn1 | head -15
# 2) 把 TID 转成十六进制(jstack 里的 nid 是十六进制,必须带 0x 前缀)
printf '%x\n' 12350 # 例如输出 303e,则 jstack 中搜 nid=0x303e
# 3) 连续抓 3 次线程栈,间隔 5 秒(一次抓栈说服力不足)
for i in 1 2 3; do
jstack -l $PID > /tmp/jstack-$i.txt
sleep 5
done
# 4) 在工作目录下搜索该线程,直接看它停在哪个方法
grep -A 25 'nid=0x303e' /tmp/jstack-1.txt判断要点:如果三次抓栈里这个线程都停在同一行代码,那就是死循环或热点循环;如果线程状态是 RUNNABLE 且栈里反复出现 java.util.regex.Pattern,大概率是正则回溯(Catastrophic Backtracking);如果大量线程都是 BLOCKED 且等待同一把锁,而 CPU 又被 GC 线程占满,那可能是频繁 GC 导致内存不足的连锁反应,需要先按内存占用过高排查。
# 顺带确认是不是 GC 把 CPU 吃光了
jstat -gcutil 12345 1000 10
# 若 FGC(Full GC 次数)持续上涨、FGCT 占比高,问题本质在内存而非 CPUC/C++ / Go / Rust / Nginx:perf 采样热点函数
结论:原生程序用 perf(Performance Counters for Linux)采样,可以直接看到 CPU 时间落在哪个函数、哪一行。
# 实时看整机热点函数(类似 top,但按函数粒度)
perf top
# 只看某个进程
perf top -p 12345
# 采样 30 秒并生成报告(带调用链,-F 99 是常用采样频率)
perf record -F 99 -p 12345 -g -- sleep 30
perf report --stdio | head -60
# 生成火焰图(需先安装 FlameGraph 工具)
perf script > out.perf
stackcollapse-perf.pl out.perf > out.folded
flamegraph.pl out.folded > cpu.svgGo 程序另有一个更轻量的选择:curl http://127.0.0.1:6060/debug/pprof/profile?seconds=30 -o cpu.prof 然后 go tool pprof cpu.prof,前提是程序开启了 pprof。
第三步:常见高 CPU 成因速查表
结论:CPU 打满的原因高度集中,下面这张表覆盖了线上 90% 以上的情况。
| 成因 | 典型特征 | 快速验证 | 根治办法 |
|---|---|---|---|
| 死循环 / 异常递归 | 单线程长期 100%,三次 jstack 停在同一行 | top -Hp + 多次 jstack 比对 | 修代码逻辑,加循环上限与超时 |
| 正则回溯 | 特定输入触发,栈里反复出现 Pattern/Matcher | 用可疑输入在本地复现 | 重写正则,避免嵌套量词 |
| 频繁 Full GC | jstat 中 FGC 持续上涨,GC 线程占 CPU | jstat -gcutil PID 1s | 按内存泄漏排查,调堆参数与对象生命周期 |
| 慢查询 / 全表扫描 | 数据库进程 CPU 高,伴随磁盘 IO 高 | show processlist / pg_stat_activity | 加索引、改写 SQL、限流 |
| 大量加解密 / 序列化 | 请求量上涨时线性恶化,栈停在 crypto/JSON | perf top 看 libcrypto、json | 引入缓存、改用更快的算法、连接复用 SSL session |
| 日志刷屏 | sy 偏高,磁盘 IO 同步升高 | dmesg、tail -f 看日志速率 | 降日志级别,异步写日志 |
| 网络小包风暴 | si(软中断)高,ksoftirqd 吃 CPU | cat /proc/softirqs 看 NET_RX 增速 | 调网卡多队列、增大 ring buffer、上防 DDoS |
| 被植入挖矿木马 | 陌生进程、境外 IP 长连接、CPU 常驻 90%+ | 见下节 | 隔离、清理、改密钥、补漏洞 |
挖矿木马的专项排查
结论:如果你发现一个陌生进程长期占用 80%~100% CPU,且它对外连接境外 IP 的 3333、8080、14444 等端口,几乎可以断定为挖矿木马(Cryptojacking),处置重点是切断外连 + 清理持久化 + 修补入口漏洞,只杀进程必然复活。
# 1) 看 CPU 占用最高的进程,注意进程名伪装(如 kworker、systemd-daemon、nginxs)
ps aux --sort=-%cpu | head -10
# 2) 查该进程的可执行文件真实路径(/proc/PID/exe 会显示被删除的原始路径)
ls -l /proc/12345/exe
ls -l /proc/12345/cwd
# 3) 查外连:看是否连接矿池
ss -antp | grep 12345
lsof -p 12345 | grep -i established
# 4) 查持久化:定时任务、systemd 单元、启动脚本、SSH 公钥
crontab -l; ls -la /etc/cron.d/ /var/spool/cron/crontabs/
systemctl list-unit-files --state=enabled | egrep -i 'suspect|update|network'
cat /root/.ssh/authorized_keys第四步:根因分析与复盘
结论:止血成功只是把故障从"进行中"变成"待复发"。没有根因修复,同一问题平均会在两周内再次出现。
建议用下面这个固定模板做复盘记录,写进团队知识库:
【故障复盘模板】
1. 影响:时间段 / 影响接口 / 受损请求比例 / 持续时间
2. 时间线:
- HH:MM 监控告警触发(当时告警的是哪个指标)
- HH:MM 人工介入
- HH:MM 执行止血动作 X
- HH:MM 业务恢复
3. 直接原因:哪个进程 / 哪个线程 / 哪段代码
4. 根因:为什么这段代码会在此时出问题(触发条件是什么)
5. 为什么没提前发现:监控缺失了什么指标?压测为什么没覆盖?
6. 改进项(每项要有负责人和截止日期):
- 代码修复
- 补充监控告警(如 CPU > 85% 持续 5 分钟)
- 增加防护(限流 / 超时 / 熔断)
- 预案文档化配套要补的两个监控项:CPU 使用率告警(持续 85% 超过 5 分钟)和load/核数比值告警(超过 1.5 持续 10 分钟)。前者抓突发,后者抓慢性恶化。
常见误区 / 排错提示
- CPU 高了第一反应就是重启:重启能恢复业务,但也销毁了唯一的现场。正确做法是先用
ps/top/jstack快照落盘,再决定重启范围(优先重启单服务)。 - 用
top的默认 3 秒刷新看瞬时尖峰:3 秒一轮会平滑掉 1~2 秒的尖峰。要看瞬时值用top -d 0.5或pidstat -u 1。 - 只看了 %CPU 没看 load:CPU 60% 但 load 30(8 核机器)说明大量进程在排队等 CPU 或等 IO,业务已经卡了。load 与 CPU 要一起看。
- 忽略
%CPU可能超过 100%:多线程进程在 top 里可以显示 200%、400%,这是核数维度的累加,不是异常。 jstack抓一次就下结论:单次抓栈可能正好抓到线程在切换的瞬间。必须间隔 3~5 秒抓 3 次,都停在同一处才算证据。- 容器里 CPU 高但宿主机正常:Docker/K8s 场景下要看
docker stats与 cgroup 限额,容器 CPU 打满可能只是配额给少了,而非应用有问题。 perf报 "Permission denied":需要echo -1 > /proc/sys/kernel/perf_event_paranoid或加 sudo,部分云主机内核不支持 perf 计数器,此时改用jstack/pprof替代。
企业QQ咨询




