我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

线上CPU 100%排查实战:从负载、线程堆栈到火焰图

线上CPU 100%排查实战:从负载、线程堆栈到火焰图 1. 先想清楚收到告警后第一件事不是敲命令1.1 分清楚“100%”到底是哪类指标线上CPU 100的告警很多人一上来就打开top然后盯着进程列表发呆发现能看到一堆进程名却看不出问题在哪。我见过太多人把“CPU使用率”和“负载load average”混为一谈这俩是完全不同的概念搞混了后面的排查方向都会跑偏。CPU使用率是单位时间内CPU处于非空闲状态的比例也就是真正在做计算、等IO、处理中断的时间占比。而负载是个更“宏观”的指标表示正在运行和等待运行的进程平均数量。负载高不一定CPU打满也可能是进程阻塞在IO上、锁上、网络上大量任务排着队CPU反而很闲。还有一层容易被忽略的现在服务器动辄16核、32核CPU 100%到底指“所有核都满了”还是“某一个核满了”单核打满会导致总体使用率看起来只有百分之几比如32核机器单核满载整体才3.1%表现就是接口偶发卡顿但监控面板上似乎没大问题。反过来如果是所有核都被打满那就是另一个量级的事故了系统通常已经处于半瘫痪状态。遇到告警我的固定习惯是先看三个数据uptime直接给出1分钟、5分钟、15分钟的平均负载能快速判断是突发还是持续走高。top只看第一行和进程列表重点不是看谁第一而是看us用户态、sy内核态、waIO等待、st被虚拟机偷走的时间这几项占比。wa高说明问题可能在磁盘st高说明宿主机资源争抢别一上来就赖代码。mpstat -P ALL 1逐核观察确认是不是个别核被某一组线程打满。提示先判断“是计算饱和还是排队变长”再决定往下查的方向。这一步决定了你是往代码堆栈里深挖还是往基础设施上找问题。1.2 动手之前先建立“变更时间线”CPU 100%很少是凭空冒出来的。线上系统出问题绝大多数都有触发点——只是有时候触发点不明显看起来像“突然就高了”。收到告警后我要求团队先别急着敲命令花1~2分钟把时间线拉出来告警是什么时候触发的持续了多久是第一次还是周期性出现这个时间点前后有没有发版记录、配置变更、数据库扩容、依赖服务上线流量图有没有异常上涨比如促销活动、脚本刷量、爬虫集中来袭。有没有定时任务刚好在这个时间段执行数据补偿、报表生成、日志清理这些都很容易引发CPU陡增。我遇到过不少案例CPU打满其实是因为半夜的数据对账脚本多跑了一批数据或者新上线的功能在流量高峰期触发了某个慢查询的连锁重试。如果一上来就盯着jstack分析线程栈绕了一大圈才发现是同部门的离线任务在抢资源那就白费功夫了。所以现在我的团队有一个不成文的规定所有线上排查群里的第一句话永远是“先同步告警时间点和最近30分钟的变更记录”数据没齐之前谁都不许去生产环境乱敲命令。这不是流程主义这是无数次踩坑换来的教训。2. 进程→线程→代码逐层拆解高CPU现象2.1 top命令的正确打开方式明确了是CPU本身的计算压力上来之后再去看进程。top几乎是每个后端开发者都会敲的命令但大多数人只是进去按一下大写的P按CPU排序看一眼高的那个进程就退了。这个操作本身没错但信息量远远不够。我的顺序是这样的# 全量看一眼load、us/sy/wa/st、进程占用排序 top -bn1 -o %CPU | head -30-b是批处理模式-n1只输出一帧-o %CPU按CPU占用排序。这个命令的意义在于它对终端输出的格式是“干净”的适合存日志、发给同事、回放现场。然后针对嫌疑进程再看它的线程分布# 查看指定进程内的线程以及线程的CPU占用 top -H -p PID这一步很关键。一个进程整体CPU 100%并不代表进程里每个线程都在干活通常只是某一个或某几个线程在高速运转。通过top -H -p PID你能看到进程内每个线程的CPU占比那个能冲到90%以上的线程ID就是接下来的突破口。还有两个细节我建议大家养成习惯看TIME列这个值是进程/线程累计消耗CPU的时长。如果某个线程现在的CPU占用看起来不是最高但TIME特别大说明它是持续吃CPU的“惯犯”而不是刚被流量带上来的“临时工”。另一个是看进程的启动时间如果是刚刚才启动的新进程CPU又居高不下这基本就是业务逻辑问题了。2.2 用pidstat锁住具体线程top是快照但CPU抖动很快有时候一秒钟前还是100%下一秒就掉下来了。所以快照之后最好用pidstat做一段连续采样把跳变的线程抓出来。pidstat属于sysstat工具包CentOS上装过sysstat的话系统里基本都有。它的好处是可以按进程、按线程分别统计CPU占用并且是定时采样输出方便留证据。# 按进程维度每1秒采样一次连续5次 pidstat -p PID 1 5 # 按线程维度每1秒采样一次连续5次 pidstat -t -p PID 1 5-t就是thread的维度会在输出里多一个TID列这个TID就是线程号后面跳转堆栈的时候要用它。实测中pidstat比top更直观的地方在于top只会显示当前一瞬的CPU值而pidstat能看出线程在这5秒内的平均占用稳定性更高。遇到那种“一下100%一下掉到0”的间歇性CPU飙高pidstat的连续输出比top截图更容易锁定真凶。2.3 ps命令作为补充还有人习惯用ps来查比如ps -L -p PID -o pid,tid,psr,pcpu,stat,commpsr是线程跑在哪个CPU核上pcpu是CPU占用stat是线程状态。这个命令好处是输出清爽适合导出归档但它的CPU百分比也是平均值时效性不如top和pidstat。所以我的建议是ps用来“做记录”top和pidstat用来“抓现场”。这一层排查做完你手里应该握着三个明确的信息哪个进程高、哪个线程高、线程大概占了多少CPU。接下来的问题是这个线程到底在干什么这才真正开始考验功力。3. 从线程号翻译成代码jstack、GC日志与火焰图的实战3.1 jstack和线程ID的十六进制换算Java应用在线上占据半壁江山排查Java进程CPU飙高的经典套路就是拿线程号去匹配jstack输出的线程栈。第一步拿着pidstat里的TID转成十六进制。Java的线程栈里线程ID是以nid0x...的十六进制形式出现的所以需要用printf算一下# 十进制TID转十六进制 printf %x\n TID比如TID是23456转出来就是5ba0在jstack里搜nid0x5ba0即可。第二步采集线程栈。这里有一个特别重要的习惯多取几次别只取一次。我出线上事故时一般会连续取三份每份间隔3~5秒for i in 1 2 3; do jstack PID /tmp/jstack_$(date %s).log sleep 3 done为什么要多取几次因为线程栈是瞬态快照。如果一个线程持续处于CPU密集计算中它在不同时刻的调用栈大概率是接近的这样更容易通过对比发现“稳定复现的路径”如果只取一次可能刚好抓到一段中间态的栈反而误导排查方向。第三步看栈grep -A 20 nid0x十六进制ID /tmp/jstack_xxx.log输出的前几行就是这个线程当时正在执行的代码路径。配合jstack -l PID还能输出锁的补充信息如果线程在做Lock等操作能看到具体在等哪把锁。需要提醒的是生产环境如果开了很大的JVM堆jstack在采集时会导致STW式的停顿会让服务出现几十甚至几百毫秒的卡顿。所以我的习惯是优先在业务低峰执行这个操作如果必须高峰期处理也得先通知业务侧并且只做一两次别循环十次。3.2 从堆栈里读出常见问题拿到堆栈之后读栈是个经验活。我挑几个在线上遇到过的高频场景给大家参考。最典型的是正则回溯。Java的正则在某些极端输入下会进入灾难性回溯CPU消耗可以被拉满。堆栈里的特征非常明显大量栈帧都是java.util.regex包下的方法比如Pattern$Curly.match0、GroupHead.match、Pattern$BmpCharPredicate.match这类方法重复出现。这类问题大多出现在日志打印、参数校验、手机号/邮箱/URL格式校验这些场景。排查到正则层面之后建议立刻用简单的字符串判断替代正则或者给正则加上更严格的锚点和量词避免回溯失控。另一个常见的是大对象序列化/反序列化。比如把一个几万条数据的List直接放在日志里打点或者对一个大String频繁做JSON解析堆栈里会反复看到com.fasterxml.jackson.databind或com.alibaba.fastjson的相关方法。这种问题有时候CPU占比看起来不是特别高但TIME涨得非常快因为序列化本身是“慢节奏的CPU消耗”。还有一种是自旋/循环等待。比如基于while(true)的空转轮询、Thread.sleep搭配不好的重试机制、用System.currentTimeMillis()死等某个状态位。堆栈里往往是一个简单的业务方法反复被调用没有明显的外部依赖。识别这类问题的关键是看栈深度如果每个线程的栈都特别浅来回就是同一个业务方法那多半是死循环或者自旋。我把常见栈特征整理成了一张速查表方便大家对照排查。堆栈特征典型场景处置方向java.util.regex.Pattern系列方法高频出现正则回溯简化正则、加锚点或改用字符串处理Jackson/Fastjson相关帧反复出现序列化大对象/频繁解析去掉大对象打印点、升级类型引用、考虑改用流式解析自定义业务方法反复出现且栈浅死循环、热点轮询查循环退出条件、加退出标志或超时熔断sun.nio.ch.*、epollWait等NIO方法比例很高网络事件循环正常状态重点排查事件回调里的业务耗时GC线程VM Thread、G1 Young RemSet Sampling占CPUGC本身跑得勤立刻看GC日志和堆内存堆栈分析是很上头的操作但别陷进去太久。我的经验是如果连看三份堆栈都指向同一个热点那基本已经定位了如果三份堆栈各不相同反而要考虑是不是有锁竞争或者随机性的外部流量扰动这时候把堆栈合在一起看特征而不是纠结单个线程。3.3 用perf和火焰图兜底jstack对Java有效但如果线上服务不全是Java或者问题出在更底层——比如JVM源码层、C/C服务、内核某个模块——就得换工具了。perf是Linux自带的性能剖析工具可以采集CPU指令级别的调用栈对任何用户态进程都有效。# 实时查看CPU事件 perf top -p PID # 采样并保存30秒的数据 perf record -g -p PID -- sleep 30 # 分析采样结果 perf reportperf record采集完成后会生成一个perf.data文件perf report可以把它渲染成带调用栈的统计。如果觉得命令行输出不够直观可以用火焰图工具链把结果可视化。火焰图的核心阅读逻辑就是“看宽条、不看高条”横向宽度代表CPU时间占比越宽的条越是焦点纵向是调用层级越深越靠近叶子函数。不过在生产环境用perf有两件事要提前确认一是部分容器环境里perf因为权限受限无法使用可能需要额外配置或换用perf top --guest等方案二是perf record的采样本身会给服务带来额外开销事件采样频率默认很高如果服务本就已经很吃紧建议加-F 99把采样频率降下来比如每秒99次。注意机器上如果出现perf: permission denied类似的输出别硬着头皮改内核参数。先看看是不是没有perf_event_paranoid权限或者直接改用受限容器下的perf替代采样否则为了排查一次事故把机器搞得更不稳定就亏大了。4. 线上CPU 100%的几大典型场景与避坑4.1 死循环与热点业务逻辑说到CPU 100%很多人第一反应就是业务代码死循环。没错这确实是最常见的原因之一而且它最大的特点是CPU飙升毫无征兆进程健康检查还在跳动但所有线程都在空转业务吞吐量断崖式下跌。死循环的经典触发方式我在线上见过几类while循环的退出条件依赖外部状态但这个状态永远等不到。循环里对集合做了remove/add操作导致迭代器行为异常永远走不到末尾。用contains在一个几万条元素的List里做循环判断复杂度从O(n)变成O(n*m)数据量一大CPU直接饱和。定时任务里的数据补偿逻辑补偿失败之后没有退避重试限制一直疯狂重跑。排查这类问题jstack往往是最好用的。看到堆栈里有明显的for/while循环帧找到循环变量相关的业务代码基本就跑不掉。解决方式也直接循环加最大次数限制、每次循环做一次状态校验、对集合预判大小或改用HashSet。一个更隐蔽的点业务代码里的热点逻辑不一定是死循环也可能是“正常但在错误场景下被放大”的算法。比如大量日志打印、拦截器里做了重复的权限查询、网关层对每个请求都做全链路动态配置解析。这些逻辑平时看起来人畜无害一旦流量翻个几倍CPU就会被打爆。这种情况下的特点是提升流量后CPU增长不成线性而是类似指数爆炸。排查时重点用火焰图看叶子函数的横向宽度找到宽度最大的那个函数往往就是热点。4.2 GC频繁、锁竞争导致“假业务线程”第二种典型但相对难判断的场景是JVM的GC和锁竞争。先说Full GC。老年代对象过多、元数据区膨胀、内存泄漏没爆出来之前都可能出现频繁Full GC。每次Full GC都会触发STWStop-The-WorldJVM所有业务线程都会暂停从外部看CPU使用率暴涨但业务完全没有响应。识别方式很简单jstack里只能搜到GC相关线程甚至看不到业务线程在干活再配合jstat看一下GC数据jstat -gcutil PID 1000 5重点关注FGC列如果这个数字在5次采样内持续快速上涨FGCTFull GC耗时也在涨那CPU很大概率是被垃圾回收吃掉的业务线程反而是受害者。这种情况优先处理的是内存dump堆、分析大对象、在线上的临时方案是调大堆内存或调整GC策略治本的方案还是看代码哪里不停造对象、哪里把大对象都堆在了老年代。锁竞争的情况稍微复杂一点。很多人有个误解认为线程在等待锁的时候会占CPU实际上Java里阻塞态的线程是不占CPU的真正占CPU的是持锁线程在占用锁期间执行的代码以及锁释放和重新申请过程中的系统调用开销。如果通过jstack看到大量线程处于BLOCKED状态都在等待同一把锁而持锁线程栈显示正在做CPU密集型操作那问题就清楚了要么把这个同步块拆小要么优化锁粒度。还有一种情况线程状态是RUNNABLE但一直盯着元空间做类加载、反射调用这种情况在Spring早期版本、动态代理比较多的项目里比较常见需要关注VM Thread的CPU时间和类加载统计。4.3 基础设施与容器层别把所有锅都扣给代码排查进到第4层我强烈建议冷静下来回退一步看看问题到底是不是在应用层。容器化时代CPU 100%很可能不是业务代码造成的而是基础设施的干扰。常见的有容器被设置了CPU quotacfs_quota_us当容器内多线程并发跑到配额上限时会被强制节流throttled。这时应用看到的CPU使用率可能不到100%但容器内负载很高、请求很慢。排查方式是看容器所在目录下的cpu.stat文件里的nr_throttled数值。云服务器上hypervisor层面的资源争抢其他虚拟机疯狂读写、占CPU宿主机会把一部分CPU时间“借走”表现就是top里st值很高。这种场景在物理机的mpstat输出里尤其明显如果是纯应用层排查代码分析再久也没用。虚拟机平台的故障也不少见。比如虚拟CPU进入异常状态、VMware里出现资源调度异常表现为整个VM忽然卡死、内部CPU冲高但服务无响应甚至直接重启。这时候应用层的数据已经失真了要看虚拟机管理后台的事件记录。还有一类特别容易被忽略的CPU指令集和二进制不匹配。比如某次Linux升级内核后服务启动直接报类似cpu does not support x86-64-v2的错误或者性能断崖式下降。这不是业务逻辑问题是编译或运行环境要求的CPU指令集版本变了只能通过更换镜像、重编译或者调整虚拟机的CPU模式来解决。基础设施层的排查最核心的原则是先确认问题区域的边界。如果应用层的三份jstack看不出任何热点GC指标也正常top里st又居高不下那大概率已经不是代码的事了。放下代码思维去看宿主机指标、容器限制、虚拟化平台事件往往比在代码里纠结更高效。5. 开发机与服务器同一套排查思路的不同侧重5.1 Windows开发机上CPU飙高的另类元凶聊完线上服务器顺手聊一下开发机的CPU 100%。很多小伙伴在本地开发时也遇到过CPU打满的情况桌面环境虽然和服务器不同但排查思路是可以平移的。在Windows上最典型的几个高CPU进程包括ntoskrnl.exe这是Windows内核进程。它在开发机上CPU飙高通常与驱动异常、电源策略、内存管理有关。比如休眠唤醒后偶发或者某些老旧网卡驱动疯狂中断就会看到ntoskrnl占着大量CPU。antimalware service executable这是Windows自带的杀毒组件。它在做全盘扫描、定期更新病毒库时非常吃CPU尤其在小内存机器上一扫描基本就让开发机进入半卡状态。遇到这种占用优先查看“病毒和威胁防护”里最近一次的扫描时间和触发策略。compattelrunner.exe这是Windows兼容性遥测组件。它会定期把系统使用数据上传CPU占用偶发拉升。我见过好几台机器配了它之后CPU经常无端飙到100%可以调整相关计划任务的执行策略或者彻底关掉遥测功能。排查这些开发机上的问题方法很简单任务管理器→详细信息→按CPU排序看到可疑进程之后右键→转到具体线程再做一次“分析等待链”或者用Process Explorer看线程的调用栈。不过开发机毕竟不是服务器处理起来自由度大得多找到元凶后卸载组件、调整服务、修改电源计划都不影响线上可以大胆一点。5.2 线上服务器不同于本机的操作纪律本地怎么折腾都行线上不行。排查线上问题时我给自己和团队定的纪律有三条第一所有命令的输出必须留档。top、pidstat、jstack、jstat的结果按时间戳命名存放到统一目录。这不只是为了事后复盘更关键的是很多问题的现场只有一次如果当时没记录后面再想分析就什么都没有了。第二能不重启就不重启必须先原地取证再往后处理。CPU 100%的现场最值钱的资产就是进程内的线程栈和GC日志。一旦重启这些内存中的证据就全部丢失了。我承认很多时候重启能快速止损但止损之前花三分钟抓一下现场这是对整个团队最友好的行为。第三不要跳过中间态直接猜结论。线上环境的变量非常多流量、发版、依赖、资源限制都可能改变现象。看到CPU高就一口咬定“是某段代码的问题”然后让业务开发去查代码这往往是低效的。正确的方式是按前面说的流程从宏观指标逐层向下一步步缩小范围每一步都有数据支撑。6. 常见问题与排查技巧实录6.1 一张线上CPU告警快速处理速查表把前面提到的内容浓缩成一个“告警后60秒”的操作卡片实测下来非常管用。时间窗口操作目的0~10秒确认告警时间点、机器范围、当前负载、top -bn1建立基线区分单点还是集群问题10~20秒查看最近变更、发布记录、流量趋势建立时间线找到可能触发点20~40秒pidstat -t -p 锁定高CPU进程/线程定位到线程粒度40~60秒jstack连续取2~3份 jstat查看GC获取代码级证据这几步做完要么问题已经定位要么手里的证据足以支持下一步专项分析。很多新手容易在40秒这个节点上慌了甚至直接跳到了“重启”这个动作。我的感受是CPU 100%的问题大部分都可以通过冷静的60秒现场取证得到线索真正需要“果断重启”的场景反而很少。6.2 那些年踩过的坑按我自己的经历再补充几个最容易踩的坑每一条都是真金白银换来的。第一个坑只看top一秒钟的结果就下结论。CPU是动态的瞬时快照很可能恰好抓到一个不具代表性的画面。比如某个后台任务刚启动3秒正好被你看到了100%它跑完就退了你却把它当成事故主因在那折腾了半天。正确的做法是至少以5~10秒为窗口观察用pidstat或者top的-d刷新频率来看趋势而不是看单帧。第二个坑把单核满载和多核满载混为一谈。两种场景的处理方式完全不同。单核满载可能是某个带状态的On/Off任务被放在单一线程上执行业务影响通常较小多核满载往往意味着并发压力全面上来或者死循环产生了多个线程。用mpstat -P ALL确认核数分布再决定要不要喊更多人协助。第三个坑忽略了容器/虚拟化边界。现在线上环境动不动就是容器、虚拟机但很多人的排查习惯还停留在物理机时代看到CPU高就以为一定是进程问题。结果查了半天jstack最后发现是容器CPU quota被限流或者宿主机高负载传导过来。所以排查之前先确认运行环境容器看cpu.stat虚拟化看st值和平台事件这些东西不花一分钟就能排除掉但可以有效避免几个月排查毫无进展的尴尬。第四个坑拿开发机的Windows经验套生产服务器。Windows上排查高CPU进程的思路没问题但生产环境基本都是Linux而且系统裁剪得更严格、权限更受限。比如很多排查命令需要sudo或者特定的capability没有提前开通遇到事故时你连diagnostic工具都跑不起来。建议平时就把常用命令在堡垒机上试一遍确认权限、确认输出格式别等出事了再临时找运维开通权限。6.3 把排查步骤脚本化把经验沉淀给团队分享一个我个人的习惯线上CPU排查的常用命令我是写成脚本存起来的取名叫cpu-debug.sh。它做的事情不多但都是按时间顺序自动执行包括当前时间戳、load、top快照、pidstat线程采样、jstack连续取样、GC概览最后自动打包成一个目录。出问题时我可以直接一条命令把现场的“证据包”完整留存下来然后慢慢分析。这个脚本花不了多少时间但它带来的价值非常大。第一它把排查动作标准化了团队里任何人遇到同类问题都能复用同一套流程第二它减少了事发时的手忙脚乱人是情绪动物半夜被叫起来处理事故精神高度紧张手敲命令很容易出错或漏步骤脚本能保证每次采集的完整性。最后再分享一个从实战中得来的小技巧排查CPU高的问题时手机拍屏和截图可以作为辅助但真正可靠的是命令输出的文本文件。文本可以精确检索、可以计算、可以对比突发事件哪怕当时没看懂事后依然能够复盘。所以无论当时多慌多花十秒钟把输出重定向到文件都不会亏。
返回列表