
1. 项目概述从一次线上告警说起那天下午监控大屏上一个刺眼的红色告警弹了出来“生产环境某核心服务CPU使用率持续超过90%”。整个团队的心都提到了嗓子眼。这可不是普通的测试环境而是承载着每秒数千笔交易的核心Java应用。告警就是命令我们立刻投入了战斗。在接下来的半小时里我们像侦探一样从宏观的系统负载到具体的进程再到线程最后定位到一行有问题的代码。这个过程就是一次标准的“进程CPU占用率过高”排查实战。事后我把整个流程梳理成笔记它不仅仅是一套命令的堆砌更是一种层层递进、抽丝剥茧的排查思维。无论你是运维工程师、开发人员还是系统管理员掌握这套流程都能让你在面对类似问题时从慌乱变得从容快速恢复服务并找到根因。这篇笔记就是我结合那次实战和多年经验为你总结的“CPU高占用排查指南”。2. 排查流程总览与核心思路排查CPU问题最忌讳的就是一上来就扎进代码里漫无目的地看。一个高效的排查流程必须是自顶向下、由表及里的。我们的核心思路可以概括为“四步定位法”系统 - 进程 - 线程 - 代码。这就像医生看病先看整体生命体征系统负载再检查是哪个器官出了问题哪个进程接着看器官里哪部分组织异常哪个线程最后进行病理分析哪行代码。第一步系统级观察。目的是确认问题真实存在并排除由系统级因素如其他无关进程、硬件资源不足导致的整体负载高。工具主要是top或htop。第二步进程级定位。在确认系统整体负载高后迅速定位到罪魁祸首——是哪个或哪几个进程消耗了绝大部分的CPU资源。这里top命令的交互模式或ps命令是主力。第三步线程级深挖。一个Java进程CPU高可能是其中某一个或几个线程在“疯狂工作”。我们需要进入进程内部看看是哪些线程在“作祟”。top -Hp或jstack是这里的关键。第四步代码级根因分析。结合线程堆栈信息和可能的性能剖析工具最终定位到有问题的代码逻辑比如死循环、低效算法、锁竞争等。这个流程是普适的无论是排查java进程、baidunetdiskunite进程还是wechatappex这类你不熟悉的进程方法论都是一致的。下面我们就拆解每一步的具体操作和心法。3. 第一步系统级宏观观察与初步判断当收到告警或发现系统卡顿时不要慌首先通过SSH或控制台登录到目标服务器。第一个使用的命令永远是top。3.1 使用 top 命令进行全局扫描在终端输入top后你会看到一个动态刷新的界面。我们的注意力应该集中在头部几行摘要信息上负载平均值load average例如load average: 1.05, 0.70, 0.80。这三个值分别代表过去1分钟、5分钟、15分钟的系统平均负载。对于单核CPU持续高于1通常意味着有进程在排队等待CPU对于多核比如4核则阈值相应提高如4。如果1分钟值远高于15分钟值说明负载在近期陡增是突发现象。总体CPU使用率%Cpu(s)us用户态运行用户进程所占用的CPU时间百分比。这是我们排查应用问题最关注的指标。sy内核态运行内核进程所占用的CPU时间百分比。过高可能意味着系统调用频繁或内核有瓶颈。id空闲CPU空闲时间百分比。我们通常希望ussy高时id很低。如果us或sy长期高于80%甚至接近100%就明确证实了CPU资源已成为瓶颈。注意在虚拟化环境如k8s虚拟机中top看到的CPU使用率是相对于虚拟机分配到的vCPU的。如果宿主机资源争抢严重虚拟机内看到的id可能很高但应用依然响应慢这时需要结合宿主机监控看。3.2 解读进程列表并锁定目标top下半部分的进程列表默认按CPU使用率降序排列。这是我们锁定目标的关键区域。PID进程ID进程的唯一标识。USER进程所有者。可以帮助你快速判断这是否是一个预期的系统进程或应用进程。%CPU该进程占用CPU的百分比。这是最关键的列。立刻找到那个 %CPU 异常高的进程。一个健康的Java应用在无负载时%CPU可能在0%~5%波动高负载时可能达到几十甚至上百对于多核CPU可以超过100%比如800%代表占满了8个核。COMMAND进程启动命令。这里可以看到进程名例如java、nginx、mysqld等。如果是Java进程通常能看到包含主类的全路径或jar包名。实操技巧在top界面中按P大写可以强制按CPU使用率排序默认即是。按M可以按内存使用率排序有时高CPU伴随高内存可以辅助判断。按1可以展开显示每个逻辑CPU核心的使用情况对于判断CPU使用是否均衡很有帮助。如果进程列表刷新太快看不清可以按s然后输入一个数字如5将刷新间隔改为5秒。假设我们通过top发现一个PID为12345的Java进程其%CPU持续在250%左右而其他进程都很低。那么目标进程就初步锁定了PID12345。4. 第二步进程级深度剖析与信息收集锁定高CPU进程后我们需要收集关于这个进程的更多详细信息为下一步的线程分析做准备。4.1 使用 ps 命令获取进程快照top是动态视图而ps能给我们一个静态的快照方便记录和分享。一个非常实用的命令组合是ps -eo pid,user,%cpu,%mem,command --sort-%cpu | head -20这个命令会列出所有进程的PID、用户、CPU、内存和命令并按CPU使用率降序排列显示前20条。你可以从中再次确认你的目标进程是否“名列前茅”。要获取某个特定进程比如PID 12345的详细信息可以用ps -fp 12345或者更详细的ps aux | grep 123454.2 进阶工具与场景分析htop可以看作是top的增强版界面更友好支持鼠标操作颜色区分树状显示进程关系。如果你有安装权限强烈推荐使用htop它能让你更直观地看到进程和线程。pidstat这是一个更专业的性能统计工具来自sysstat包。它可以按周期采样特定进程的CPU、内存、IO等数据对于需要持续监控一段时间变化的场景非常有用。# 每2秒采样一次针对PID 12345共采样5次 pidstat -p 12345 2 5特殊场景思考终端进程启动失败: 启动期间发生本机异常这类错误通常与进程启动环境有关可能涉及终端模拟器、PTY配置等。如果这个失败的进程反复尝试启动可能会短暂推高CPU但通常不会造成持续高占用。排查重点应是解决启动失败的根本原因。挖矿进程被隐藏这是安全应急场景。恶意挖矿进程往往会改名、隐藏其进程名或通过rootkit技术从ps/top列表中隐藏。此时不能完全信任top。需要结合系统整体负载load average异常高、网络连接netstat发现异常外连、以及cat /proc/loadavg等底层命令综合判断。使用chkrootkit、rkhunter或终端输入ls -la /proc/[0-9]*/exe查看进程真实路径可能发现端倪。baidunetdiskunite进程或alibabasafe service进程这类厂商软件的守护进程。首先确认其是否为官方正常进程。有时它们由于Bug或资源争抢可能导致CPU高。排查思路不变先定位然后根据其日志或官方文档分析。5. 第三步线程级微观洞察与热点定位找到高CPU进程后真正的挑战才开始。一个Java进程内部有几十甚至上百个线程我们需要找出是哪个线程在“疯狂燃烧CPU”。5.1 使用 top 查看进程内线程这是最快捷的方法。首先在top界面中按Shift H有些版本默认已开启线程模式。这会打开线程显示模式进程列表会变成线程列表。或者更直接的方式是使用命令top -H -p 12345-H表示显示线程-p 12345指定进程ID。这时top显示的就是进程12345内部的所有线程同样按CPU排序。记下那个CPU占用最高的线程的PID注意这里显示的是线程ID在Java里通常称为nid我们记为TID例如12401。5.2 将线程ID转换为十六进制Java的线程堆栈信息中线程ID是以十六进制表示的。而top或ps看到的是十进制。我们需要进行转换。假设高CPU线程的TID是12401。printf “%x\n” 12401输出会是3071。这个0x3071就是我们下一步在堆栈信息中要寻找的关键标识。5.3 使用 jstack 获取线程堆栈jstack是JDK自带的工具用于打印Java进程的线程堆栈信息。这是定位代码问题的“显微镜”。jstack 12345 /tmp/thread_dump_$(date %Y%m%d_%H%M%S).log这条命令将进程12345的线程堆栈输出到/tmp目录下的一个带时间戳的文件中。强烈建议在问题发生时立即抓取多次如间隔10秒抓取2-3次通过对比可以更容易发现始终处于运行状态的线程。打开堆栈文件搜索我们之前转换得到的十六进制线程ID3071。你会找到类似这样的段落“http-nio-8080-exec-1” #32 daemon prio5 os_prio0 tid0x00007f8b3820e800 nid0x3071 runnable [0x00007f8b1f7f9000] java.lang.Thread.State: RUNNABLE at com.example.app.ProblemClass.infiniteLoop(ProblemClass.java:25) at com.example.app.ProblemClass.run(ProblemClass.java:15) ...看nid0x3071对上了并且线程状态是RUNNABLE最重要的是它告诉了我们代码位置com.example.app.ProblemClass.infiniteLoop(ProblemClass.java:25)。问题很可能就出在这个类的第25行的一个循环或密集计算中。实操心得如果jstack执行很慢或卡住可能是因为进程CPU太高JVM的 Safepoint 机制无法到达。可以尝试使用jstack -F强制打印但可能会使JVM停顿更久生产环境慎用。除了jstackjcmd是更现代的统一命令行工具功能类似jcmd 12345 Thread.print。对于electron应用electron 渲染层向主进程发送信息其本质是Node.js进程。你可以使用node的调试工具或llnode来分析但高CPU排查思路相通先找到Node进程再用top -H看线程用--inspect参数获取分析剖面。6. 第四步代码级根因分析与常见模式拿到问题线程的堆栈信息就像侦探拿到了关键证据。接下来就是分析代码逻辑。高CPU的代码根因通常有以下几种模式6.1 无限循环或密集计算这是最直接的原因。堆栈会停留在某个循环或计算方法内部。特征线程状态持续为RUNNABLE堆栈顶部始终是同一个业务方法。解决检查循环条件是否永远为真或者算法复杂度是否在特定数据下爆炸如嵌套循环处理大数据集。6.2 锁竞争激烈线程没有在“计算”而是在“等待”或“争抢”但top看到的可能是系统态CPU (sy) 偏高因为线程频繁地在用户态和内核态之间切换进行系统调用以获取锁。特征可能看到多个线程状态为BLOCKED或WAITING等待同一个锁waiting on 0x0000000712345678。使用jstack多次采样会发现线程在RUNNABLE抢锁和BLOCKED之间切换。解决分析锁的粒度考虑使用更细粒度的锁、并发容器如ConcurrentHashMap或改用无锁数据结构。6.3 低效的IO或外部调用线程在等待网络响应或磁盘IO时状态可能是WAITING(on object monitor) 或TIMED_WAITING但如果IO操作设置不当如超时时间极短导致重试风暴或者处理IO结果的回调函数中有密集计算也会导致高CPU。特征堆栈中可能包含Socket.read、HttpClient.execute、数据库驱动方法等。结合iostat、netstat等命令查看系统IO和网络状态。解决优化IO逻辑增加合理的超时和重试机制使用异步非阻塞IO如NIO减少线程等待。6.4 JVM自身活动在某些情况下高CPU可能是由JVM的GC线程或JIT编译线程引起的。GC导致频繁的Full GC会导致所有应用线程暂停但GC线程自身会消耗大量CPU。可以通过jstat -gcutil 12345 1000每秒观察GC情况如果FGCFull GC次数和FGCTFull GC时间快速上升同时CPU高则很可能是内存问题触发了GC风暴。JIT编译在应用启动后一段时间或触发新的热点代码时JIT编译线程会活跃可能导致短暂的CPU尖峰这通常是正常现象。6.5 结合性能剖析工具对于更复杂的问题或者为了量化代码中各个方法的热度可以借助性能剖析工具。Arthas阿里开源的Java诊断神器。使用thread命令可以直接查看最忙的线程使用profiler命令可以生成火焰图直观展示CPU时间在方法调用上的分布。Async-Profiler一款低开销的性能分析器可以生成非常精确的CPU或内存火焰图。VisualVM或JProfiler图形化工具功能强大适合在开发或测试环境进行深度性能分析。火焰图是分析CPU热点最强大的工具之一。它自上而下显示调用栈宽度代表消耗的CPU时间。最顶层的“平顶山”就是最热点的代码路径一目了然。7. 实战案例与排查技巧实录让我们复盘一个简化版的真实案例串联整个流程。场景线上订单服务CPU使用率突然飙升到300%。系统观察top命令显示系统us占用超过80%load average的1分钟值达到10机器为4核。一个名为order-service.jar的Java进程%CPU稳定在280%。PID为8888。进程确认ps -fp 8888确认这是我们的订单服务。线程定位top -H -p 8888发现一个TID为9999的线程持续占用约95%的CPU。转换十六进制printf “%x\n” 9999得到270f。堆栈分析连续执行两次jstack 8888 /tmp/dump1.log和jstack 8888 /tmp/dump2.log。在两个文件中搜索nid0x270f发现该线程状态均为RUNNABLE堆栈顶部都指向同一个方法com.xxx.order.service.coupon.CouponCalculator.calculateBatch(List)。代码根因查看CouponCalculator.calculateBatch代码发现其中有一个针对用户订单列表的循环循环内部又调用了另一个isEligible方法而该方法执行了一个未使用索引的数据库级联查询。当批量处理用户数增多时算法复杂度呈指数增长导致CPU暴增。临时解决与优化立即通过配置中心降级该批量计算功能CPU回落。长期优化方案是为查询添加缓存、优化数据库索引、将O(n²)的算法重构为O(n log n)。常见问题排查表现象/问题可能原因排查命令/方向top显示%CPU高但jstack看不到RUNNABLE的热点线程1. GC 导致。2. JNI 本地代码。3. 排查间隔中热点转移。1.jstat -gcutil观察GC。2. 使用能分析本地栈的工具如perf或Async-Profiler。3. 缩短采样间隔多次抓取。线程状态多是BLOCKED或WAITING激烈的锁竞争或资源等待。分析jstack输出中locked 0x...和waiting on 0x...指向的同一个对象找到持有锁的线程。%sy系统态CPU异常高1. 大量的系统调用如频繁的IO。2. 进程/线程上下文切换频繁。1.strace -p PID跟踪系统调用生产环境慎用性能影响大。2.vmstat 1查看cs上下文切换列是否过高。怀疑是隐藏进程如挖矿进程被 rootkit 隐藏。1. 检查/proc目录下的进程ID数量与ps列出的是否差异巨大。2. 使用unhide等工具扫描。3. 检查计划任务 (crontab -l)、系统服务 (systemctl list-units) 和启动项。Java进程启动失败或崩溃如opencv导致进程崩溃或无法启动 conpty1. 查看应用日志、系统日志 (/var/log/messages或journalctl)。2. 检查核心转储文件 (core dump)。3. 检查环境变量、依赖库 (ldd)。独家避坑技巧保存现场在重启“问题进程”前务必保存以下信息至少2-3份间隔数秒的jstack输出、top -H截图、vmstat和iostat的统计信息。这些是事后分析的唯一证据。对比分析法在问题发生前后分别对进程做一次jstack。用文本对比工具如diff比较看哪些线程是新出现的或状态发生了集中变化。监控要全面不要只监控CPU。内存、磁盘IO、网络流量、GC日志、应用业务指标如QPS、耗时的联动异常往往能给你更早、更准确的预警。CPU高通常是结果而不是原因。理解工具局限jstack在极端高负载下可能失效。Arthas的thread命令在这种情况下往往更可靠。对于Go、Python等语言进程思路一致工具换为pprof、py-spy等即可。排查CPU问题本质上是一个“观察 - 假设 - 验证”的科学过程。这套流程笔记为你提供了系统的观察工具和验证手段。真正的功力在于根据看到的线索结合对自身系统架构和代码的理解做出最合理的假设并快速验证它。每一次成功的排查不仅是解决问题的过程更是加深你对系统理解的过程。