CPU占用高排查实战:从进程到线程的深挖指南

CPU占用高排查实战:从进程到线程的深挖指南 凌晨两点告警群弹出一条消息某台服务器CPU使用率持续100%超过15分钟。这大概是每一位运维和服务端开发最不想在深夜看到的消息。说实话CPU占用高这个问题在所有的系统故障里属于高发而且看起来好查的类型——打开任务管理器或者top一眼就能看到哪个进程在吃CPU。但真到了排查的时候你会发现这只是万里长征第一步进程是找到了可这个进程为什么吃CPU是业务流量涨了还是代码出了Bug是锁竞争还是GC问题是机器本身的问题还是上游调用方把压力传导过来了每一层都要往下挖。这篇文章就是我这些年排查CPU问题的经验总结从认知准备、工具链、高频原因、真实案例到长期治理一条线讲清楚。不管你是刚入门的新手还是已经在排查路上踩过不少坑的老手跟着这套思路走一遍再遇到CPU告警的时候能少走很多弯路。1. 拿到告警先别慌先把正常负载和故障负载分清楚1.1 基线数据没有对比就没有排查我先说一个很多人容易犯的错误一看到CPU占用50%以上就觉得出事了。实际上判断CPU是否有问题第一件事不是看数字而是看这台机器平时是什么水平。举个真实的例子。一台跑了十多个Java微服务的4核开发机平时CPU就在40%~60%之间波动一旦有人执行一次全量数据同步瞬间飙到90%以上这是正常业务行为。而一台只跑了一个Nginx反代的机器平时CPU几乎在1%以下某天突然稳定在30%这绝对有问题——哪怕数字看上去不高。所以你在排查之前必须先回答三个问题这台机器/这个应用的CPU基线是多少当前CPU高是持续稳定高位还是周期性波动高CPU和业务高峰、定时任务、发布变更有没有时间上的关联这三个问题的答案决定了你接下来是往业务扩容方向走还是往代码Bug方向走。没有基线就下结论是排查工作里最大的坑。1.2 持续高位和瞬时尖峰两种完全不同的排查思路CPU占用高的形态基本上可以分成两类持续高位型。CPU长时间维持在85%以上几乎没有回落。这种问题通常指向代码层面的确定性缺陷比如死循环、无限递归、某个长任务在不停地做无效计算。这类问题一旦锁定进程基本就能一步步找到根因相对好处理。瞬时尖峰型。CPU周期性地冲到100%每次持续几秒到几十秒然后又掉下来。这种问题要复杂得多可能是指标采集任务在整点触发、日志压缩在半夜进行、某个定时任务扫描了大批数据也可能是GC风暴的周期性发作。这类问题的难点在于抓不到现场——你打开监控的时候尖峰已经过去了。针对瞬时尖峰我后面专门有一节讲怎么用历史数据和性能计数器来倒推。这里先记住一个原则不要只盯着实时数据。把趋势图拉出来看尖峰的周期、持续时长、触发时间点往往比实时数值更有价值。提示CPU查不出问题的时候先看趋势图再看变更记录。80%的莫名CPU高都能在历史趋势和时间轴里找到答案。2. 排查工具链从任务管理器一路深挖到线程级现场大部分人的CPU排查止步于看到某个进程占用高但这恰恰只是第一步。一个进程占用高你需要继续回答三个问题是哪个线程在跑这个线程在执行什么代码这一段代码为什么消耗这么大的CPU以下工具就是回答这三层问题的利器。排查层级Windows侧工具Linux侧工具进程级任务管理器、PerfMon、资源监视器top、htop、pidstat线程级资源监视器、Process Explorertop -H、pidstat -t函数级WPR/WPA、VTuneperf、gdb代码级Javajstackjstack系统调用级API Monitorstrace2.1 Windows侧的工具组合与使用时机任务管理器是第一眼工具适合快速确认哪个进程在作妖。注意在详细信息标签页里加上CPU列的排序而且最好把CPU单位切换成百分比基于逻辑处理器的视角来看否则多核机器上单个进程的CPU占比会被稀释成很低的数字误导你的判断。资源监视器能比任务管理器更进一步。任务管理器只能看到进程级别资源监视器可以按进程展开看到线程级别的CPU占用。选中一个进程展开分析等待里的线程列表按CPU排序能直接看到这个进程里哪些线程在疯狂消耗CPU。这个信息已经能帮我们把范围缩到很小了。Process Explorer是Sysinternals套件里的王牌工具强烈建议每个做服务端的人都装上。它可以右键一个线程查看堆栈信息Stack直接看到这个线程当前正在执行的函数调用链。很多看起来无解的CPU问题在这一步就能看到端倪——比如你发现线程栈停在一个字符串重试逻辑、正则匹配或者一个你完全没想到的第三方库函数里。它还能显示每个进程的CPU历史曲线对判断是不是周期性尖峰非常直观。PerfMon 数据收集器集负责解决历史数据缺失的问题。当你需要时间维度的数据时Windows自带的性能监视器可以配置数据收集器集每秒或每分钟采集一次CPU相关计数器比如Processor% Processor Time、Process% Processor Time、Thread% Processor Time。把这些数据落到日志文件里等故障再次发生时拉出来分析是最靠谱的守株待兔手段。wmic命令行雖然在新版Windows里逐步被弃用但很多老脚本还在用。比如获取CPU信息可以用wmic cpu get processorid /value这条命令输出的是主板上的CPU标识信息在排查CPU型号、核对机器配置时偶尔会用到。不过说实话日常排查CPU占用问题wmic帮助非常有限。我提它是因为很多人还在问这条命令报Cannot run program的问题——这通常是因为在非管理员权限的IDE里调用了系统命令环境变量里的System32路径没被带上。解决方式是在命令前写全路径C:\Windows\System32\wbem\wmic.exe或者改用PowerShell的等价方案。WPR/WPA是Windows下的终极大杀器。Windows Performance Recorder配合Windows Performance Analyzer可以做非常深度的CPU采样分析能精确到函数级甚至能看到每个CPU核心上的调度情况。这个工具学习曲线比较陡但处理极端疑难问题时它是v最后兜底的武器。2.2 Linux侧的命令链Linux侧的排查思路和Windows基本一致只是命令换了一批top/htop是第一眼工具。我的习惯是进入top之后先按P键按CPU排序记住可疑进程的PID。htop的优势是颜色直观、支持树状视图按H键可以展开线程视图直接看到进程下线程的执行情况。pidstat可以按进程和线程分别输出CPU统计而且支持自定义时间间隔非常适合观察哪个线程在周期性消费CPUpidstat -t -p PID 1这个命令每秒输出一次指定进程的线程级CPU占用比top一层层翻方便得多。perf是Linux下最强的性能分析工具。perf top可以实时显示当前CPU热点函数perf record则可以把采样结果记录到文件中再通过perf report去分析函数调用链。对定位C/C程序、内核态驱动的CPU问题perf几乎是不可替代的。jstack是Java环境的必备武器。如果吃CPU的进程是Java应用把线程ID转成十六进制在jstack输出里找到对应的线程栈配合top里看到的线程CPU排序能直接定位到代码行。这个操作在第四节案例里我会完整演示一遍。strace/gdb在问题出在系统调用层面时使用。strace -p PID可以实时跟踪进程的系统调用对CPU定位来说属于间接手段但有时候能提供关键线索。比如你发现某个进程在疯狂执行futex系统调用那基本就是锁竞争导致的了。2.3 从进程到线程定位成败的分水岭我见过太多人卡在知道是PID 1234在吃CPU然后就没有然后了。究其原因是不少人只关注进程级指标而从没意识到**一个进程里可能有几十上百个线程真正吃CPU的往往只有一两个关键线程。**如果不去挖线程你永远只能在某个进程有问题这个层面上打转无法把问题落到具体的代码路径上。Windows下获取线程CPU占用比较方便资源监视器和Process Explorer都能看见线程级的占比点击排序即可。Linux下稍麻烦一点需要用top -H -p PID来显示指定进程的所有线程或者用pidstat -t来按线程输出。拿到占用最高的线程TID之后如果是Java进程还需要用printf %x\n TID转成十六进制再去jstack里搜对应的nid值。这一步操作本身不难却是从看见现象走向找到根因的分水岭。一旦你能在代码层面看到那个线程正在执行的调用栈剩下的事基本就是顺藤摸瓜了。3. 高频CPU元凶的长相与识别方法这些坑我最常遇到排查CPU问题做多了之后你会发现根因翻来覆去就那么几类。我把它们归纳一下给出各自的长相和识别方法看到一个现象就能往对应方向上想。3.1 死循环与空转逻辑最经典也最容易复现死循环导致CPU高是新手最容易遇到、也最容易理解的问题。常见场景包括业务代码里的while(true)循环退出条件因为某个状态一直没有满足而永远不触发递归调用没有正确的终止条件定时任务里对一个大集合反复进行无效遍历这类问题的典型特征是CPU稳定在接近某个核的100%水平占用的线程号相对固定线程栈多次采样都指向同一段代码。识别方法很简单做两到三次线程栈采样时间间隔几秒钟。如果每次采样里同一个线程的栈顶都在同一个函数上而且这个函数本身没有阻塞逻辑比如没有在等锁、等IO那基本就是死循环了。3.2 锁竞争、自旋等待与上下文切换风暴锁竞争导致的CPU高往往被忽略因为它的表现非常隐蔽。正常情况下一个线程持有锁其他线程在等待锁阻塞是不消耗CPU的。但如果锁的实现涉及自旋spin lock或者锁竞争引发的线程频繁阻塞和唤醒导致上下文切换暴增CPU消耗会显著上升。这里顺便回答一个很多人困惑的问题**单核CPU上自旋锁为什么不会死循环**答案是自旋锁设计默认基于SMP多核环境持锁线程和被锁线程可以在不同核心上并行执行所以自旋等待有意义。在单核CPU上持锁线程如果不能被抢占CPU时间或者说自旋线程占住了唯一的核心持锁线程就永远得不到执行自旋锁就退化成了死循环。这也是为什么内核在单核配置下通常会禁用自旋锁或者把自旋等待替换为睡眠等待。Java和.NET里的锁大多不是单纯的spin lock但也有轻量级锁、偏向锁和自适应自旋机制。当你看到CPU高而大量线程栈阻塞在锁获取上时要考虑是不是锁粒度过大或者产生了锁的热竞争。这种情况下的解法是通过线程栈统计出竞争最激烈的锁对象然后把锁粒度拆细、改用读写锁或者用无锁数据结构替换。3.3 GC频繁与内存抖动运行时的隐形CPU杀手在Java、.NET这类带垃圾回收的语言里CPU高有一个非常隐蔽的常见原因内存持续在分配-回收的循环里打转。当堆内存反复达到阈值触发GCGC线程会占用大量CPU而业务线程又在不断分配新对象两者互相拉扯形成恶性循环。GC导致CPU高的典型表象是CPU占用呈锯齿状周期性波动业务线程本身占用并不高GC线程Java里是GC Thread占用了大量CPU。遇到这种情况第一步是拿到GC日志看每次GC的频率、耗时和回收前后的堆占用。如果每次GC回收后堆占用都降不下来说明是内存泄漏或内存设置过大如果回收后能降下来但很快又涨上去说明是对象分配速率过高。解决方向不外乎调整堆内存参数、检查是否存在大对象或内存泄漏、优化业务代码减少对象创建。这里提醒一句不要一上来就调堆大小先搞清楚是泄漏还是分配过猛否则调参只会把问题掩盖掉等数据量再涨一批又会爆发。3.4 系统服务、驱动与第三方进程的干扰这类问题最容易被人甩锅但也最常见。Windows系统里的典型元凶包括comsurrogate进程COM Surrogate莫名CPU高常见诱因是缩略图预览、视频文件解码或者损坏的Shell扩展edge浏览器或Chrome的多个渲染进程叠加起来吃掉大量CPU尤其开了大量标签页或装了吃资源的扩展时系统索引服务Windows Search、Defender实时扫描在后台大面积扫描文件各种输入法、录屏软件、远程控制软件的后台进程我自己踩过的一个坑是某台Windows开发机CPU持续偏高排查了半天发现是一个叫COM Surrogate的进程在疯狂吃CPU最后定位到是一个第三方图片预览插件注入到系统缩略图进程里导致的删掉那个插件后瞬间恢复。另外很多开发者反馈IDE比如IDEA很卡、占用CPU很高除了项目本身编译索引的原因之外也不要忽略插件市场里那些常年不维护的老插件它们经常是后台CPU大户。Linux服务器上类似的问题也不少logrotate在日志文件巨大时的压缩操作、监控Agent的采集线程失控、某些云厂商的运维组件在凌晨跑定时任务等。这类问题的排查思路和业务代码问题不同不能只盯着业务线程栈要先把系统里所有进程的CPU分布扫一遍把所有非业务进程的CPU占比加在一起往往就能发现真凶。4. 一次真实排查全记录从100% CPU到定位根因的完整链路前面讲了不少方法论这一节我完整还原一次实际经历过的排查过程把每一步的命令、判断依据都写出来。你会发现排查CPU问题并不神秘关键是把流程走完整。4.1 现象与初步判断先排除业务真有这么高的流量那是一个工作日的下午运营反馈后台系统操作非常卡。我第一反应是看监控某台应用服务器CPU稳定在98%以上持续时间超过30分钟。但当天流量有没有异常我看了一下QPS曲线发现和平时基本持平——业务量没涨CPU却接近打满那必然不是扩容就能解决的问题一定是代码或者环境出了问题。这一步判断非常关键。如果QPS涨了5倍CPU涨到100%那是正常的你该做的是扩容限流而不是查代码。只有当业务没有明显变化CPU却异常升高时才值得进入下一阶段。反过来如果业务量没变CPU却异常高说明系统的单位处理成本变高了这是最危险的一种情况。4.2 线程级定位top、jstack与十六进制换算确认要排查之后我登录服务器执行了top看到一个Java进程PID 12345的CPU占到了整个机器的85%。这是我们的核心API服务。接着用top -H -p 12345查看该进程内的线程CPU排序看到几个线程的CPU都接近100%。取第一个线程的PID假设是23456转成十六进制printf %x\n 23456得到类似5ba0的结果。然后执行jstack抓线程栈jstack 12345 /tmp/jstack_1.txt在输出的线程栈里搜索nid0x5ba0找到了对应线程的完整调用栈。栈顶停在一个JSON序列化的方法上下面一层是一个定时任务执行器调用的方法。第一次采样可能不够准确我又等了几秒钟再采样一次两次栈顶一致基本确认这个线程长期卡在JSON序列化逻辑上。别小看这一步两次采样比对能过滤掉大量的恰好经过的假线索是我排查CPU问题的一个铁律。4.3 顺着调用栈找根因一个O(n²)引发的惨案顺着这个线程栈我去翻了对应的定时任务代码。这个任务每隔几分钟会从数据库拉一批用户数据然后统一做一次处理。往下一看问题出在一个很隐蔽的地方代码里对这批数据做了一次List.contains()判断而传入的列表居然是一个超大的ArrayList。contains操作是O(n)的外层又套了一层循环整体复杂度从O(n)变成了O(n²)。数据量小的时候根本看不出来可当某一天业务数据增长到几万条之后这行看似无辜的代码就变成了CPU大杀器。每次定时任务触发这层循环就在疯狂比较CPU直接被打满任务还没跑完下一轮又开始了于是CPU居高不下。这类问题在真实代码里非常常见它不是一眼就能看出来的死循环而是算法复杂度爆炸。识别它的方式主要靠线程栈的一致性——如果线程栈每次采样都停在同一个业务方法上你就有理由怀疑这个方法的实现存在性能瓶颈。4.4 修复、验证与复盘几个关键决策挽救了排查时间修复方案非常简单把contains判断改为使用HashSet或者把外层循环改成按ID批量查询。改完发布之后观察CPU在下一个定时任务周期内没有出现尖峰稳定回落到5%以下问题彻底解决。复盘这一段排查过程真正影响方向的关键点有三个第一第一时间确认了业务流量没有变化排除了正常负载这条路明确了是代码问题。 第二坚持下钻到线程级而不是停留在某个Java进程占用高这个层面。 第三线程栈做了多次采样对比才敢确定这个线程是持续占用而不是偶发经过。这三个决策任何一个没做对排查时间都会翻倍。尤其是线程栈的多次采样对比很多人在这一步图省事只采样一次结果被一个恰好经过的线程带偏了方向白忙活几个小时。5. 几种容易被带偏的棘手场景遇到别慌逐个拆前面讲的是标准流程但实际排查中总会遇到一些不按剧本走的场景。我把几个高频的棘手情况单独列出来遇到的时候能有个心理准备。5.1 瞬时尖峰抓不到现场用历史数据倒推很多CPU问题不是持续高而是每隔一段时间就跳一次持续几秒就消失。等你打开top尖峰已经过去了什么都看不到。遇到这种情况我的经验是先去监控系统看趋势图确认尖峰出现的周期。比如固定每小时的15分、30分出现就重点查定时任务如果固定在每天的凌晨出现就查日志清理、报表任务、备份脚本。再去看应用日志和系统日志找到尖峰时间窗口内有哪些任务在执行、有没有异常堆栈、有没有慢查询日志。很多时候CPU尖峰是结果而不是原因——一个慢SQL把数据库IO打满应用在等数据库响应等待过程中线程池线程全部忙等CPU就被撑起来了。如果监控系统没有历史采样数据那就在性能计数器层面提前布点。Windows上用PerfMon的数据收集器集Linux上用sar -u或者部署一个轻量的采集脚本把CPU使用率按秒级或分钟级落盘。等下一次尖峰出现你就有数据可看了。这里要强调一下临时应急采集好过事后猜很多故障发生的时机是随机的平时不布点真出事就只能靠直觉了。5.2 虚拟化与云主机上的CPU宿主机也要纳入排查范围在虚拟机、云主机上排查CPU问题还需要多考虑一层你看到的CPU使用率到底是虚拟机内部的真实消耗还是宿主机的资源争抢造成的有一个很典型的场景VMware或VirtualBox里的Windows虚拟机启动时提示客户机操作系统已禁用CPU。请关闭或重置虚拟机。这个错误和安全软件、BIOS虚拟化设置、CPU指令集有关常见诱因是宿主机BIOS里没有开启VT-x/AMD-V虚拟化支持或者虚拟机的CPU类型配置和客户机系统预期不符。解决思路一般是有序排查确认宿主机BIOS开启硬件虚拟化检查虚拟机CPU类型设置给客户机分配兼容的CPU核心数。云主机上还有一个常见问题是邻居噪声同一台物理机上其他虚拟机在跑高负载任务虽然云厂商的调度器会有隔离但CPU争抢仍然会造成你这边出现莫名的性能波动。这时候用top看CPU idle和steal被虚拟机监控器偷走的时间会很有帮助。Linux下可以通过mpstat -P ALL 1查看每一核的详细统计steal值如果长期偏高说明宿主机资源已经非常紧张需要提工单让云厂商介入。5.3 睿频、降频与散热硬件层的隐性因素软件排查了一圈都没发现问题最后发现是硬件层面的事这种情况我也遇到过。最常见的是三种睿频失效。CPU在满载时应该自动提升主频但如果散热不良或者BIOS设置了固定的功耗墙CPU可能一直在低频运行表现就是任务很卡、CPU看似没跑满但就是慢。Windows下可以用wmic cpu get MaxClockSpeed,CurrentClockSpeed来看当前主频是否达到预期或者用CPU-Z这类工具实时查看频率曲线。有些追求稳定性的用户会在BIOS或Windows电源设置里关闭睿频这就是win11关闭CPU睿频这个需求出现的原因。如果机器是服务器且对单核性能要求高关睿频可能会导致性能不符合预期这个取舍要想清楚。散热降频。笔记本和某些紧凑型服务器上CPU温度冲到90度以上时会触发保护性降频CPU占用不变但执行效率大幅下降。排查时看CPU温度曲线和频率曲线的相关性如果温度高频率低占用高同时出现优先解决散热。有人用CPU-Z做稳定性测试时问负载70%正常吗——其实单看70%这个数字没有意义关键是测试期间温度是否在安全范围内、频率是否稳定不掉如果70%负载温度就破90度那散热一定有问题。电源策略。Windows的电源计划被设置为节能或平衡时CPU可能不会快速拉升主频导致低负载时反应慢、高负载时性能不足。服务器上通常建议设置为高性能并配合BIOS里的相关配置。这个问题在笔记本上特别常见很多人把笔记本外接显示器当台式机用忘了电源计划还在节能。6. 排查之后的长期治理别等下一次告警再来一遍把眼前的问题解决掉只是完成了第一步接下来如果不做长期治理类似的告警迟早还会再来。这一节聊聊我这几年沉淀下来的一些治理习惯。6.1 告警阈值怎么设置才不变成狼来了CPU告警是运维社区里被诟病最多的告警之一因为CPU高和系统故障并不总是等价。如果告警阈值设得太低、持续时间设得太短一天能收到几百条告警真正的故障反而会被淹没。我的建议是区分业务高峰和低谷告警阈值按时间段设置。高峰期CPU 90%可以不管低谷期CPU 50%就要关注。加上持续时间条件比如连续5分钟CPU高于90%再告警抑制瞬时尖峰造成的误报。同时监控load average和上下文切换单看CPU使用率容易忽略CPU不高但系统很卡的场景。把告警和变更记录关联起来每次发布后的一小时内暂时放宽告警阈值避免发布过程本身触发误报。6.2 写代码时的几个CPU友好习惯很多CPU问题根子上是代码写出来的。以下几个习惯如果能在团队里落实能省掉一大半的CPU排障时间第一警惕算法复杂度膨胀。就像我前面讲的contains案例数据量上去之后O(n²)和O(n)的差距是灾难性的。写代码时养成习惯涉及循环内查表的场景优先用HashSet/HashMap尽量别在大集合上用线性查找。第二控制线程池和并发度。线程池开得太大CPU在上下文切换上的开销会显著上升。纯CPU计算类型的任务线程数一般建议不超过核心数的2倍IO密集型任务可以适当放宽但也要有个度。第三减少无意义的对象创建。在Java或.NET里高频路径上疯狂创建临时对象会加速GC触发间接推高CPU。这不是让你做过度优化而是说在热点路径上要有意识地复用对象减少分配压力。第四给定时任务增加分布式锁或防重入机制。很多调度框架在任务执行时间超过间隔时间时会开启下一个线程重复执行同一个任务多个任务叠加就把CPU打满了。加一个上次任务未结束则跳过本次的开关能避免这个问题。6.3 运维侧的例行检查清单最后给运维和SRE同学一个可持续执行的检查清单不用每天都看至少每周复盘一次各核心服务的CPU基线和周环比趋势是否有缓慢抬升的苗头是否有进程的CPU用量在持续变化即使还在告警阈值之下系统日志里是否有频繁的OOM、GC、锁等待相关提示CPU温度曲线是否有异常抬升散热风扇是否正常工作定时任务的执行时长是否有超出历史均值的趋势CPU问题大多数不是突然爆发的而是慢慢积累的。如果能在它变成半夜告警之前从趋势数据里发现苗头处理的成本和难度都会小很多。另外在选型新服务器的时候把业务类型和CPU型号匹配起来——如果业务是大量高并发短请求多核心大缓存的服务器CPU通常比单核高主频的更划算如果是计算密集型的科学计算或视频编码则要优先看AVX-512之类的指令集支持和单核浮点性能。网上那些服务器CPU天梯图可以当参考但千万别只看跑分排名关键是匹配你自己的负载特征。我现在的习惯是每次排查完一个CPU问题都会顺手把当时的线程栈、命令输出和判断思路整理到团队的运维Wiki里。同一个坑第二次踩的时候照着文档走十分钟就能定位不用再从头摸索一遍。这个习惯坚持了几年收益非常大——很多看起来无解的疑难杂症其实早就在wiki里躺着答案了。