运维知识
悠悠
2026年6月17日

线上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吃满了,这种情况下进程级别的数据是有欺骗性的。

image-20260617213821604

image-20260617213849975

2.2 mpstat —— 各核CPU详情

mpstat -P ALL 1 3

image-20260617213911234

这个命令能看到每个CPU核心的使用率,包括usr、sys、iowait、softirq等细分指标。如果发现某个核CPU特别高而其他核正常,那可能是单线程瓶颈或者中断都绑在一个核上。

# 看软中断详情
mpstat -I SCPU 1 5

# 看硬中断详情
mpstat -I CPU 1 5

image-20260617214009091

这两个命令在排查网络中断、定时器中断类问题时特别有用。之前那个定时器的案例,就是通过mpstat -I SCPU发现HRTIMER软中断从正常的200次/秒飙升到2000多次/秒,才锁定了问题方向。

2.3 pidstat —— 进程级CPU分析

# 每秒输出一次,输出5次
pidstat 1 5

# 针对特定进程
pidstat -p <PID> 1 5

image-20260617220113845

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_waitfutex_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排查这事儿,工具会用是基础,关键是思路要对。先定位方向再深入分析,一步一步缩小范围,再隐蔽的问题也能揪出来。


如果觉得这篇文章对你有帮助,欢迎转发给身边的同事,说不定哪天就用上了。

公众号:耕云躬行录

个人博客:躬行笔记

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码