ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux CPU占用率排查:从top、ps到jstack定位高消耗线程

Linux CPU占用率排查:从top、ps到jstack定位高消耗线程 简介面向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也就是 Shiftp按 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/cpuinfonprocnproc返回的是逻辑核数量。如果你的机器是 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 实际上是线程 IDTID记下它后面转换进制要用。另一种不用交互界面的方法是 ps -mp在远程终端里更顺手也更容易把结果重定向到文件里留证据ps -mp 2633 -o THREAD,tid,time | sort -rn | head -n 15这段命令要拆开说。-m表示按线程显示进程信息-p 2633指定进程-o THREAD,tid,time是自定义输出列THREAD 标识线程信息tid 是线程 IDtime 是该线程累计消耗的 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 写成了十六进制格式类似nid0xe2a。这个 nid 在 JVM 源码里就是 native thread ID也就是操作系统看到的线程 ID只是展示时用十六进制表达。这是整个排查链路里第一个容易卡住的点数字格式不统一。操作系统工具 top、ps 默认给十进制JVM 的 jstack 给十六进制中间必须做一次转换。转换的思路就是拿十进制线程 ID让它以十六进制形式输出形式上有两条路用 printf 配合%x格式符或者用 bc 计算器配合 ibase/obase 变量。3.2 printf %x\n最稳的单行转换方式printf %x\n 3626printf 是 bash 内置命令%x表示把参数按无符号十六进制输出\n换行。这条命令的输出是e2a注意是纯小写字母。重点提示后续 jstack 输出里的 nid 也是小写grep 是区分大小写的写成E2A根本匹配不到。参数说明%x是格式占位符字母 x 代表小写十六进制如果想转成八进制把%x换成%o如果想转回十进制用%d。这条命令对超过 32 位的数字可能有精度问题但正常线程 ID 也就几万完全够用。3.3 echo 配合 bc计算器方式的边界条件另一种写法是把线程 ID 丢给 bc 计算器处理这也是原文提到的方法一里的标准做法echo obase16;3626 | bcobase16表示输出采用十六进制分号后面的 3626 是输入值。原理是 bc 把输入当作十进制处理按指定的输出进制输出结果。和 printf 相比这条命令多了一层管道一旦 echo 或 bc 的某一个环节有问题结果往往是空的或者 0而且 bc 不会报错容易让人误判。这里有个边界要注意bc 的obase和ibase是全局变量如果你先设置了ibase16后面的数字解析就全变了混用时经常算出莫名其妙的结果。我的习惯是固定只用 printf因为它是 shell 内置的不依赖额外包出错概率最低。对于只需要转一个线程 ID 的场景bc 那条命令足够但一旦涉及脚本循环处理多个值printf 的优势就明显了。3.4 批量转换和逆向验证脚本思路生产环境一个 Java 进程可能有几十个线程在抢 CPU一次只转一个 ID 太慢。我一般会把 ps -mp 的输出喂给脚本批量转换所有可疑 TIDfor tid in 3626 3631 3640; do hex$(printf %x $tid) echo TID $tid - nid0x$hex done脚本逻辑很简单循环遍历可疑 TID 列表用 printf 逐一把十进制转成十六进制拼成 jstack 里的 nid 格式。这里用$(...)做命令替换把 printf 的结果赋给变量 hex再输出。好处是一次处理多个线程而且格式和 jstack 保持一致后面 grep 直接复制避免手写转录出现大小写或漏位错误。逆向转换也值得顺手掌握特别是你想拿 jstack 输出里的多个 nid 反查对应的十进制 TID 时echo $((0xe2a))$((...))是 bash 的算术求值0xe2a是带 0x 前缀的十六进制字面量结果输出十进制 3626。这条命令在做交叉验证时很有用jstack 输出里有多个nid0x...你不知道哪个匹配目标把每个都转回十进制去和 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 30jstack 2633输出进程下所有线程的堆栈grep e2a -A 30匹配到nid0xe2a这一行后把后面 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 秒。抓完后对比三份日志里同一个nid0xe2a的堆栈内容如果栈顶方法一致说明线程长期停留在这段代码里定位结果可信。4.3 解读堆栈从线程状态到代码入口jstack 抓到的内容大概长这样我简化后贴一段便于解释pool-3-thread-7 #67 prio5 os_prio0 tid0x00007f8b2400c800 nid0xe2a 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)第一行里nid0xe2a就是我们转换出来的线程 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 输出的nid0xe2a是小写字母grep 默认区分大小写。解决的方案是复制粘贴命令输出不要手敲或者直接在 grep 模式里加-i忽略大小写但-i会把其他包含 e2a 的所有行也带出来输出会变乱不推荐最稳的还是命令替换和直接复制。第二次翻车是在进程级别直接处理跳过了线程定位。现象是确认某个 Java 进程 CPU 高直接重启服务结果几分钟后 CPU 又打满。原因是问题在业务代码里重启只是把线程重置了一遍流量一进来同样的代码路径继续吃 CPU。解决的方案是把排查链路走完进程到线程线程到堆栈堆栈到代码行修复后重新发布才有效。对于线上紧急情况重启可以作为临时止血手段但永远不是答案停了重启只会把问题延后不会消失。5.2 jstack 抓取阶段的两个坑第三个坑是高 CPU 线程恰好刚结束jstack 抓不到。现象是top 里明明看到线程 3626 占用很高但 jstack 输出 grepe2a没有任何结果。原因是 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 和 triggeritem 用system.cpu.util[,system]这类内置键值trigger 表达式设置阈值为 85 并指定持续时间Nagios 则是通过 NRPE 插件在远端执行 check_load再配合告警间隔。这套方案的优点是自建、可控缺点是每台机器都要装 Agent规则要自己维护小团队初期容易漏配。如果你现在用的是云主机云厂商自带的监控面板能直接看到 CPU 曲线但默认没有告警推送需要手动配告警规则。还有一种更省事的做法是使用绑定云账号只读 AccessKey 的运维告警工具比如这篇案例里提到的王教授这类产品绑定后可以把云监控的告警事件推送到团队的即时通讯群里遇到 CPU 持续高占用会直接发出通知不需要自己维护监控节点。这类工具的核心价值是把「主动盯屏幕」变成「被动收通知」减少漏看告警的概率。这里我强烈建议每一次真实故障处理完都顺手做一次验证——故意让某个测试接口去做一次高消耗计算确认监控告警能在预定的阈值和时间窗口内触发推送。别等到下一次故障才知道告警根本没配置成功。从那以后我每次处理完高 CPU 问题都强制自己走一遍完整动作现场快照存文件、线程定位记录 TID、转换结果复制不手抄、jstack 连续抓三份、最后把堆栈和代码行归档到故障记录里。这套链路看着笨但恰恰是它让我在下次遇到同样问题时十分钟内就能把元凶翻出来。希望帮到你。本文还有配套的精品资源点击获取
返回列表