凌晨两点半,手机屏幕上跳出第三十四条告警,内容从“CPU使用率超过95%”变成了“主机失联”。我揉着眼睛爬起来,打开笔记本连上跳板机,第一件事不是看监控面板,而是直接敲下 top——这是所有运维遇到“机器卡死”时最本能的动作。如果你也经历过类似的夜晚,或者正在为手头某台CPU飙到100%的服务器发愁,这篇文章就是写给你的。它不会教你背命令,而是把从“发现卡死”到“揪出元凶”再到“止损恢复”的完整排查链路拆开揉碎,让你下次遇到同样问题时,能少走弯路,别在告警风暴里手足无措。
CPU 100%不算罕见,但“卡住了”这三个字背后有太多歧义:是整机完全无响应,还是某个服务请求超时?是用户态进程算满了CPU,还是内核态资源被耗尽?不同的表现对应完全不同的排查方向。下面我按自己惯用的排查顺序,把整个诊断过程一步步展开,每一步做什么、为什么这么做,都交代清楚。
1. 先分清“卡死”的三种真实面貌,再决定要不要冲上去
接到“CPU 100%”的告警后,我从不直接杀进程,而是先花一两分钟确认故障现象。经验告诉我,很多“CPU打满”其实是表象,底层原因差异巨大,动手前没搞清楚状况,可能把小问题搞成大事故。
1.1 整机无响应 vs 单服务卡顿,处理方式完全不同
- 整机完全卡死:鼠标动不了、SSH连不上、ping偶尔通偶尔不通。这种情况通常是CPU资源被完全耗尽,或者内核层面出了问题(比如IO死锁、内存耗尽触发OOM后系统疯狂swap)。处理时要先想办法进系统,进不去就只能通过带外管理(如IPMI、iDRAC)重启,或者用云控制台强制重启,风险最大,要谨慎。
- SSH还能连,但执行命令很慢:这是最常见的“半卡死”状态。
top能打开但刷新很慢,ls都要等好几秒。说明系统还有残余资源在处理你的请求,通常是一个或几个进程把CPU吃满了,系统在“抢”时间片。这种情况是排查的黄金窗口,一定要稳住,别急着敲一堆命令加重负载。 - 单个服务响应超时,但机器整体负载不高:有时候告警来自应用监控(比如Nginx 502、Java接口超时),而登上去看
top发现CPU才20%。这种“假CPU告警”可能是应用线程阻塞、连接池耗尽、锁竞争导致的,CPU只是背锅侠。虽然标题是CPU100%,但实操时也常有这类干扰项,我会顺便提一句,免得你被误导。
1.2 先看 load average,它是“卡死”程度的体温计
登录进系统后,第一件事是 uptime 或 top 左上角的 load average:
code复制load average: 8.41, 6.32, 3.78
这三个数字分别代表1分钟、5分钟、15分钟的平均负载。load average 不等于CPU利用率,它是处于可运行状态和不可中断状态的进程平均数量。如果服务器是4核,load average长期大于4,说明任务排队严重——这就是“卡”的直接原因。
- 1分钟负载 > 5分钟负载 > 15分钟负载:说明负载正在上升,故障正在发生。
- 三个数字都很高且接近:说明系统已经高负载持续了一段时间,需要尽快处理。
- 15分钟负载很高但1分钟降下来了:可能已经在恢复中,可以再观察一会。
判断好负载水平后,下一步才是真正定位进程。我见过不少同事一上来就 kill -9,结果杀错了进程导致业务中断,这就是没分清故障性质的后果。记住:第一步永远是观察,不是动手。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. top 第一屏的信息量,比你想象的更大
很多运维用 top 就直接看进程列表里的CPU列,这没错,但太浪费了。第一屏信息量极大,够挖出很多线索。
2.1 从 top 切换出真正的“CPU大户”进程
进入 top 后,默认按CPU降序排列吗?不一定。有些系统默认按PID排序。所以第一件事是按下键盘 P(大写P),按CPU使用率排序。这时候进程列表顶部基本就是元凶候选者。
观察CPU列的数字:
- 单个进程CPU超过100%(比如显示300%):说明它在多核并行运行,是个多线程程序,消耗极大。
- 多个进程CPU都是90%-100%:可能是fork了大量子进程的程序,比如PHP-FPM、Java的线程池扩张、或者一个崩溃循环疯狂拉起的worker进程。
- CPU列不高但load很高:说明不是CPU算力不够,而是大量进程在等待IO或锁,这种情况看
wa列和D状态进程。
top里还有个细节容易忽略——%Cpu(s)那一行的 us、sy、wa、st:
us高:用户态程序在跑,通常是业务代码死循环或复杂计算。sy高:内核态消耗过高,可能是系统调用频繁、上下文切换爆炸。wa高:IO等待,CPU其实在等硬盘/网络,需要查磁盘和网络。st高:虚拟化环境被宿主机超卖,CPU时间片被抢走。
这一行指示了排查方向,不要跳过。
2.2 先别急,用 top 的“累积模式”和“线程模式”看清结构
如果 P 排序后看到的进程很多,可以按下 c 切换完整命令行,看看进程对应的程序路径和参数——有时候光看进程名(比如 java、nginx)根本看不出是哪个业务,加上命令行能定位到具体的配置文件、工作目录或用户。
我常用的组合操作:
- 按
x高亮当前排序的列,按b加粗,屏幕会更清楚。 - 按
1查看每个CPU核心的使用情况,看是所有核都忙,还是单核打满。单核打满的场景常见于老旧的单线程应用、或者某段没做好并发的代码;所有核都忙则一般是整体计算压力太大。 - 按
H进入线程模式,可以看到每个线程的CPU占用。对Java、Python这类多线程应用,这一步能直接暴露是哪个线程在狂转。
bash复制# 如果不习惯top交互模式,也可以用命令行方式一次拿到结果
top -b -n 1 | head -50
-b 是批处理模式,-n 1 表示只输出一次。这样拿到的是快照,适合留档备案。如果要盯实时变化,还是交互模式好用。
3. 顺着进程ID挖到“代码级”的元凶,才算真正定位
找到CPU占用最高的PID只是第一步。作为运维,机器卡死了要能给开发一个交代:到底是哪个服务、哪段逻辑把CPU吃满的。这一章讲怎么从进程挖到线程,再挖到具体代码位置。
3.1 用 top -Hp 定位“肇事线程”,别盯着主进程发呆
假设 top 显示 PID 2468 的Java进程CPU占用200%。直接 kill 这个Java进程?那今晚的变更窗口就没了。先看看是哪个线程在里面作妖。
bash复制top -Hp 2468
这会显示该进程内部的所有线程,同样按CPU排序。找到一个CPU接近100%的线程号,比如 2581。注意了,这里有个坑:top 里显示的线程号是Linux的TID(线程ID),而Java的jstack生成的线程栈里用的是十六进制的nid(native thread id)。要把2581转成十六进制:
bash复制printf "%x\n" 2581
# 输出: a15
然后用这个十六进制去找 jstack 里对应的线程:
bash复制jstack 2468 | grep -A 20 "nid=0xa15"
-A 20 表示显示匹配行后面20行,足够看到线程栈了。栈里面一般会明确告诉你线程在干什么,比如正在执行某个SQL查询、正在做GC、或者卡在某段JSON序列化代码里。
3.2 不只是Java:Python、Node.js、C++ 的线程定位思路
- Python:如果CPU高的是
python3,用py-spy dump --pid <PID>可以直接拿到Python级别的线程栈,不需要重启进程或装额外agent。注意py-spy需要sudo权限,而且对某些编译过的扩展模块支持一般。 - Node.js:可以通过
kill -SIGUSR1 <PID>触发诊断,或者用node --prof-process分析CPU profile。不过生产环境不建议随便发信号,最好确认下你们的Node版本和运维规范。 - C/C++ 或未知二进制:用
perf top直接看内核给出的采样调用栈,这个后面细说。
还有一种常见情况:线程在等锁。jstack里你会看到 waiting for monitor lock 或者多个线程处于 BLOCKED 状态。这种时候CPU整体未必高,但如果有线程在自旋锁,就会看到CPU飙高,需要把相关锁信息丢给开发,让他们去查代码里的锁竞争逻辑。
3.3 GC 风暴:Java进程CPU 100%里最经典的原因
Java服务打满CPU,有一半概率是GC导致的。当堆内存不足时,JVM会反复触发Full GC,GC线程占满CPU,但业务线程几乎停滞。
去确认的方法:
bash复制# 观察GC日志,看是否有频繁的Full GC
jstat -gcutil 2468 1000 5
如果看到 FGC 次数在几秒内不断上涨,而 FGCT(Full GC耗时)也在增长,基本就是GC风暴。这时候不能光盯着CPU了,要看内存:用 jmap -heap 2468 看堆内存使用情况,或者 jstat -gccapacity 看各代容量。
GC风暴的处理方向不是kill进程,而是调大堆内存、排查内存泄漏、或者临时加机器。你要做的是把证据链路(线程栈、GC日志、堆内存快照)完整保留下来,然后联系开发一起看。
3.4 perf 出场:当代码全在“黑洞”里时
有些场景下,你既没源码也拿不到堆栈(比如一个编译过的老二进制),或者看到的线程栈全是系统调用看不出来业务逻辑。那就上 perf:
bash复制perf top -p 2468
perf top 会采样CPU执行的热点函数,直接显示在内核态和用户态的栈帧。虽然是采样,不一定100%精确,但能快速看出热点集中在哪个模块、哪个函数。比如你看到 do_sys_open、loop 之类,就知道是文件操作频繁;看到 memcpy 或 copy_user_enhanced_fast_string,说明是大流量数据拷贝——这些对判断问题方向极有帮助。
提示:有些云主机默认未安装
perf,包名一般是linux-tools-common或linux-perf。装完可能还需要匹配内核版本,遇到perf not found for kernel x.x.x报错时,把对应版本的linux-tools也装上。
4. 一句话定位的进阶工具链:pidstat、strace、lsof
遇到老江湖,其实不需要 top 层层下钻。很多问题用一两把“手术刀”就直接切到核心了。这一章列几个我常用的工具,按场景选用。
4.1 pidstat:看进程的实时行为轨迹
pidstat 是sysstat包里的工具,比top更适合自动化排查。它可以直接查看进程的用户态/内核态CPU占用、上下文切换次数、以及线程级别的统计。
bash复制# 每1秒输出一次PID 2468的CPU、上下文切换、线程状态
pidstat -p 2468 -u -w -t 1 5
-u:CPU使用率-w:上下文切换(cswch/s:自愿切换,nvcswch/s:非自愿切换)-t:线程级别
自愿切换多:说明程序在等待锁、IO等资源,主动让出CPU,可能是阻塞型问题。
非自愿切换多:说明时间片被抢占,说明系统CPU整体紧张,进程在抢资源。
这一数据能辅助判断是单点问题还是系统性问题。
4.2 strace:看进程卡在哪个系统调用上
如果进程CPU高但看不出业务逻辑,可以 strace 跟一下系统调用:
bash复制strace -p 2468 -c -f -o /tmp/strace_2468.log
-c 汇总统计系统调用耗时和次数,-f 跟随子进程,跑10秒后 Ctrl+C,然后看统计文件:
bash复制cat /tmp/strace_2468.log
# % time seconds usecs/call calls errors syscall
如果 read、write、epoll_wait、futex 之类的耗时特别高,说明程序在频繁做IO或线程切换。不过生产环境上strace要慎用,-p 附加到高负载进程时,可能让CPU进一步飙升。建议用 -c(汇总模式)而不是无休止地输出每一个调用,并且控制采样时长,比如10秒就够。
4.3 lsof + /proc/PID:查进程到底打开了什么
有时候CPU 100%的根源是进程在处理大量文件或网络连接。用 lsof -p 2468 | wc -l 看看文件描述符数量是否异常,如果开了几万个文件描述符,很可能是在遍历某个大目录或者日志句柄泄漏。
补充一个 /proc 下的冷门大杀器:
bash复制cat /proc/2468/status | grep -E "Threads|voluntary|nonvoluntary"
cat /proc/2468/schedstat
/proc/2468/status 能看到线程数量和上下文切换累计值,配合 schedstat 能看到进程消耗的CPU时间总量,在排查“到底跑了多久”时很好使。其实很多“运维老手”的快捷键就是一个 cd /proc/PID 加 ls -la,能把进程的cwd、fd、exe链接都看一遍。
5. 从单进程视角跳出来:容器、虚拟化、全局监控的盲区
排查CPU高负载时,还有一类坑是“看了一圈没发现问题”。比如 top 显示整机CPU不高,但服务就是卡;或者容器内看到CPU 100%,宿主机上却只有50%。这种时候要跳出单个进程,从系统架构层面找原因。
5.1 容器场景:是谁在偷走你的CPU时间片
容器里执行 top 看到的是宿主机视角还是容器视角?大多数 top 看到的是宿主机的整体CPU情况,容器内的进程看到的 /proc 并不是隔离完整的。如果你直接在容器里 top -p,显示的CPU%可能是容器CPU限制的百分比,也可能被宿主机整体负载拉低,容易误判。
正确姿势:
bash复制# 在宿主机上看容器进程对应的宿主PID
docker inspect --format '{{.State.Pid}}' <container_name>
# 然后在宿主机上top/top -Hp那个PID
top -p <宿主PID>
还可以用 docker stats 看容器的CPU配额使用率:
bash复制docker stats --no-stream
注意 docker stats 显示的是容器相对于配额的使用率。如果容器limit是0.5核,显示100%说明它已经把配额吃满,即使宿主机整体才20%,你也必须优化这个容器。
容器还有一个经典参数 --cpu-shares 或 --cpuset-cpus 配置不当的问题:某容器没限制,在流量高峰期把宿主机整体拖垮。排查时一定要登录宿主机看整机负载,而不是只盯着出问题的那个容器。
5.2 虚拟化层:CPU steal time是SaaS用户的“隐藏税”
云服务器上经常看到 %Cpu(s) 这行有 st 值。st 是 steal time,表明宿主机上的其他虚拟机抢占了你这台VM的CPU时间片。
如果 st 长时间超过10%,说明你的云主机CPU资源被“偷走”了,业务卡顿根本不是自己代码的问题,而是云厂商宿主机超卖或邻居“吵闹”。这种情况处理起来最憋屈:你没法在系统层面解决,只能提工单给云厂商,或者迁移实例。vmstat 也可以观察 st 列:
bash复制vmstat 1 5
# 输出里的 st 列就是 steal time
如果发现是这个问题,建议在架构上做高可用,避免单点——比如双活部署、跨可用区容灾,因为整机迁移或宿主机抢修时,你的服务才能不中断。
5.3 监控告警工具不是摆设:用历史数据定位“从什么时候开始坏的”
很多人遇到CPU问题只盯当前状态,但我建议一定打开监控平台看趋势图。有些故障是渐变式的,比如内存泄漏导致GC逐渐频繁,CPU是慢慢爬上去的;有些是突发的,比如定时任务凌晨3点触发,瞬间20倍流量。看趋势图能快速缩小范围:
- 找出CPU曲线开始飙升的时间点,对齐到对应时段的发布记录、定时任务配置、数据库慢查询日志。
- 把监控维度拆细:按主机、实例、容器、接口等多维度看CPU,快速定位是单台机器问题还是集群整体扩容问题。
举个例子,有一次排查线上卡顿,top 上一切都正常,CPU不到20%。但监控图显示某台机器15分钟前刚完成扩容,流量分发不均衡,导致CPU全打在一台机器上。这种问题不下钻到监控趋势根本发现不了。
6. 止损的艺术:什么时候kill、什么时候扩容、什么时候等待
定位到元凶后,最要紧的是止损。但怎么止损需要一点判断力,盲目kill可能二次伤害。这里分享几个我基于实际案例总结的决策建议。
6.1 可杀与不可杀的判断标准
如果定位的PID是以下类型,基本可以快速处理:
- 崩溃循环拉起的子进程(比如因配置错误不断fork的php-fpm、uwsgi)。
- 无状态的定时任务,比如日志清理、数据归档脚本,kill掉重启即可。
- 明显由外部触发的小程序、一次性爬虫任务。
如果PID是以下类型,哪怕CPU飙到500%,也要谨慎:
- 核心业务的JVM进程、数据库实例(MySQL/Redis)、消息队列Broker。
- kill会导致数据不一致或需要在恢复时花更长时间回放的中间件。
- 容器内的Pause进程(容器生命周期管理用的,杀了容器就挂)。
一个靠谱的做法:在kill之前记录现场(top -n1 > top_before.txt、ps -ef | grep PID、lsof -p PID),把证据存好。这样就算恢复后业务异常,也有据可查。
6.2 两种止损路径:硬杀 vs 软降级
- 硬杀:
kill -9 <PID>,立刻终止,但可能造成数据文件损坏、连接池瞬间断开、缓存雪崩。适合进程已经无法正常响应、且无状态可恢复的场景。 - 软降级:先重启进程或服务,如果重启后CPU仍打满,就直接摘流量、上黑名单、切流到备用节点。比如Nginx后端某台Tomcat CPU打满,可以先把该节点从upstream里标记
down,让流量切走,再慢慢排查,业务自动恢复。
重启前建议:停流量 > 等存量请求结束 > 优雅重启 > 观察是否恢复 > 再放流量。这一套下来对业务影响最小。
6.3 一次线上CPU打满的真实处置流程复盘
有一次线上订单服务CPU飙到98%,整机负载从2跳到30(16核机器)。我登上去 top 看到进程是Java的订单服务,CPU 800%。top -Hp 发现有两个线程各自CPU逼近200%。jstack 后定位到线程栈在 BigDecimal.divide 和 java.util.HashMap.put 上,疑似死循环。但这没法直接判断是哪个接口触发的,于是我先摘掉该节点流量,再用 jstack 连续抓了三份线程栈,对比发现它们都卡在同一个HashMap的resize上——线程不安全导致的死循环。
这时我才知道,kill -9 很可能是最差的选择,因为它会丢失正在处理的订单请求。正确的操作是:摘流量 -> 用 jcmd <PID> Thread.print 继续采样 -> 让开发确认代码问题 -> 发布修复版本 -> 逐步恢复流量。整个过程里,jstack 抓到的线程栈成了判断“能不能重启”的关键证据。
7. 事后补救:如何把一次CPU故障变成监控体系的“疫苗”
故障解决了不是终点。如果每次CPU打满都靠人肉排查,团队迟早被拖垮。事后做这几步,下次故障恢复时间能缩短50%以上。
7.1 确定性排查脚本:把“经验”沉淀成“工具”
每次线上排障后,我都会把排查套路固化成一个脚本。比如一台机器CPU高,一键输出以下内容:
bash复制#!/bin/bash
# 快捷排查脚本
echo "===== top排序 ====="
top -b -n 1 | head -30
echo "===== 进程列表 ====="
ps -eo pid,ppid,%cpu,%mem,cmd --sort=-%cpu | head -20
echo "===== 负载与CPU状态 ====="
uptime
vmstat 1 3
echo "===== CPU高进程的线程情况 ====="
for pid in $(ps -eo pid --sort=-%cpu | head -5 | tail -n +2); do
echo "--- PID $pid ---"
top -b -H -n 1 -p $pid | tail -20
done
这个脚本不复杂,但能在你慌的时候把最重要的信息一次性抓全。建议放到跳板机的 /usr/local/bin/cpu_check.sh,并赋予执行权限。另外提醒一点:脚本里要有异常判断,比如 ps 输出为空时不要继续往下执行,以免误杀。
7.2 监控告警阈值设置:让告警“早于”卡死出现
不要把CPU告警阈值设成100%才报警——到那时候系统已经卡得连告警都发不出去了。建议在50%时预警、80%时严重告警、95%以上直接走紧急事件流程。还可以设置时长维度,比如CPU连续5分钟超过80%才触发,避免瞬时抖动带来的告警疲劳。
同时加上进程数量监控:比如某主机Java进程数超过阈值、容器CPU limit利用率超过90%、load average大于核数持续10分钟,这些都能在卡死前给到提示。
7.3 建立“CPU高负载”专项SOP,连新人都能照做
一个好的SOP应该包含:故障分级、信息收集、定位流程、止损决策、恢复验证、复盘闭环。把前面的经验全写进去,给新入职的同事看一遍就能上手。
SOP里还有个常被忽视的地方:故障期间的通讯录和运维知识库。CPU卡死时最怕找不到人,联系不上开发、中间件负责人、云厂商技术支持。把关键联系人参在SOP开头,比任何技术都管用。
7.4 经验不等于复现:把故障案例变成压测场景
最后一步可能有些团队觉得“多余”,但对避免同类问题非常有效:把这次故障的场景抽象成一次压测用例。比如如果是HashMap死循环,可以设计一个并发写同一Key的测试接口;如果是GC风暴,可以模拟内存压力触发Full GC。然后放到压测环境里,验证监控告警能否提前发现、自动扩容或熔断能否生效。
我在实际工作中发现,很多CPU故障的根因是代码层面,靠运维单方面改配置解决不了根本。只有把故障转化为可演练的用例,推动研发写单元测试、做代码评审、加接口级监控,才能减少下一轮踩坑。
说回运维本身,CPU 100%排障这件事,说穿了就是“信息收集 -> 定位 -> 止损 -> 复盘”的循环。工具来来回回就那些:top、jstack、pidstat、strace、perf,真正拉开差距的是面对告警时的判断力和冷静程度。希望这篇文章能给你一些启发。下次再遇到CPU卡死时,不妨先深呼吸,按着这套思路来,问题大概率能更快落地。
