简介:面向linux运维与后端开发人员的pdf电子资料,聚焦cpu占用率过高的排查与解决,内容涵盖top与ps -mp两种定位方法、线程id十六进制转换、jstack堆栈分析,以及生产环境java进程cpu 300%的真实故障案例,最后补充zabbix/nagios等监控与告警建议。资源为单份pdf文档,压缩包约147kb,便于下载后直接阅读与随查随用。目前已有4448人学习,适合初入运维或希望提升故障排查效率的读者。通过文中命令组合与案例演示,读者可掌握按进程、线程逐层定位cpu瓶颈的完整思路,理解如何将线程id转换为十六进制并借助jstack输出定位问题代码,从而在面对高负载告警时更快找到根因,减少业务影响。
1. linux cpu 占用率排查:不是玄学,是一套固定动作
凌晨两点被电话叫醒,说生产环境有个 java 服务 cpu 占用率已经冲到 300%,页面卡得打不开,这种场景做过 linux 运维的人多少都经历过。cpu 占用率过高的问题之所以让人头疼,不是因为难度大,而是因为很多人一开始就在乱试:有人直接 kill 掉进程,有人反复重启服务,折腾半宿也没搞清楚到底是哪行代码把 cpu 烧起来的。
这篇笔记要解决的就是这件事:用一套固定的排查链路,从进程到线程,再到线程的堆栈信息,最终把元凶锁定到具体代码。思路不复杂,核心就两类工具——top 负责定位进程,ps 和 jstack 负责下钻线程。适合刚转 linux 运维、照着命令敲就能上手的同学,也适合有几年经验、想对照检查自己有没有漏步骤的从业者。我会把两种排查方法、一个真实生产案例的完整复盘,以及我踩过多次的坑都拆开讲清楚。
2. 两条排查主路线:先用 top 锁进程,再用 ps 下钻线程
2.1 top 的 p 排序:别只盯着默认视图发呆
很多新人打开 top 后习惯盯着第一屏看,看到某个进程 %cpu 高就直接动手处理,这是第一个误区。默认的 top 视图虽然一般按 cpu 使用率降序排列,但不同发行版、不同配置下的排序规则可能完全不同,而且 top 是动态刷新界面,你想截证据提交给同事时,界面一跳就抓瞎。标准做法是进入界面后按大写 p(也就是 shift+p)按 cpu 排序;如果想留档,就用批处理模式跑一条命令:
top -bn1 -o %cpu | head -n 15
参数拆开讲: -b 是 batch 模式,不进入交互界面,直接输出结果后退出; -n1 表示只采样一次; -o %cpu 按 cpu 占用率排序; head -n 15 只取前 15 行,避免刷屏。这条命令非常适合写进运维脚本里定时采集,晚上 cpu 告警时翻日志就能看到历史现场,不用人蹲在终端前面按 p。
再解释一下 %cpu 这个数字的本质:它是进程在采样周期内占用的 cpu 时间与单核总时间的比值,多核机器上最高可以到 100% 乘以核数。所以看到 java 进程显示 300%,意味着它差不多吃满了 3 个逻辑核,这是正常显示,不是工具出 bug。判断机器到底有几个核,用 nproc 或者看 /proc/cpuinfo :
nproc
nproc 返回的是逻辑核数量。如果你的机器是 8 核,一个进程跑到 800% 才说明它把全部 cpu 吃满;只跑到 300% 则说明还有剩余算力,但业务卡顿可能已经很明显了。这里有一个常见误用:有人拿 windows 的习惯,看到超过 100% 就以为系统跑飞了,其实是多核下的正常表现。拿到可疑 pid 之后,先不要急着杀,下一步是做进程到线程的下钻。
2.2 从进程到线程:top -h 和 ps -mp 两种下钻方式
锁定了 pid,接下来要弄明白进程里的哪个线程在消耗 cpu。java 进程里有业务线程、gc 线程、编译线程、定时任务线程等,同一个进程下线程们 cpu 消耗量差异很大,只看到进程级别说明不了问题。最直接的常见做法是用 top -h 进入线程模式:
top -h -p 2633
-h 让 top 显示该进程下的线程列表, -p 2633 限定只看 pid 为 2633 的进程。进入界面后,再按一次大写 p,线程就会按 cpu 占用率降序排列。此时第一列显示的 pid 实际上是线程 id(tid),记下它,后面转换进制要用。
另一种不用交互界面的方法是 ps -mp,在远程终端里更顺手,也更容易把结果重定向到文件里留证据:
ps -mp 2633 -o thread,tid,time | sort -rn | head -n 15
这段命令要拆开说。 -m 表示按线程显示进程信息, -p 2633 指定进程; -o thread,tid,time 是自定义输出列:thread 标识线程信息,tid 是线程 id,time 是该线程累计消耗的 cpu 时间; sort -rn 按第一列数字降序; head -n 15 取前 15 行,把 cpu 时间最长的线程排在前面。输出里第一列是累计 cpu 时间,第二列通常是 tid,第三列才是具体数值,不同系统列顺序略有差异,先跑一次不带 head 的命令确认字段位置。
这里有一个容易被忽略的区别:top 里的 %cpu 是瞬时采样值,会上下跳动;ps 输出的 time 是线程从启动到现在的累计 cpu 时间,相对稳定。我一般会同时看两列:瞬时值高说明此刻正在跑,累计值高说明它长时间占用 cpu。如果瞬时高的线程每次都不一样,说明是线程池在调度,任务在换人做;如果某个线程累计值特别高,那基本可以断定它就是长期吃 cpu 的那个。
如果觉得这两条命令来回敲太麻烦,还可以用 sysstat 包里的 pidstat 做持续观察:
pidstat -t -p 2633 1 5
-t 显示线程级统计, -p 2633 指定进程, 1 5 表示每秒采样一次、连续采样 5 次。输出里的 %cpu 是采样周期的平均值,适合观察一段时间内的趋势,快速抖动也能捕捉到。三条命令的适用场景我习惯这样区分:
| 工具 | 特点 | 适用场景 |
|---|---|---|
| top -h | 交互式、实时刷新 | 现场手动排查、观察瞬时占用 |
| ps -mp | 一次性快照、可留档 | 记录证据、脚本定时采集 |
| pidstat | 趋势采样、输出稳定 | 分析波动规律、对比多轮数据 |
2.3 高 cpu 线程的常见分类:先猜方向再动手
定位到具体线程后,不要急着转十六进制去 jstack,先根据线程名和状态做个初步判断。常见的高 cpu 线程大致分三类:第一类是业务线程,线程名通常是业务自定义的或者包含接口名,堆栈里能看到你自己的代码;第二类是 gc 线程,线程名带 gc 字样,这类线程把 cpu 吃满往往说明 jvm 在疯狂做垃圾回收,问题根源通常在内存;第三类是 jit 编译线程,名字类似 c2 compilerthread,短时间内高 cpu 正常,持续高就要留意。
这个分类决定了下一步动作。如果是业务线程,直接走 jstack 抓堆栈;如果是 gc 线程,优先去看堆内存使用情况和 gc 日志,而不是死磕代码。很多人花半天时间在 jstack 输出里找业务代码,最后发现全是 gc 线程,方向一开始就偏了。如果排查对象不是 java 进程而是 c/c++ 程序,jstack 用不上,常见做法是改用 gdb 或 perf top 直接看内核态和用户态调用栈,这一步的操作链路完全不同,别把 java 的经验直接套上去。
3. 线程 id 转十六进制:printf 和 bc 的写法与常见坑
3.1 为什么必须转十六进制:jstack 输出的 nid 格式
上一章拿到了线程 id,比如 3626,现在要找这个线程在 jvm 里的堆栈信息。直接拿十进制数字去 jstack 输出里 grep 是找不到的,因为 jstack 输出的线程标识里,线程 id 被 jvm 写成了十六进制,格式类似 nid=0xe2a 。这个 nid 在 jvm 源码里就是 native thread id,也就是操作系统看到的线程 id,只是展示时用十六进制表达。
这是整个排查链路里第一个容易卡住的点:数字格式不统一。操作系统工具 top、ps 默认给十进制,jvm 的 jstack 给十六进制,中间必须做一次转换。转换的思路就是拿十进制线程 id,让它以十六进制形式输出,形式上有两条路:用 printf 配合 %x 格式符,或者用 bc 计算器配合 ibase/obase 变量。
3.2 printf "%x\n":最稳的单行转换方式
printf "%x\n" 3626
printf 是 bash 内置命令, %x 表示把参数按无符号十六进制输出, \n 换行。这条命令的输出是 e2a ,注意是纯小写字母。重点提示:后续 jstack 输出里的 nid 也是小写,grep 是区分大小写的,写成 e2a 根本匹配不到。
参数说明: %x 是格式占位符,字母 x 代表小写十六进制;如果想转成八进制,把 %x 换成 %o ;如果想转回十进制,用 %d 。这条命令对超过 32 位的数字可能有精度问题,但正常线程 id 也就几万,完全够用。
3.3 echo 配合 bc:计算器方式的边界条件
另一种写法是把线程 id 丢给 bc 计算器处理,这也是原文提到的方法一里的标准做法:
echo 'obase=16;3626' | bc
obase=16 表示输出采用十六进制,分号后面的 3626 是输入值。原理是 bc 把输入当作十进制处理,按指定的输出进制输出结果。和 printf 相比,这条命令多了一层管道,一旦 echo 或 bc 的某一个环节有问题,结果往往是空的或者 0,而且 bc 不会报错,容易让人误判。
这里有个边界要注意:bc 的 obase 和 ibase 是全局变量,如果你先设置了 ibase=16 ,后面的数字解析就全变了,混用时经常算出莫名其妙的结果。我的习惯是固定只用 printf,因为它是 shell 内置的,不依赖额外包,出错概率最低。对于只需要转一个线程 id 的场景,bc 那条命令足够,但一旦涉及脚本循环处理多个值,printf 的优势就明显了。
3.4 批量转换和逆向验证:脚本思路
生产环境一个 java 进程可能有几十个线程在抢 cpu,一次只转一个 id 太慢。我一般会把 ps -mp 的输出喂给脚本,批量转换所有可疑 tid:
for tid in 3626 3631 3640; do
hex=$(printf '%x' "$tid")
echo "tid $tid -> nid=0x$hex"
done
脚本逻辑很简单:循环遍历可疑 tid 列表,用 printf 逐一把十进制转成十六进制,拼成 jstack 里的 nid 格式。这里用 $(...) 做命令替换,把 printf 的结果赋给变量 hex,再输出。好处是:一次处理多个线程,而且格式和 jstack 保持一致,后面 grep 直接复制,避免手写转录出现大小写或漏位错误。
逆向转换也值得顺手掌握,特别是你想拿 jstack 输出里的多个 nid 反查对应的十进制 tid 时:
echo $((0xe2a))
$((...)) 是 bash 的算术求值, 0xe2a 是带 0x 前缀的十六进制字面量,结果输出十进制 3626。这条命令在做交叉验证时很有用:jstack 输出里有多个 nid=0x... ,你不知道哪个匹配目标,把每个都转回十进制,去和 ps -mp 的输出核对,就不会抓错对象。
这里必须多说一句:转换结果手写抄录很容易抄错。我见过同事把进制的转换结果抄到笔记里,隔天排查同类型问题直接复制旧值,结果 grep 出来一堆错位信息。每次排查都现场重新用命令算一次,是成本最低的安全措施。
4. jstack 抓线程堆栈:一次生产故障的完整复盘
4.1 故障现场:一个吃完 300% cpu 的 java 进程
说一个生产环境里实际处理过的场景,过程比教科书简单,但每一步都值得拆开看。故障是监控先发现的:业务侧反馈订单处理变慢,同时监控面板显示某个节点的 cpu 使用率接近 300%。登录服务器后第一件事,就是跑 top -bn1 -o %cpu 拿现场快照,确认是 pid 2633 的 java 进程,cpu 占用接近 300%,并且持续了大约 12 分钟没有回落。
这里有个时间判断要说明:如果进程只是启动初期瞬间冲高,可能是 jit 编译或者类加载,一般几分钟会回落;持续 10 分钟以上还高,基本可以排除初始化原因,进入实质排查。我在当时先把快照存到文件里,再顺手记录一下当前时间和进程启动时间,为后面做时间线比对留素材。这一步看起来多余,但在复盘故障报告时非常有用,能区分到底是代码问题还是外部流量突发。
4.2 定位线程:ps 排序、进制转换、jstack 抓取
确认进程后,进入线程定位阶段。执行以下命令:
ps -mp 2633 -o thread,tid,time | sort -rn | head -n 15
输出中排在第一位的 tid 是 3626,累计 cpu 时间已经有 12 分钟,几乎和进程的 cpu 告警时间对得上,这个线程基本就是罪魁祸首。注意这个场景里 tid 和进程 pid 位数接近,很容易被误当成另一个进程,实际上线程 id 和进程 id 是独立的两个数字,同一个进程内的线程 tid 各不相同。
接着做进制转换:
printf "%x\n" 3626
这里算出来是 e2a ,不是某些资料里随手写的 e18 。这里特别提示: 0xe18 换算回十进制其实是 3608,和 3626 对不上。排查现场一定要以自己命令的实际输出为准,别照抄别人笔记里的数值,同一个进程下多个线程同时高占用时,抄错一个数字就会抓错线程。
转换完成后,用 jstack 抓堆栈,并把包含目标十六进制线程 id 的上下文打出来:
jstack 2633 | grep "e2a" -a 30
jstack 2633 输出进程下所有线程的堆栈, grep "e2a" -a 30 匹配到 nid=0xe2a 这一行后,把后面 30 行堆栈打印出来。为什么不直接全量看?因为生产环境一个 java 进程可能有几百个线程,全量输出的信息量太大,先按目标线程过滤是效率最高的做法。 -a 30 的数值可以根据堆栈深度调整,一般 30 行足够覆盖调用链。
如果担心一次抓取不够准,可以连续抓三份快照:
for i in 1 2 3; do
jstack 2633 > "jstack_$(date +%h%m%s).log"
sleep 5
done
脚本逻辑:循环三次执行 jstack,每次输出到带时间戳的文件,间隔 5 秒。抓完后对比三份日志里同一个 nid=0xe2a 的堆栈内容,如果栈顶方法一致,说明线程长期停留在这段代码里,定位结果可信。
4.3 解读堆栈:从线程状态到代码入口
jstack 抓到的内容大概长这样,我简化后贴一段便于解释:
"pool-3-thread-7" #67 prio=5 os_prio=0 tid=0x00007f8b2400c800 nid=0xe2a runnable [0x00007f8b1e9f9000]
java.lang.thread.state: runnable
at com.example.order.service.orderservice.calculateprice(orderservice.java:142)
at com.example.order.service.orderservice.buildorder(orderservice.java:88)
at com.example.order.worker.orderworker.run(orderworker.java:55)
第一行里 nid=0xe2a 就是我们转换出来的线程 id,确认对象没有抓错; runnable 表示线程正在运行。下面的堆栈自底向上看,最上面 at 开头的行是当前正在执行的代码位置,也就是问题最可能的爆发点。 orderservice.java:142 这一行是核心线索——一个计算价格的业务方法正在持续执行,配合线程池名称 pool-3-thread-7 ,能推断出是订单处理线程池里的某个任务卡在了高消耗的循环或正则计算里。
堆栈解读的原则我一般这样把握:第一,先看线程状态是 runnable 还是 blocked、waiting,高 cpu 消耗基本只会出现在 runnable,后两者通常和锁等待、休眠相关;第二,看 at 行所在的包名和类名,如果全是 java.* 或 gc 相关,说明方向错了;第三,拿两次 jstack 快照对比,间隔 5 到 10 秒,如果同一个线程停在同一个代码位置,说明它真的卡在这段逻辑里,而不是刚好路过。
线程状态对判断方向的帮助很大,常见状态简单总结如下:
| 线程状态 | 含义 | cpu 消耗特征 |
|---|---|---|
| runnable | 正在执行代码 | 高,是排查重点 |
| blocked | 等待监视器锁 | 通常不高,可能有锁竞争 |
| waiting / timed_waiting | 等待唤醒或超时 | 接近零 |
| dead | 已结束 | 忽略 |
解读到这里,问题基本清晰:一个订单处理线程在执行价格计算时陷入高消耗逻辑。处理方式是先对接口 做限流止血,再让开发排查 calculateprice 方法里的循环和算法逻辑。整个排查链路从 top 到 ps 到 jstack,耗时大约十五分钟。
5. 高 cpu 排查避坑指南:五个最常见的翻车点
5.1 进程定位阶段的两次翻车
第一次翻车是十六进制大小写写反,grep 无结果。现象是:printf 已经算出 e2a ,但 grep 的时候用成 e2a ,结果 jstack 输出里怎么都找不到这个线程,怀疑自己是不是转错了数字。原因是 jstack 输出的 nid=0xe2a 是小写字母,grep 默认区分大小写。解决的方案是:复制粘贴命令输出,不要手敲;或者直接在 grep 模式里加 -i 忽略大小写,但 -i 会把其他包含 e2a 的所有行也带出来,输出会变乱,不推荐,最稳的还是命令替换和直接复制。
第二次翻车是在进程级别直接处理,跳过了线程定位。现象是:确认某个 java 进程 cpu 高,直接重启服务,结果几分钟后 cpu 又打满。原因是问题在业务代码里,重启只是把线程重置了一遍,流量一进来同样的代码路径继续吃 cpu。解决的方案是把排查链路走完:进程到线程,线程到堆栈,堆栈到代码行,修复后重新发布才有效。对于线上紧急情况,重启可以作为临时止血手段,但永远不是答案,停了重启只会把问题延后,不会消失。
5.2 jstack 抓取阶段的两个坑
第三个坑是高 cpu 线程恰好刚结束,jstack 抓不到。现象是:top 里明明看到线程 3626 占用很高,但 jstack 输出 grep e2a 没有任何结果。原因是 top 的采样和 jstack 的执行有时间差,线程可能已经执行完,线程池里这个线程已经开始处理别的任务,或者线程直接销毁了。解决的办法是连续抓多份 jstack,或者先用 top -h -p 观察 10 秒钟,记录高线程 id 的波动范围,再针对多个线程 id 一起抓。如果线程波动范围很大,说明是短任务线程池在抢 cpu,单次抓取意义不大,改成 pidstat 做趋势观察更有效。
第四个坑是 jstack 本身执行失败。现象是:执行 jstack 2633 报错,常见提示有 unable to open socket file 或者 operation not permitted ,看起来像是工具装错了。原因通常有两个:一是当前用户不是进程属主,也没有 root 权限,jvm 不允许低权限用户 attach;二是系统开启了对 ptrace 的限制,centos 7 及之后的系统默认 kernel.yama.ptrace_scope = 1 ,非属主进程无法被 attach。解决的办法是换 root 执行,或者用 sudo -u 进程属主用户 jstack 2633 ;如果是容器环境,需要在宿主机上找到对应 pid 再操作,容器内经常没有 jstack 命令,或者 jdk 版本和运行环境不匹配。
5.3 现象误导:看起来是 cpu 问题,其实是内存问题
第五个坑最具有迷惑性。现象是:cpu 占用高,jstack 抓到的堆栈全部是 gc 相关线程,比如 gc task thread#0 或者 g1 young remset sampling ,业务代码一行都看不到。原因是 jvm 在疯狂进行垃圾回收,gc 本身消耗了大量 cpu,而业务线程基本都在等待内存分配,真正的问题在堆内存,不在业务逻辑。此时继续在 jstack 里找业务代码毫无意义,应该先把视角切到 gc 日志和堆内存上。
解决的方法是先跑 jstat -gcutil 2633 1000 看 gc 频率和堆空间使用率,重点观察两个指标: fgc 表示 full gc 次数, fgct 是 full gc 累计耗时。如果 full gc 次数持续上涨,说明堆内存快撑不住了。接下来看堆参数是否合理,必要时用 jmap -dump 打堆转储分析对象占用。这一条我愿称之为高 cpu 排查里最容易走弯路的分岔口——cpu 高只是表象,内存压力才是根因。
排查到这里,完整链路已经很清楚了。把这些坑串起来看,能发现一个共同点:大多数失败不是命令不会敲,而是对采样时机、输出格式、权限边界这些细节没有校验。细节校验到位,这条链路基本不会出错。
6. 监控与告警:从被动救火到主动发现
cpu 高占用的排查,核心链路是 top 到 ps 到 jstack,但比解决故障更重要的,是让故障在被用户投诉之前就有人看见。人工盯着终端等告警不现实,监控软件把这件事自动化是唯一出路。
zabbix 和 nagios 这一类传统监控,思路是提前设置好触发器或规则,比如 cpu 使用率连续 5 分钟超过 85%,就触发告警。zabbix 的做法是在模板里添加 item 和 trigger,item 用 system.cpu.util[,system] 这类内置键值,trigger 表达式设置阈值为 85 并指定持续时间;nagios 则是通过 nrpe 插件在远端执行 check_load,再配合告警间隔。这套方案的优点是自建、可控,缺点是每台机器都要装 agent,规则要自己维护,小团队初期容易漏配。
如果你现在用的是云主机,云厂商自带的监控面板能直接看到 cpu 曲线,但默认没有告警推送,需要手动配告警规则。还有一种更省事的做法,是使用绑定云账号只读 accesskey 的运维告警工具,比如这篇案例里提到的王教授这类产品,绑定后可以把云监控的告警事件推送到团队的即时通讯群里,遇到 cpu 持续高占用会直接发出通知,不需要自己维护监控节点。这类工具的核心价值是把「主动盯屏幕」变成「被动收通知」,减少漏看告警的概率。
这里我强烈建议:每一次真实故障处理完,都顺手做一次验证——故意让某个测试接口去做一次高消耗计算,确认监控告警能在预定的阈值和时间窗口内触发推送。别等到下一次故障才知道告警根本没配置成功。
从那以后,我每次处理完高 cpu 问题都强制自己走一遍完整动作:现场快照存文件、线程定位记录 tid、转换结果复制不手抄、jstack 连续抓三份、最后把堆栈和代码行归档到故障记录里。这套链路看着笨,但恰恰是它让我在下次遇到同样问题时,十分钟内就能把元凶翻出来。希望帮到你。
以上就是linux cpu占用率排查教学:从top、ps到jstack定位高消耗线程的详细内容,更多关于linux cpu占用过高结节的资料请关注代码网其它相关文章!
发表评论