线上CPU飙到100%?这套排查方法论建议收藏,关键时刻能救命
CPU异常飙升这个问题,说大不大说小不小,有时候就是一个死循环,有时候是一个隐藏很深的GC问题,有时候甚至是一行正则表达式搞的鬼。但不管根因是什么,排查思路其实是有套路的。今天我就把这几年在生产环境中积累的CPU排查经验整理出来,从告警接入到最终定位,全流程走一遍。
先搞清楚:CPU到底在忙什么
很多人收到CPU告警的第一反应是直接top看一眼哪个进程吃CPU,然后就去翻代码了。这个习惯不能说错,但效率不高。
CPU使用率这个指标本身是个很粗粒度的东西,你得先搞清楚是哪部分在消耗CPU。Linux里top命令展示的那几个指标——us、sy、wa、si、hi,每一个都代表不同的含义。
us(user)是用户态CPU时间,你的业务代码跑在用户态,如果us高,说明是业务代码本身在烧CPU。sy(system)是内核态CPU时间,如果sy异常高,可能是系统调用太频繁,或者内核处理中断太忙。wa(iowait)是等待IO的时间,严格来说wa高不代表CPU忙,而是CPU闲着没事干在等磁盘。si(softirq)是软中断,网络收发包、定时器这些都会触发软中断。
我之前遇到过一个案例,top看CPU使用率80%多,第一反应是业务代码有问题,结果仔细一看sy占了60%多,us才十几。顺着sy高这条线往下查,发现是一个定时器间隔设得太小,500微秒触发一次,内核疯狂处理定时器中断。最后把定时器间隔改成1ms,CPU直接降了一半。
所以拿到CPU告警,第一件事不是急着翻代码,而是搞清楚CPU时间花在哪了。
排查工具箱:从粗到细的武器库
2.1 top / htop —— 快速定位
top命令大家都会用,但有几个小技巧值得说一下。
# 按CPU排序,只看前20
top -bn1 | head -20
# 看某个具体进程的线程级CPU
top -H -p <PID>-H这个参数很关键,它能展开到线程级别。有些Java应用你用top看进程CPU不高,但用top -H一看,某个线程CPU吃满了,这种情况下进程级别的数据是有欺骗性的。


2.2 mpstat —— 各核CPU详情
mpstat -P ALL 1 3
这个命令能看到每个CPU核心的使用率,包括usr、sys、iowait、softirq等细分指标。如果发现某个核CPU特别高而其他核正常,那可能是单线程瓶颈或者中断都绑在一个核上。
# 看软中断详情
mpstat -I SCPU 1 5
# 看硬中断详情
mpstat -I CPU 1 5
这两个命令在排查网络中断、定时器中断类问题时特别有用。之前那个定时器的案例,就是通过mpstat -I SCPU发现HRTIMER软中断从正常的200次/秒飙升到2000多次/秒,才锁定了问题方向。
2.3 pidstat —— 进程级CPU分析
# 每秒输出一次,输出5次
pidstat 1 5
# 针对特定进程
pidstat -p <PID> 1 5
pidstat比top好的地方在于它能持续输出,方便你观察CPU使用率的波动趋势。而且它还能区分usr和system时间,对判断问题方向很有帮助。
2.4 strace —— 系统调用追踪
当你发现某个进程sy(内核态)CPU高的时候,strace是直接有效的工具。
# 追踪进程的所有系统调用,统计耗时
strace -c -p <PID>
# 输出示例:
# % time seconds usecs/call calls errors syscall
# ------ ----------- ----------- --------- --------- ----------------
# 45.23 0.045123 45 1002 3 futex
# 23.15 0.023115 23 998 read
# 15.67 0.015670 31 500 write-c参数会统计每个系统调用的次数和耗时,一眼就能看出哪个系统调用在烧CPU。不过要注意,strace本身有性能开销,生产环境慎用,特别是高QPS的服务。
2.5 perf —— 性能分析神器
perf是Linux内核自带的性能分析工具,可以说是CPU排查的核心武器。
# 实时查看CPU热点函数
sudo perf top -p <PID>
# 采样30秒
sudo perf record -F 99 -g -p <PID> -- sleep 30
# 查看采样报告
sudo perf report --stdio -g fractal
# 指令级分析某个热点函数
sudo perf annotate --symbol=<function_name>perf top适合快速看一下当前CPU在执行什么函数,perf record则是采样后做深度分析。
关于采样频率-F 99,为什么不用100?因为100Hz容易和系统的定时器产生共振,导致采样偏差。99Hz是个经验值,Brendan Gregg推荐的。
调用栈展开方式有三种:fp(帧指针,开销最小但需要编译时加-fno-omit-frame-pointer)、dwarf(不需要特殊编译参数但开销5-15%)、lbr(Intel硬件支持,零开销但调用栈深度有限)。生产环境我一般用lbr,开发环境用fp。
2.6 火焰图 —— 可视化利器
perf report的文本输出虽然信息量大,但看起来确实费劲。火焰图把这些信息可视化成一张SVG图,哪个函数吃CPU一目了然。
# 生成火焰图的三步走
sudo perf record -F 99 -g -p <PID> -- sleep 30
sudo perf script > perf.txt
./stackcollapse-perf.pl perf.txt > folded.txt
./flamegraph.pl folded.txt > flame.svg火焰图怎么看?横轴是采样比例(可以理解为CPU时间占比),纵轴是调用栈。越宽的函数占用的CPU时间越多。找到最宽的那个"平顶",沿着调用栈往下找,就能定位到是哪段代码在烧CPU。
火焰图的配色也有讲究。标准的CPU火焰图用暖色调(红橙黄),Off-CPU火焰图用冷色调(蓝绿),这样一眼就能区分是CPU繁忙还是IO等待。
三、不同语言的不同套路
3.1 Java应用
Java应用的CPU排查有自己的特点,因为JVM本身就是个复杂的运行时。
# 第一步:找到CPU高的Java进程
top -bn1 | grep java
# 第二步:找到CPU高的线程
top -H -p <PID>
# 第三步:把线程ID转成16进制
printf "%x\n" <TID>
# 第四步:用jstack抓线程栈
jstack <PID> | grep -A 30 <十六进制TID>这套组合拳是Java CPU排查的标准流程。拿到线程栈之后,看这个线程在干什么,是执行业务代码、GC、还是锁等待。
如果是GC导致的CPU高,jstat -gcutil <PID> 1000能看GC频率和各代内存使用情况。Full GC频繁的话,CPU不飙才怪。
还有个工具叫async-profiler,比jstack好用很多,能直接生成火焰图,强烈推荐。
# async-profiler生成火焰图
./profiler.sh -d 30 -f flame.html <PID>3.2 Go应用
Go的CPU分析用pprof就够了。
# 如果应用已经集成了pprof
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# 在pprof交互界面
(pprof) top 10 # 看CPU消耗最多的10个函数
(pprof) web # 生成调用图
(pprof) list <func> # 看某个函数的代码级耗时Go的pprof还可以用-http参数启动一个web界面,直接在浏览器里看火焰图,体验很好。
3.3 Python应用
Python应用可以用cProfile或者py-spy。
# py-spy生成火焰图,不需要修改代码
py-spy record -o flame.svg --pid <PID>py-spy的好处是不需要侵入代码,直接attach到进程上就行,生产环境也能用。
几个真实案例
案例一:正则表达式导致的CPU 100%
这个案例印象特别深。一个风控服务,平时CPU也就20%左右,某天突然飙到100%。top看是Java进程,jstack抓线程栈发现一个线程卡在Pattern.matcher上。
代码里有一段正则校验逻辑,用来校验用户输入的邮箱格式。正常输入没问题,但有个用户输入了一串超长的特殊字符,触发了正则表达式的回溯灾难(ReDoS)。那个正则写得有问题,存在嵌套量词,面对特定输入时复杂度变成指数级。
修复方案很简单,换一个不存在回溯问题的正则表达式,或者限制输入长度。但这个问题排查过程花了不少时间,因为谁也没想到一个正则能把CPU干满。
案例二:死循环导致的CPU飙升
这个比较经典。一个数据处理服务,某天CPU持续100%。perf采样后生成火焰图,发现CPU全耗在一个while(true)循环里。
代码逻辑大概是这样的:从队列里取任务,处理完继续取。正常情况下队列有数据就处理,没数据就阻塞等待。但某次代码改动,把阻塞取改成了非阻塞取,队列空的时候也不sleep,直接疯狂空转。
// 问题代码
while (true) {
Task task = queue.poll(); // 非阻塞,队列空返回null
if (task != null) {
process(task);
}
// 没有else分支,队列空时CPU空转
}
// 修复后
while (true) {
Task task = queue.poll(1, TimeUnit.SECONDS); // 阻塞等待1秒
if (task != null) {
process(task);
}
}这种问题在代码review时其实很容易发现,但在生产环境排查时,如果没有火焰图,光看top和日志很难定位。
案例三:Full GC导致的CPU飙升
一个Spring Boot应用,CPU周期性飙升,每隔大概30秒就飙一次然后降下来。
jstat看GC情况,发现每隔30秒就有一次Full GC,每次Full GC耗时2-3秒,这期间CPU直接拉满。
进一步排查发现是老年代空间设置太小了,大对象直接进老年代,很快就触发Full GC。调大堆内存和新生代比例后,Full GC频率降到了几分钟一次,CPU恢复正常。
GC导致的CPU问题有个特征:CPU是周期性波动的,不是持续高。看到这种模式,第一时间查GC。
案例四:锁争用导致的多核满载
一个多线程数据处理服务,从2线程扩展到8线程后CPU接近满载,但吞吐量只增加了15%。
火焰图一看,45%的CPU时间花在pthread_mutex_lock上。代码里一个全局锁保护了共享数据,但临界区包含了耗时200ms的数据处理操作,8个线程基本串行执行。
修复方案是把耗时操作移到锁外面,临界区只保护共享数据的读写。修复后CPU降到290%,吞吐量提升了5倍多。
锁争用在火焰图上的特征很明显——你会看到大量的__lll_lock_wait或futex_wait,而且这些函数的宽度占比很高。
排查流程总结
把上面的内容梳理一下,CPU异常飙升的排查流程大概是这样的:
第一步:确认问题范围。 用top和mpstat看是系统级CPU高还是单个进程高,是us高还是sy高。这决定了你后续的排查方向。
第二步:定位到进程和线程。 top -H或pidstat找到CPU消耗最高的线程。
第三步:分析线程在干什么。 根据语言选择合适的工具——Java用jstack/async-profiler,Go用pprof,C/C++用perf,Python用py-spy。
第四步:生成火焰图。 把采样数据可视化,找到最宽的那个函数调用栈。
第五步:结合代码分析。 拿着火焰图上的热点函数名去找代码,分析为什么这个函数会消耗这么多CPU。
第六步:修复并验证。 改完代码后重新采样,用差分火焰图对比优化效果。
整个过程中,最关键的是不要跳步。很多人拿到CPU告警直接就去翻代码了,没有先做数据收集和分析,结果翻半天也找不到问题。先收集数据,让数据告诉你方向,这比凭直觉猜要高效得多。
还有一点想强调的是,生产环境采样要注意控制影响。perf用LBR模式零开销,采样30秒就够了。不要在生产环境跑strace,不要用高频率采样,采样完把perf.data传到开发机分析,别在生产服务器上生成火焰图。
CPU排查这事儿,工具会用是基础,关键是思路要对。先定位方向再深入分析,一步一步缩小范围,再隐蔽的问题也能揪出来。
如果觉得这篇文章对你有帮助,欢迎转发给身边的同事,说不定哪天就用上了。
公众号:耕云躬行录
个人博客:躬行笔记