兄弟们,做后端和运维的,线上服务CPU持续飙高、load average爆表、接口响应从毫秒级退化到超时,这类告警大家肯定都处理过。
这类事故的核心矛盾就一个:CPU飙高只是表象,背后可能是业务流量暴涨、代码死循环、频繁Full GC、慢SQL拖垮连接池、锁竞争,甚至是被植入了挖矿木马。 必须快速定位到底是哪一种,不能盲目重启掩盖问题。
在动手之前,先把这四条"保命纪律"刻在脑子里,事故后严禁执行以下操作:
- 不要上来就重启服务! 现场信息全丢,重启完根因找不到,问题还会再犯。
- 不要看CPU高就杀掉疑似可疑的进程! 你以为是挖矿木马,结果把核心业务进程杀了。
- 不要在没抓top和日志证据前就盲目扩容或重启! 掩盖问题等于埋雷。
- 不要只看CPU不看load和IO! CPU不高但load爆表,大概率是IO等待或者锁竞争,排查方向完全不一样。
下面直接上干货,完整复盘我们的标准处置SOP。
一、 标准处置流程:先留证据,后止血
记住八字真言:先留证据,再动手 。
完整流程:告警确认 → 现场留证(快照) → 快速止血(判断摘流量还是观察) → 定位根因(进程→线程→代码) → 恢复业务 → 监控回归 → 复盘防再犯。
二、 Linux系统级排查:揪出嫌疑进程
上了机器,先别慌,按顺序敲命令,把系统级快照留下来。
bash
# 1. 看load趋势和运行时间
uptime
# 2. top命令看全局,进去后按大写 P 键,按CPU占用排序
top
# 找到最耗CPU的进程PID,比如是 12345
# 3. 如果top卡死,用ps直接抓快照
ps aux --sort=-%cpu | head -n 20
# 4. 看多核CPU分布,确认是所有核都高,还是单核被打满(单核高大概率是死循环)
mpstat -P ALL 1 5
# 5. 看CPU、IO和上下文切换(us高是用户态,sy高是内核态,wa高是IO等待)
vmstat 1 10
# 6. 判断是不是IO等待导致load高
iostat -dx 1 5
# 7. 看历史CPU使用率,确认是突然飙高还是慢慢爬升
sar -u 1 5
⚠️ 顺便排查是否被挖矿:
bash
# 查异常外联IP
ss -antp | grep ESTABLISHED
# 查陌生定时任务
crontab -l
# 查最近修改的可疑文件
find /tmp /var/tmp -type f -mmin -60
三、 Java应用级排查:定位到代码行
如果确认是Java进程(比如PID 12345)在疯狂吃CPU,继续往下挖。
bash
# 1. 找出这个进程里最耗CPU的线程ID (TID)
top -Hp 12345
# 2. 把线程ID转成十六进制(假设线程ID是 12500)
printf "%x\n" 12500
# 输出: 30d4
# 3. 抓线程栈快照(多抓几次,间隔2秒,方便对比)
jstack 12345 > jstack_1.log
sleep 2
jstack 12345 > jstack_2.log
# 4. 在jstack日志里搜刚才的十六进制线程ID (0x30d4)
grep -A 30 "nid=0x30d4" jstack_1.log
在栈里找线索:
- 看到
RUNNABLE且一直在业务代码里循环:大概率是死循环。 - 看到
BLOCKED或WAITING在等锁:大概率是锁竞争。 - 看到大量
GC task thread:大概率是频繁Full GC。
辅助诊断命令:
bash
# 看GC次数和耗时(每秒刷一次,看FGC次数和耗时是不是在狂飙)
jstat -gcutil 12345 1000
# 看堆内对象统计(按实例数排序,看是不是某个对象泄漏了)
jmap -histo 12345 | head -n 20
# 【⚠️生产高危】千万别在生产随便用 jmap -dump:format=b,file=heap.hprof,全量dump极易引发二次Full GC直接把服务搞挂!
# 如果装了Arthas,直接用神器
# 查看最忙的前3个线程
thread -n 3
# 生成CPU火焰图,一眼看出哪行代码在烧CPU
profiler start
profiler stop --format html
Java常见根因总结:
- Full GC频繁:老年代满,内存泄漏或大对象突增。
- 死循环:代码逻辑Bug,或者正则表达式回溯(单核CPU打满)。
- 锁竞争:synchronized或ReentrantLock粒度太大,线程全在排队。
- 慢SQL拖垮连接池:数据库慢查询导致线程全阻塞在获取连接上。
- 线程池耗尽:下游服务超时,当前服务线程池队列打满,疯狂创建线程或拒绝。
四、 中间件场景排查差异
有时候CPU高的不是业务应用,而是中间件。
1. MySQL CPU飙高
- 成因:慢SQL(没走索引、全表扫描)、复杂JOIN、并发太高。
- 排查 :登进MySQL执行
show processlist;看State和Time。去查慢查询日志(slow log),拿到SQL后用explain看执行计划。
2. Redis CPU飙高
- 成因 :大Key操作(如
hgetall一个几MB的Hash)、热Key集中访问、缓存穿透/击穿/雪崩、复杂命令(如keys *)。 - 排查 :
redis-cli --hotkeys找热Key,redis-cli --bigkeys找大Key,slowlog get 10看慢日志。用info clients看连接数是不是打满了。
3. Nginx CPU飙高
- 成因:并发连接数过高、keepalive配置不当、后端响应太慢导致Nginx worker阻塞、遭遇CC攻击。
- 排查 :看
access.log的QPS和IP分布。用strace -p <worker_pid> -c看系统调用耗时。检查worker_connections和keepalive_timeout参数。
五、 快速止血与恢复(保命操作)
定位需要时间,但业务不能一直挂着,必须快速止血。
哪些情况必须立即摘流量或重启?
- 服务已经OOM边缘,或者发生死锁完全无响应。
- 确认是下游依赖彻底挂了,当前服务跟着拖死。
- 注意:重启前,务必把刚才的jstack、jstat、heap histo、慢SQL日志打包发给自己或传到OSS!
哪些情况必须先抓现场再动?
- CPU高但服务还能勉强响应,接口没大面积超时。
- 还没抓到jstack和线程快照。
摘流量的落地方法:
- 网关/负载均衡踢节点 :在Nginx、APISIX或微服务网关层,把故障节点的权重调为0,或者直接从注册中心下线,千万别直接拔网线或kill进程。
- Kill慢查询 :如果是MySQL慢SQL导致,写个脚本循环
kill掉show processlist里Time超过阈值的连接。 - 接口限流:在网关层对非核心接口开启限流,保住核心交易链路。
六、 验证与上线(别急着下班)
根因修复、代码发版后,别以为就万事大吉了。
- 确认指标回落 :盯着监控,看CPU使用率降到正常水位,
load average回到核数以内。 - 接口延迟回归:看APM(如SkyWalking/Pinpoint)或网关监控,接口P99延迟回到基线水平。
- 灰度放量观察:先放10%流量,没问题再50%、100%。
- 持续盯盘:发版后至少盯盘半小时,重点看GC频率和load,确认没有复发再收工下班。
七、 事后预防(填坑闭环)
救火只是治标,复盘防再犯才是治本。
- 监控告警阈值:CPU > 80% 持续3分钟告警,接口P99 > 500ms告警,FGC > 1次/分钟告警。
- 压测与容量规划:大促前必须做全链路压测,摸清系统水位,提前扩容。
- 慢SQL治理:上线前必须过SQL审核,严禁无索引查询,定期跑慢SQL巡检报告。
- 线程池/连接池参数:根据压测结果合理设置核心线程数、最大线程数和队列大小,拒绝默认配置打天下。
- JVM参数调优:合理设置新生代/老年代比例,选择合适的垃圾回收器(如G1/ZGC),避免频繁Full GC。
- 限流降级熔断:核心接口必须配Sentinel或Resilience4j,下游挂了要能优雅降级,不能跟着一起死。
八、 总结:处置高频踩坑血泪教训
最后,总结几个兄弟们最容易踩的坑,希望大家别再交学费:
- 上来就重启丢现场:重启一时爽,排查火葬场。重启前不保留jstack和heap,根因永远是个谜。
- 把业务进程当挖矿进程误杀 :看到个不认识的进程名直接
kill -9,结果把核心网关杀了。杀进程前一定要ls -l /proc/<pid>/exe看清楚。 - 只看CPU不看IO和GC:CPU 10%但load 50,你去查代码有个屁用?那是磁盘IO瓶颈或者锁等待。
- 慢SQL没抓到就下结论:数据库一慢,就拍脑袋说是网络问题。没看执行计划和慢日志前,别乱甩锅。
- 靠加机器掩盖问题没找根因:CPU高了就加机器,结果代码里的死循环或者内存泄漏没解决,机器加到100台照样被打挂,成本还翻倍。
线上排查拼的就是肌肉记忆和标准动作。希望这篇实战复盘能帮大家在遇到CPU飙高时少走弯路,精准定位。
如果你觉得这篇文章对你有帮助,或者在排查时有什么独门秘籍,欢迎点赞、收藏、转发!在评论区聊聊你遇到过最坑的CPU飙高原因,我们一起交流避坑!