
上一章咱们把故障排查的整体流程捋了一遍从告警接收到定位恢复算是把大框架立住了。这一章专攻一个所有运维和后台开发都会撞上的场景CPU飙高。说实话干这行这些年半夜被叫起来十次有八次是CPU跑满或者load average飙得没法看。很多人一上来就敲top看到哪个进程CPU高就准备kill这种做法不能说错但往往会漏掉真正的根因——尤其当你面对的是一个多线程服务、一个容器或者一台虚拟化出来的机器。我自己的固定路线是先看负载再看进程后看线程最后落到代码或者系统层。四步走完不说百分之百但百分之八十的CPU疑难杂症都能说清楚来龙去脉。这篇就按这条线展开适合刚入门的运维、后端开发也适合那些已经被CPU问题折磨过几次想建立一套系统排查方法的朋友。1. 动手之前先搞懂负载和使用率的区别1.1 load average到底在说什么很多人排查CPU问题第一步就是敲uptime看load average然后看到1分钟负载高了就慌。这里有个基础概念必须先掰扯清楚负载高不等于CPU使用率高这两个指标经常被混为一谈。load average是系统里处于可运行状态和不可中断睡眠状态的平均进程数。你可以把它想象成高速路收费站的排队长度CPU是收费员进程是车。车多排队多可能是收费员干活慢CPU忙也可能是收费口前面发生了车祸导致车辆堵着过不去进程在等IO。后者这个场景下CPU使用率可能并不高但负载可以飙得很高。看load的时候有一个粗略的参考线对于一台N核机器load average长期超过N就要警惕超过2N基本可以判定系统处于过载状态。但注意这只是参考因为load里包含了D状态不可中断睡眠的进程。我遇到过一台机器CPU使用率才20%load却到了30多最后查出来是NFS挂载点卡死一堆进程堵在IO上醒不过来。这种情况下你把CPU盯穿了也找不到问题得去看IO和内核栈。顺带说一句热词里的“一文看懂cpu cache的基本原理”其实和这个有关。CPU飙高但应用表现很慢有一种可能就是缓存命中率极低CPU大量时间在等待内存数据而不是执行指令。这种情况表面上CPU占用高本质是访存模式不友好排查思路又不一样。不过日常场景里先按负载和进程这条线走大多数问题能定位到。1.2 什么情况算“CPU飙高”以及排查前要准备什么判断CPU飙高不能只看一个数字。我用top看CPU状态时固定看五个值us用户态CPU占用跑业务代码吃的主要是它。sy内核态CPU占用系统调用、内核线程、中断处理都算在这里。waIO等待CPU在等磁盘、网络等IO完成。ststeal时间虚拟机里被宿主机偷走的时间。id空闲这个不用说。一个健康的业务系统us高通常说明业务确实在算东西sy高要警惕频繁系统调用或者线程切换wa高说明瓶颈在IOst高则要抬头看看宿主机是不是超卖了。排查前先把这些值记下来心里有个预判后面不容易跑偏。正式动手之前我还习惯做三件事一是确认时间点。CPU是什么时候开始飙的持续了多久如果是周期性发作比如每天固定时段那大概率是定时任务或者业务高峰。二是确认变更。最近有没有发版有没有改配置有没有动过流量调度很多次我排查到最后发现就是一次发布把死循环带上了线。三是拉监控图。有监控系统就看监控没监控也要尽量找历史数据。CPU飙高是瞬间的还是缓慢爬升的对判断根因的方向影响很大。瞬时飙高往往是流量冲击或死循环触发缓慢爬升则更像内存泄漏导致的GC螺旋或者连接数堆积。2. 定位流程从宿敌到真凶的三板斧2.1 top第一现场无论如何登录服务器后我第一个命令一定是top。它不一定是万能的但它是建立第一现场感最快的工具。一台出问题的机器上top输出看起来大概是这样top - 14:23:41 up 83 days, 2:14, 3 users, load average: 8.41, 5.22, 3.10 Tasks: 198 total, 1 running, 197 sleeping, 0 stopped, 0 zombie %Cpu(s): 85.6 us, 10.2 sy, 0.0 ni, 3.8 id, 0.0 wa, 0.0 hi, 0.4 si, 0.0 st MiB Mem : 32098.7 total, 1024.3 free, 24567.8 used, 6506.6 buff/cache PID USER PR NI VIRT RES SHR S %CPU %MEM TIME COMMAND 12345 root 20 0 25.3g 1.2g 12000 R 380.0 3.7 234:23.67 java第一眼看load第二眼看%Cpu(s)那行重点看us、sy、wa、st谁高第三眼看进程列表里按CPU排序靠前的是谁。注意top默认是按CPU排序的但如果没有显式按P键也确认一下排序方式。这里必须提醒一个新人特别容易踩的坑top里显示的%CPU可以是超过100%的。一个进程显示380%不代表它疯掉了而是它用满了差不多4个核心。你首先要搞清楚机器有几个核才能判断这个数字意味着什么。按数字1键可以看到每个核心的情况。top进去之后有几个交互键要记住P按CPU排序M按内存排序H开启线程视图c显示完整命令行1展开每个CPU核心的使用情况。我一般进去之后先按P再按c把最可疑的几个进程完整命令行看清楚确认不是自己误判了进程身份。2.2 pidstat行为追踪top只能看到某一个瞬间的快照排查问题我更推荐用pidstat这类可以持续采样的工具。它的好处是可以按固定间隔输出而且能记录每个进程甚至每个线程的CPU行为方便事后复盘。比如我想持续观察PID为12345的进程每1秒采样一次总共采5次pidstat -p 12345 1 5输出里会有%usr和%system两列分别对应进程的用户态CPU和内核态CPU占比。如果%usr高说明是业务代码在疯狂计算如果%system高就要考虑是不是这个进程在做大量的系统调用比如频繁读写、频繁创建线程、频繁申请内存。pidstat还有一个参数-t可以按线程维度输出效果类似top的H视图但更适合脚本化和自动化采集。我记得有一次帮朋友排查他用的压测工具一启动CPU就满top看进程名是wrk怎么看都是正常的压测流量。结果用pidstat -t一看压测进程的线程CPU分布极度不均匀只有一个线程在跑其他线程全部空闲。顺藤摸瓜才发现是他在代码里把并发数写死了根本没用上多核。还有一点pidstat很适合用来验证一个进程的CPU占用是否稳定。如果采样多次CPU数值忽高忽低可能是有间歇性任务如果长期稳定在高位更像死循环或者持续计算。2.3 线程级下钻找到具体在跑什么确诊了某个进程CPU高之后接下来要回答的问题是它到底在跑什么这一步需要用到线程级分析。如果是Java应用常规路线是这样先看进程中哪个线程在烧CPUtop -Hp 12345找到CPU最高的线程记下它的PID其实这里显示的是线程ID。比如看到线程ID是23456然后在Java的线程栈里找它。Java线程的nid是十六进制表示的所以先做一次进制转换printf %x\n 23456得到十六进制值比如5ba0。然后执行几次jstack把线程栈打出来jstack 12345 /tmp/jstack1.txt jstack 12345 /tmp/jstack2.txt在dump文件里搜索5ba0这个nid就能看到这个线程当时的调用栈代码走到哪个类哪个方法一目了然。这里我要强调多次dump的价值单独一次dump只能说明那一瞬间线程在哪里如果两次dump相隔几秒指向同一个位置才能确认它是一个持续存在的热点而不是碰巧采样到的一个瞬时状态。非Java场景怎么办用perf。perf top可以直接看到内核和用户态的热点函数直观且不需要改代码perf top -p 12345它会动态刷新把CPU占用最高的函数列在最上面。看到函数名之后结合代码或者符号表通常能很快定位到具体逻辑。有一次我用perf top排查一个C服务热点全集中在一个字符串替换函数上最后发现是业务方把一个大JSON当字符串反复做正则替换性能直接被拖垮。3. 按图索骥CPU飙高的几类典型根因3.1 业务代码死循环与空转最常见的一类原因就是在某个业务逻辑里出现了死循环或者接近死循环的空转。这类问题典型特征是us高Java或其他应用进程的单个线程长时间稳定吃满一个核。死循环的触发点千奇百怪我遇到过一次特别典型的是正则表达式的灾难性回溯。业务方写了一个类似(a)$的正则去匹配一长串并不匹配的字符串输入一旦变长匹配耗时呈指数级上涨CPU一下就满了。还有人是while循环里忘了更新退出条件条件永远为真线程只能在里面空转。这类问题用jstack或者perf top定位到具体代码行之后修起来都很快难的是怎么准确定位——而这正是线程级下钻的价值。另外有一些“看起来像死循环”但不是死循环的情况比如在大数据集上做不合理的嵌套循环、对超长列表反复contains查找、在循环里new大对象本质上都是算法复杂度失控。这类问题定位到热点之后需要结合业务代码做优化单纯重启解决不了。3.2 GC频繁与内存压力Java服务CPU飙高有一大类根因不在业务代码本身而是GC。Full GC的时候GC线程会占用大量CPU而且因为垃圾回收要STWStop The World业务线程全部被暂停结果就是服务响应变慢甚至超时看起来像系统卡死。怎么判断是GC导致的CPU问题当top看到java进程CPU高时顺手用jstat看看GC情况jstat -gcutil 12345 1000 10关注YGC、FGC、FGCT这几列。如果FGC还在持续增长且FGCTFull GC耗时不断累积基本可以断定GC是CPU飙高的主要推手。GC问题常见的深层原因是内存压力堆内存设置不合理、内存泄漏导致对象无法回收、大对象太多等。这种现象有一个很典型的“螺旋上升”特征内存占用涨到阈值触发Full GCFull GC后内存回收了一部分但因为没有解决根因过几分钟内存又涨上去再次触发Full GCCPU随之起伏。遇到GC问题我的建议是先dump堆内存分析到底是哪些对象占用了内存而不是一上来就调大堆内存。调大堆内存只能推迟下一次Full GC相当于把问题往后挪。而且堆调得过大GC本身耗时也会增加有时候反而更糟。排查过程中的jstack和jstat结果最好都留档后面复盘和对比都很方便。3.3 锁竞争、上下文切换与内核态飙高如果top显示sy内核态占用很高比us还高那么问题往往不是业务代码算得有多猛而是系统调用太频繁或者线程切换太疯狂。这类问题在我平时接到的咨询里占比不小。先用vmstat看一下上下文切换次数vmstat 1 10输出里cs列是每秒上下文切换次数in列是每秒中断次数。如果cs持续在几十万甚至上百万的级别系统大部分CPU时间都花在切换线程上真正的业务计算反而分不到资源。锁竞争是引发高上下文切换的一个主要原因。多个线程争抢同一把锁抢不到的线程要么阻塞要么自旋导致大量线程状态切换。这里有个经典问题单核CPU上自旋锁为什么不会死循环道理很简单单核CPU同一时刻只能跑一个线程如果一个线程在自旋等待锁而持有锁的线程无法获得CPU就会永远等下去出现死锁。所以单核环境下的自旋锁实现通常会限制自旋次数或者主动让出CPU避免把整个系统拖死。遇到sy高的情况我会用perf top看看内核热点在哪里。如果看到spinlock相关函数、或者大量的memset/memcpy再结合业务场景基本能判断方向。这类问题有时候靠调整代码降低锁粒度解决有时候要靠调整线程池大小、减少无谓的线程创建也有时候是因为像循环里频繁做小文件读写这种模式导致系统调用开销巨大。3.4 定时任务扎堆与批量任务还有一种场景特别折磨人CPU飙高是有规律的每天固定时间发生过了那个点又恢复正常。这种情况十有八九是定时任务扎堆。很多系统里定时任务默认配置在整点或者整半点执行比如每小时0分跑一批数据同步、清理任务、报表生成任务。而这些任务所在的机器和应用经常是混部的一到整点所有任务同时启动CPU瞬间被打满数据库连接也被占满业务接口跟着变慢甚至超时。排查这类问题就看监控把CPU使用率曲线和定时任务时间表叠在一起对比规律会非常明显。处理办法不外乎三种错峰把不同任务的执行时间错开限流在任务里控制并发度和执行频率拆分把大任务拆成多个小任务分批执行避免一次性把所有数据加载到内存里计算。3.5 虚拟化与容器环境的特殊问题现在大部分线上环境都是虚拟机或者容器。在这种环境里排查CPU问题比物理机多了一层坑。先看stssteal值。top的%Cpu(s)里如果st那一项很高说明你所在的虚拟机CPU被宿主机偷走了。这种现象常见于宿主机超卖严重或者其他虚拟机在抢占物理CPU资源。这时候你在虚拟机里再怎么调代码都没有用问题出在宿主机层面。类似的报错还有热词里提到的“客户机操作系统已禁用 CPU。请关闭或重置虚拟机”这种情况多见于VMware环境下CPU热插拔或虚拟机配置变更导致的CPU状态异常需要从虚拟化平台侧确认CPU配置是否一致必要时关机重置虚拟机配置。还有一个常见现象是宿主机的vmware-vmx进程CPU很高。vmware-vmx是VMware的虚拟机进程它CPU高通常意味着某台虚拟机负载很高。但有一种坑是虚拟机已经关机了或者处于空闲状态vmware-vmx进程CPU依然居高不下这种情况常见于内存气球回收、快照合并、或虚拟机的虚拟CPU配置与物理CPU不匹配。容器环境则要关注CPU限额。很多人在容器里看不到完整的主机信息以为进程没用上多核。热词里有个问题“spark on yarn cpu只能用1个是为什么”我遇到过类似场景容器配置了CPU limit但应用没感知到比如JVM没有识别容器CPU限制默认按物理机核数创建线程池导致资源分配失衡。另一个方向是容器只分配了一个CPU份额应用怎么跑都只能用到一个核这是cgroup配额的问题不是应用代码的问题。排查容器CPU先看这些文件cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us cat /sys/fs/cgroup/cpu/cpu.cfs_period_uscfs_quota_us除以cfs_period_us就是容器可用的CPU核数。比如period是100000quota是100000那这个容器最多只能用一个核。如果期望用多核检查一下部署配置里的资源限制是否写对再检查应用是否支持自动感知容器配额。4. 一次完整实战从告警到修复的排查记录4.1 告警与初步判断有一次值班监控突然告警某核心Java服务CPU使用率飙到600%load average从正常的2左右一路涨到10服务接口超时率明显上升。我登录服务器先按老规矩看top发现是一个Java进程几乎吃满了6个核机器是8核。确认时间点之后发现这个现象是从一次发布之后开始的发布到现在大概40分钟正好对应监控开始告警的时间。当时的初步判断是要么发布内容里带出了问题要么发布后流量模型发生了变化。看监控曲线CPU是从发布完成之后缓慢爬升的不是瞬间打满更像一个逐渐恶化的过程——比如某个资源没有被正确释放请求越多堆积越多。先不急着重启保留现场很重要。4.2 定位线程与代码用top -Hp找到了进程内CPU占用最高的几个线程都在一个业务线程池的线程上。连续执行了三次jstack每次间隔5秒对比之后发现这几个线程每次都卡在同一个调用栈上——直接指向一个新上线的功能模块里面有一段逻辑是拿用户输入的字符串去做正则匹配。看到这里是正则匹配我第一反应就是灾难回溯。因为上线内容里刚好有一段代码用了正则去匹配用户昵称正则表达式写得比较复杂遇到某些特殊输入时回溯次数爆炸。为了验证这个判断我把线程栈里对应的输入参数捞出来在本地复现了一下同样的输入正则匹配跑了几秒钟都没结束基本坐实了。然后我做了两件事一是临时从配置中心关掉了这个功能的入口开关让流量绕开这段逻辑CPU很快就降下来了服务恢复稳定二是保留当时的线程栈和输入样本反馈给开发做代码修复。4.3 修复方案与验证代码层面修复方案并不复杂把那个复杂的正则换成更严谨的写法同时在前置校验里限制了输入长度让恶意或者极端的输入根本走不到正则匹配这一步。另外在代码里补了超时保护即使未来再次出现类似的正则问题也不会无限消耗CPU。验证的时候没有直接压生产而是在测试环境用同样的输入样本做了回归确认耗时从原来的几秒钟降到了几毫秒。生产环境这边观察重启后的CPU曲线稳定在正常水位。后续我建议开发在这个功能模块里加了正则匹配耗时的监控指标防止其他输入再次触发同类问题。复盘整个过程真正耽误时间的其实不是定位问题而是初期我在GC方向多花了十几分钟——因为看到CPU是缓慢爬升的本能地怀疑是内存问题jstat看了一眼GC并没有明显异常才转回业务代码方向。这个教训是一开始就把线程栈dump下来再看其他指标往往能省下很多弯路。5. 工具速查与避坑清单5.1 常用工具与适用场景排查CPU飙高时我常用的工具和场景大概是这样的工具定位场景常用命令示例top第一时间看负载、CPU状态、进程排行top按P排序按1看核数uptime快速看平均负载uptimepidstat持续观察进程/线程级CPUpidstat -p PID 1 5vmstat看上下文切换和中断vmstat 1 10jstackJava线程栈分析jstack PID dump.txtjstatJava GC情况jstat -gcutil PID 1000 10perf热点函数分析用户态/内核态perf top -p PIDstrace跟踪系统调用strace -p PID -cstress压测复现CPU问题stress --cpu 4 --timeout 60关于stress我多说一句。在测试环境复现CPU问题或者验证自己的排查流程用stress制造一个高CPU进程很方便。比如stress --cpu 4 --timeout 60会立刻产生4个吃满CPU的进程然后你就可以用top、pidstat这些工具练手。但是生产环境慎用没事别在生产机上压测。5.2 实操中的避坑心得排查完这么多案例我自己总结了几条特别重要的避坑经验写在这里给同行参考第一top的%CPU是多核累计值不是单核百分比。看到300%不要慌先确认机器有几个核再判断这个占用是否真的异常。判断标准是进程CPU占用的绝对核数比如8核机器上一个进程400%意味着占了一半算力属于严重;但如果32核机器上一个进程400%可能只是占了一小部分影响不一定大。第二采样时间要够长。任何监控工具看一次两次都不足以说明问题。特别是那种每隔几分钟才波动一次的CPU尖峰至少持续观察一两分钟最好能配合监控系统的历史曲线。我见过不少人拿着top的一次输出就下结论结果误杀了正常的批量任务进程。第三D状态进程不要乱kill。如果load很高但CPU不高先去查D状态进程这类进程处于不可中断睡眠通常在等IO。直接kill -9有时候没效果因为它还在内核态等IO。正确做法是先查是不是磁盘、网络文件系统的IO卡住了再把对应的服务或存储问题解决掉。第四容器环境先看配额再看应用。遇到容器里CPU飙高先确认cgroup配额和CPU steal再看应用。有时候你盯着应用代码折腾半天结果只是容器配额设置不合理。第五现场信息一定要留好。排查过程中执行的命令、得到的结果、关键时间点都记录下来哪怕当时觉得没用。很多时候问题修复之后过一段时间又复现这些现场记录就是最珍贵的对比样本。我现在有个习惯每次排障都会建一个目录把top输出、jstack dump、监控截图、变更时间线都放进去一周之后归档到团队知识库。长此以往团队里遇到类似问题翻翻历史记录就能省掉一半排查时间。