你的网站突然卡成PPT,后台一看CPU直接干到100%?别急着敲reboot——这就像闻到焦味就拔插头,火灭了,但灶上那锅烧干的水你永远不知道是哪一刻开始冒烟的。
先稳住,打开终端,我们一条命令一条命令地找。
第一步:先别重启,立刻抓取现场快照
CPU飙高时第一反应是重启?停手。你真正需要的不是“恢复”,而是“证据”。
马上登录服务器,敲:
top
按 1 看各核心负载,按 P 按CPU使用率排序。盯住最上面那三五个进程:是 java?php-fpm?mysqld?还是个叫 update.sh 的陌生名字?
记下它的 PID(最左列数字)和完整命令行(COMMAND 列可能被截断,按 c 键展开看全)。
接着补一刀:
ps aux | grep -v grep | grep [PID]
把 [PID] 替换成你刚记下的数字。这能看清它是谁启动的、跑在哪个用户下、用了多少内存、启动参数长啥样。
我见过最典型的“隐形凶手”:一个每5分钟跑一次的清理脚本,因为路径写错,每次执行都失败但不退出,子进程越积越多,最后CPU被拖垮。要是没记下父进程ID(PPID),重启后连日志都没处翻。
真实案例:某本地生活平台,每天晚8点准时变慢。运维习惯性重启,问题次日复现。后来要求值班同事在卡顿时立刻截图 top 和 ps aux 输出。发现一个 convert 进程(ImageMagick)长期霸占单核。一查日志,是用户上传了200MB的扫描件PDF,系统试图转成缩略图,结果卡死在解码环节,后面请求全堆在队列里。
第二步:深入分析“元凶”进程在做什么
确认是 Java 进程占 CPU?别猜,直接“提审”。
先用 top 找到它的 PID,再执行:
top -H -p [PID]
这会列出该Java进程下所有线程的CPU占用。找到那个数值最高的线程PID(注意:这是操作系统级线程ID,十进制)。
把它转成十六进制(Linux下可用 printf "%x\n" [线程PID]),然后导出线程快照:
jstack -l [PID] > jstack.log
打开 jstack.log,搜索你刚算出的十六进制ID。匹配到的那一段,就是正在疯狂吃CPU的线程堆栈。
重点看两处:
- 线程状态是不是
RUNNABLE(不是WAITING或BLOCKED)? - 堆栈最顶上几行是不是在反复调某个方法?比如
JSONObject.parse()、String.replaceAll()、或者一个没设退出条件的while(true)?
常见情况:有次发现一个订单导出接口,每次请求都会新建一个 ObjectMapper 实例,而这个实例初始化要加载上百个模块——接口QPS一上来,CPU就拉满。改成 Spring Bean 单例注入后,问题消失。
第三步:数据库很可能是幕后黑手
如果 top 里 mysqld 或 postgres 排第一,别怪应用,先查SQL。
连上数据库,执行:
SHOW PROCESSLIST;
或(MySQL 5.7+):
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
FROM information_schema.PROCESSLIST
WHERE TIME > 30 AND COMMAND != 'Sleep';
重点关注 TIME 超30秒、STATE 是 Sending data / Sorting result / Copying to tmp table 的那些。
挑出最可疑的一条,前面加 EXPLAIN 再执行一遍:
EXPLAIN SELECT * FROM orders WHERE user_id = ? AND status = 'paid';
看输出里的 type 字段:
ALL?说明全表扫描,赶紧加索引。rows数百万?哪怕有索引,也可能选错了字段顺序。- 出现
Using filesort或Using temporary?说明排序/分组没走索引。
真实案例:某CMS后台管理页,每次打开都触发一条 SELECT * FROM posts 查询。表有300万条记录,又没加 LIMIT。开发以为“只是查后台,影响不大”,结果定时任务一并发调用,MySQL CPU直接锁死。加了 WHERE created_at > DATE_SUB(NOW(), INTERVAL 7 DAY) 后,负载回落。
第四步:检查系统日志和恶意进程
有些“高CPU”根本不是你的服务干的——是别人偷偷塞进来的。
先快速扫一眼有没有异常:
tail -30 /var/log/messages
# 或 Ubuntu 系统:
tail -30 /var/log/syslog
重点搜 Failed password、Accepted publickey、systemd[1]: Started 后面跟着不认识的服务名。
再看那个高CPU进程打开了什么:
lsof -p [PID]
如果看到它连着境外IP、或者打开了 /tmp/.X11-unix/ 下的奇怪socket,基本可以确定是挖矿程序。
顺手检查定时任务:
crontab -l
ls -la /etc/cron* /var/spool/cron/
尤其注意 /etc/cron.d/ 下有没有名字像 sysupdate、watchdog 的文件,内容里藏着 curl http://.../xmr.sh | bash 这种。
一个简单有效的命令:
ps aux --sort=-%cpu | head -20
配合 ls -l /proc/[PID]/exe 查进程真实路径(很多木马会伪装成 kthreadd,但实际路径在 /tmp 或 /dev/shm)。之前帮朋友查一台跳板机,就靠这招揪出一个藏在 /dev/shm/.logd 的门罗币矿工。
第五步:如何监控与设置预警?
等CPU打满才出手,等于等房子烧穿了才想起灭火器在哪。
你现在就在用的工具,大概率已经装了 netdata 或 htop——它们都能实时看CPU,但缺个“提前喊你”的功能。
如果你用的是阿里云ECS、腾讯云CVM,直接打开云控制台的「云监控」页面,找到对应实例,开启「CPU使用率」报警规则:
- 触发条件:过去5分钟平均值 > 75%
- 通知方式:勾选你常用的钉钉群机器人(别用邮件,容易漏看)
不需要装Prometheus,不用配Grafana。云厂商的监控Agent早就跑在你机器上了,就差你点几下。
如果公司自建Zabbix或夜莺(Nightingale),也一样——去告警策略里加一条“CPU持续超阈值”,关联你负责的业务标签。关键是:让告警消息出现在你刷钉钉的间隙里,而不是凌晨三点弹窗吓醒你。
今天下班前就能做的一个具体操作
现在,请打开你负责的、最重要的那台服务器的 SSH。别等出问题,现在就做:
- 输入
top,按1看各核心负载,按q退出; - 输入
ps aux --sort=-%cpu | head -10,扫一眼前10名,确认都是你认识的进程; - 找到你的主应用进程(比如
java或node),记下它的 PID; - 如果是 Java,输入
jstack [PID] | head -50(只看前50行,避免卡住); - 登录数据库,运行
SHOW PROCESSLIST;,快速过一遍有没有Time超60秒的; - 最后,输入
tail -20 /var/log/messages,看看最近有没有systemd启动异常服务的记录。
全程5分钟。把这6条命令复制粘贴到你的终端,再复制一份存进企业微信「我的收藏」里。下次告警一响,你手指不用思考,就能顺着这条路径摸到根上。