干性能测试这行,LoadRunner压出来的CPU报表,大概是所有报告里最容易被误读的一张图。你这边刚把压测跑完,那边开发已经凭着“CPU使用率90%”断言这是CPU瓶颈,要求加机器。可等真从LoadRunner的Analysis里翻出完整证据链,CPU使用率往往只是表面现象——它可能是内存置换、锁竞争甚至脚本设计不当造成的“假象”。这篇文章不按菜单讲工具按钮,重点围绕LoadRunner性能测试里“CPU瓶颈”这四个字的完整判定链路展开,从监控怎么配、报告怎么看、伪瓶颈怎么排除,到调优落地的全过程,都按实战逻辑走。适合正在学LoadRunner、准备性能测试面试,或者已经在压测项目里被CPU问题卡住的人。
1. 为什么压测现场最先盯住的是CPU
1.1 响应时间模型里的CPU
用户点一个按钮,请求从浏览器出去,经过网络、负载均衡、应用服务器、数据库,最后再回传。这条链路上几乎每一跳都要消耗CPU:应用服务器要解析请求、执行业务逻辑、做序列化;数据库要生成执行计划、扫描索引、排序聚合;中间件要调度线程、维护连接池;甚至连日志框架的每次写入都要占用CPU。所以CPU是一个典型的“汇总型指标”,它会受到内存、磁盘、网络、锁等各种模块的影响,最后以“使用率升高”的形式暴露出来。
这是CPU瓶颈分析最迷人的地方,也是最容易翻车的地方。一个请求处理得慢,可能是因为CPU真的不够用,也可能是因为线程在等锁、内存在疯狂换页、GC在反复清扫堆空间。如果把这些底层原因一股脑都装进“CPU瓶颈”这个筐里,你最后得到的不是定位,而是一条错误结论。我见过不少项目,压测发现CPU高,直接扩容一倍机器,结果问题依旧。原因很简单:CPU是那个替罪羊,不是凶手。
1.2 LoadRunner性能测试闭环中CPU的位置
LoadRunner在性能测试里的角色分三块:Virtual User Generator写脚本,Controller搭场景和控制压力,Analysis出报告。瓶颈分析这件事,主要发生在Controller和Analysis之间的这一段。Controller负责在压测过程中采集监控数据,Analysis负责把这些数据变成曲线和报表。CPU使用率、CPU队列长度、上下文切换次数这些计数器,都会汇总到Analysis里和事务响应时间一起展示。
整个性能测试闭环一般是:先做基准测试拿到单用户基线,再逐步增加并发寻找拐点,发现瓶颈后定位根因,优化完再做回归对比。CPU瓶颈分析就落在“定位根因”这个环节。很多人第一次接触LoadRunner时,以为工具能自动告诉你瓶颈在哪,实际上它只负责把数据摆在你面前,并且用颜色标出“可疑区间”。Analysis的默认报告会标红一些利用率高的资源,但标红不等于定位,更不等于结论。你需要靠自己的判断,把“CPU高”和“响应时间慢”“队列堆积”“TPS上不去”这些线索串起来,才能形成一条完整的证据链。
2. 想要准确的CPU数据,监控配置这一步不能省
2.1 负载生成器与服务端资源监控分开看
我在带新手的时候,问得最多的一个问题是“为什么压测到200用户,CPU就100%了?”我第一反应是问:你说的CPU是哪台机器的?很多人根本没分清,LoadRunner的Controller里既有脚本机也有被监控的服务器,显示的颜色和节点完全不一样。如果负载生成器(Load Generator)的CPU先飙满,压力就发不出去,虚拟用户会在本机排队,服务端看到的并发数压根达不到设计值,所有后续分析都是废墟。
所以压测正式开跑前,我建议先做一个冒烟测试:用小并发跑两分钟,确认负载生成器的CPU使用率低于50%,网络没有丢包。这个动作看起来多余,实际上能救你很多次。项目的业务方只关心服务端指标,很少意识到压力机本身也是一台有CPU的物理机。你用的脚本里如果有大量字符串操作、正则提取、日志打印,LG机CPU很容易成为瓶颈。这台机器的CPU数据应该和服务端CPU分开记录,Analysis里的图也要分开放,别混在一起。
服务端这边的CPU监控有很多种开启方式。Windows目标机可以通过性能计数器直接采集,LoadRunner的“Windows Resources”图会显示% Processor Time、Processor Queue Length等指标。如果是Linux/Unix目标机,传统方式是通过rstatd协议去拉取数据,操作不算复杂,但有一些系统会禁用rstatd,这时候可以改用SiteScope代理来采集。SiteScope是LoadRunner生态里的监控组件,不是广告,只是给“监控不通怎么办”提供一个标准解法。无论用哪种方式,目标都只有一个:拿到服务端CPU在压测时间窗口内的真实变化曲线。
2.2 Windows和Linux监控计数器的选取
CPU使用率不是只有一个数。Windows下光CPU相关的性能计数器就能列出十几项,常用的这几个必须要认识:
- Processor% Processor Time:逻辑处理器处于忙碌状态的时间百分比,默认是所有逻辑核的平均值。
- System\Processor Queue Length:处理器就绪队列长度,反映有多少线程在等待CPU。
- System\Context Switches/sec:每秒上下文切换次数,线程调度频率。
- Process% Processor Time:指定进程占用的CPU比例,用来定位是哪个应用在吃CPU。
Linux下建议用mpstat和vmstat配合,看几类CPU时间:%user是用户态业务计算消耗,%sys是内核态消耗,%wa是CPU等待磁盘I/O的时间,%steal是虚拟机被宿主机挤占的时间。LoadRunner往Linux机器上采集时,很多旧版本通过rstatd拿到的只是总利用率聚合值,不一定能拆出用户态和内核态。所以我的习惯是:LoadRunner的图看趋势和关联,用服务器上的mpstat、top、pidstat抓明细和异常栈,两边结合着看。
很多人只勾选一个“Processor Time”就开跑,这是远远不够的。需要同时勾上物理磁盘写入/读取时间、内存可用字节、网络字节速率等计数器,因为CPU瓶颈判断几乎全部依赖排除法。你不看内存,就无法知道高CPU是不是换页换出来的;你不看磁盘I/O,就不知道%wa升高到底是不是慢盘拖的。监控指标至少要覆盖四类:CPU、内存、磁盘、网络。宁可多看几个指标,也不要压完一轮之后对着只有CPU数据的空报告发呆。
2.3 监控采样的细粒度
Controller里监控数据的默认采样周期一般是几秒一次,对于短时间压测或者毛刺特别多的系统来说,这个粒度太粗了。CPU如果出现一个持续两三秒的尖峰,默认采样很可能直接漏掉。建议在Controller监控设置里把采样间隔调到1秒。尤其是压测过程中如果观察到响应时间曲线像心电图一样抖动,1秒级数据能帮你判断到底是CPU在抖还是别的资源在抖。
压测时长也不能太短。我习惯跑至少10到15分钟,某些系统要跑到20分钟之后才会出现JVM内存膨胀、日志文件滚动、连接池耗尽这类慢热问题。CPU瓶颈往往不是开跑就出现的,它是随着并发堆积逐渐显现的。加压策略建议从“每30秒增加50个用户”开始,让CPU曲线先走一段平缓爬坡,找到使用率突然加速抬升的那个拐点。这个拐点对应的并发数,才是系统真实的容量边界。一上来直接拉满并发不叫压测,叫把系统打挂,打挂之后的数据没有分析价值。
3. 不只看使用率,CPU瓶颈的三个证据要互相印证
3.1 CPU使用率曲线的三种形态
拿到Analysis报告以后,第一个动作不是看平均值,而是看CPU曲线长什么样。常见的有三种形态:
第一种,平滑爬升后进入平台期,稳定在90%以上。这种形态最像“CPU算不过来”,但还不能急着下结论,因为平台期内如果CPU队列很短、线程没有等待,那说明系统只是用满了CPU,仍然能维持稳定处理。真正的瓶颈不仅要看CPU满,还要看有没有请求积压或者响应时间恶化。
第二种,周期性锯齿状。CPU每隔一段就冲高一次,然后回落到中位水平。这种形态大概率有周期任务:JVM触发Full GC、日志批处理、定时任务扫描表、甚至是监控Agent在周期性采样。要打开同一时间轴上的响应时间曲线,看毛刺是不是同步出现。如果响应时间也被拉高,问题就在这个周期任务上,单独加CPU没有意义。
第三种,CPU一直没满,但响应时间和TPS已经崩了。这是最典型的“伪CPU问题”,排除CPU瓶颈反而成了第一优先级。实际工作里这类情况很常见——数据库行锁、服务间的同步调用超时、连接池等待,都可能让响应时间暴涨,而CPU在旁边闲着看热闹。记住一个原则:CPU曲线和响应时间曲线不同步的时候,别急着怪CPU。
至于所谓的“80%阈值”或“70%告警线”,参考一下就可以,绝对不能机械套用。一个8核机器平均使用率70%,可能其中1个核已经100%,其他7个核在度假,整个请求被单核卡住了。LoadRunner的% Processor Time默认把所有核拉通平均,单核热点会被平摊掉。所以看到平均不高的时候,还要额外看一眼应用进程的CPU占比和单核使用情况。
3.2 运行队列和上下文切换
CPU使用率之外,第二个关键证据是队列长度。Windows下的Processor Queue Length如果持续大于逻辑核数的两倍,比如8核机器队列长期在16以上,说明线程正在排队等CPU,这就是典型的调度饱和。Linux下看vmstat的第一列r,如果持续跑在可调度的CPU核数以上,同时CPU使用率也高,基本可以坐实CPU瓶颈。
第三个证据是上下文切换。这个计数器经常被忽略,但它的信息量大得惊人。如果上下文切换每秒几十万次,但CPU使用率并不高,通常说明有大量线程在互相争抢、快速切换,典型场景是锁竞争、线程池设置过大或者线程空转。反过来,上下文切换高且CPU高,还要看到底是谁在消耗CPU资源。比如一个应用开了上千个线程,每个线程只做很轻量的轮询,CPU就被切来切去的开销吃掉了。
我这里给一个非常实用的判断表,写给那些拿到监控数据之后不知道该信谁的读者:
| CPU使用率 | 运行队列 | 上下文切换 | 初步结论 |
|---|---|---|---|
| 高 | 高 | 高 | 真正的CPU调度瓶颈,考虑扩容或降低并发 |
| 高 | 低 | 高 | 线程自旋/忙等/锁竞争,排查应用层 |
| 低 | 高 | 中 | I/O等待或锁阻塞,线程被挂住,CPU空闲 |
| 低 | 低 | 高 | 线程频繁切换,线程池配置很可能不合理 |
这张表不是万能公式,但能帮你把方向从“CPU满不满”转向“系统卡在哪一环”,再往后查就顺了。
3.3 响应时间与CPU在时间轴上的关系
LoadRunner Analysis里最有价值的一个操作,是新建一张叠加图,把事务响应时间曲线和服务端CPU使用率曲线放在同一个时间轴上。别只看两个图各自的均值,要看它们的时间先后关系。CPU先抬升,响应时间跟着抬升,中间隔着几十秒,这说明用户数的增长先把CPU打满了,请求开始排队,响应时间是被拖慢的——这种前后关系才支持“CPU瓶颈”的判断。
如果反过来,响应时间先涨,CPU半天之后才被拉高,那么真相更可能是数据库慢查询、外部接口依赖或者网络拥塞把请求拖住了,系统为了维持运转慢慢把CPU补到高位。分享一个我看过的真实案例:压测一个订单导入接口,响应时间从200毫秒涨到4秒,CPU却只有30%,最后发现瓶颈在数据库的锁等待。把锁优化之后,CPU才慢慢从30%爬到了45%,响应时间恢复。这个案例里,如果只盯着CPU曲线,永远不会找到真凶。
再补充一个细节:业务高峰期正常流量上涨也可能让CPU和响应时间同涨同落。要区分“真实恶化”和“线性增长”,看涨幅是否成比例。如果并发翻倍、响应时间几乎翻倍、CPU使用率线性上涨,这可能是正常的伸缩模型,系统还没到容量拐点。如果并发只增加20%,响应时间却放大3倍,CPU非线性飙升,那么问题已经不只是CPU算力了,大概率有资源竞争或者锁冲突在加剧。
4. 打着CPU名义出现的“伪瓶颈”
4.1 内存不足引发的CPU补偿问题
内存不足的时候,系统不会直接罢工,而是用CPU去“补偿”。最典型的现象就是分页:物理内存不够,操作系统把一部分内存换到磁盘上,进程访问数据时就触发频繁的内存换入换出,CPU在内核态忙着搬运数据,%sy飙高。另一个常见场景是JVM堆内存设置过小,对象老是在老年代堆积,Full GC频繁触发,GC线程本身要占CPU。表面上你看CPU使用率90%以上,冲去扩容机器,结果全扩到磁盘I/O上了,一点用都没有。
所以判断CPU瓶颈的第一道工序是排查内存。Linux下用vmstat看si和so两个列,如果持续大于0,说明内存换页很严重。Windows下可以看Memory\Pages/sec,同时观察磁盘是否有对等的读写活动。LoadRunner的Analysis报告里也会列出内存指标,但它的关联能力有限,通常需要翻原始的监控数据确认。如果确认内存吃紧,优先调堆大小、减会话缓存、关掉不必要的对象缓存,再看CPU还高不高。
4.2 锁与串行化造成的CPU尖峰
线程把共享资源锁住之后,其他线程只能等待。很多时候系统在设计上用了无意义的同步块,导致本来可以并行处理的请求全部串行,CPU为了维持一致性会频繁做上下文切换、甚至自旋空转。表现是:CPU使用率上去了,但TPS没有跟着上去,响应时间一路走跌。这个组合非常典型,也非常容易被误读成CPU不够。
定位锁问题主要靠线程Dump和数据库等待事件。Java系统用jstack抓线程,看大量线程是否Blocked或Waiting在同一个锁对象上;数据库就看AWR报告里的等待事件,例如enq: TX row lock contention或者latch: cache buffers chains。查出来之后,优化方向是缩小锁范围、去掉不必要的同步块、用无锁数据结构或ThreadLocal替换共享对象。这类问题扩CPU没用,因为核再多也只是让空转的线程轮得更勤快。
4.3 脚本设计问题伪装成CPU瓶颈
这个坑是LoadRunner项目里比较常见的“自找麻烦”。脚本里如果没有设置合理的思考时间,虚拟用户会像机器人一样连环敲接口,完全不给人喘息的机会;如果没用参数化、关联做不完整,服务器收到大量重复数据,缓存命中和业务逻辑也会失真。最典型的是把事务里的响应断言、日志输出放到高频循环里,每一次请求都要做正则匹配、写日志,CPU自然被这种“压测副作用”吃掉一大块。
压测的本质是模拟真实用户,不是测试系统在“极限锤”下能撑多久。脚本的思考时间尽量贴近业务真实操作间隔,集合点只在需要模拟特定并发尖峰时使用。日志该关的关,断言该精简的精简。很多压测项目跑完发现CPU瓶颈,最后一查是脚本里写了个输出请求体到本地日志的语句,移动端压测还好,服务端压测每一秒成百上千条日志,CPU不爆才怪。
5. 一次真实压测中的CPU瓶颈定位与调优全过程
5.1 场景还原
之前做过的订单查询系统压测可以作为完整示例。系统架构很简单:两台2核4G的应用服务器跑Spring Boot,一台数据库服务器独立部署,LoadRunner脚本模拟用户查询订单列表。目标并发200,持续15分钟,观察系统能否保持响应时间在1秒以内。第一轮压测结果很刺眼:应用服务器CPU平均使用率冲到90%,响应时间从0.5秒涨到3秒,TPS只稳定在800左右。看到这个结果,团队里已经有人拍板“CPU不够,加两台机器”。
我没有立刻同意,因为有一个细节不太对劲:两台应用服务器一共4个核,在200并发下理论上CPU用完不难理解,但TPS只有800,单核每秒才处理200个请求,这个数字对于一个简单的订单查询来说明显偏低。如果CPU烧在真正的业务计算上,TPS不该这么惨。大概率是有一部分CPU在“空转”。
5.2 逐层剥离的排查顺序
第一步先看内存和GC。用jstat观察老年代使用率,每隔几百毫秒就触发一次Full GC,GC线程本身把CPU吃掉了将近三分之一。同时应用服务器上开了全量SQL日志,每次查询都打印入参和出参,磁盘I/O也没有闲着。把JVM堆从1G调到3G、关闭SQL日志后重新压测,CPU使用率降到75%,响应时间从3秒降到1.6秒,说明内存和日志带来的影响非常大。
第二步排查锁竞争。CPU还有75%,仍然偏高,于是压测到一半抓了一次jstack,发现大量线程阻塞在同一把锁上——一个自定义的ID生成器用了同步方法,所有请求在获取ID时被串行化。改成ThreadLocal批量分配后,CPU降到60%,TPS到了1300。到这一步,CPU使用率仍然有60%,但明显每一分CPU都在干正事,TPS和响应时间都已经达标。
第三步才围绕剩下的CPU开销做细分。mpstat显示%user占大头,说明这是真实的业务计算:加密算法、序列化、List的排序逻辑都在正常吃CPU。再结合200并发下CPU 60%有余量,判断当前不需要扩容。最终改动只是把SQL日志移除、修掉锁、调大JVM堆。同样的压力和机器,响应时间稳定在1.1秒,TPS提升到1500。回头再看第一轮的90% CPU,其中有将近一半是GC、日志和锁调度贡献的“虚假繁忙”。
这个排查顺序可以沉淀成四步法:先排除内存换页和GC异常,再排除锁与上下文切换,再排除脚本失真导致的无意义CPU消耗,前三步都干净了,最后才把结论落在“CPU算力不足”上。顺序不能乱,乱了你就会把冤枉钱花在扩容上。
5.3 调优效果与复盘
调优完成之后,团队里有人说“原来第一轮的数据全是假象”,这种说法也不准确。第一轮CPU使用率90%确实是真的,系统确实在那轮测试里处于饱和状态,但饱和的原因不是“CPU资源不足”,而是“CPU被无效工作耗尽”。调优让无效工作减少,同样的CPU算力就能容纳更多有效请求。所以性能测试报告里区分“资源饱和”和“资源瓶颈”很重要,CPU高是一个现象,根因要落到具体消耗来源上。
复盘的时候我还留了一个任务:后续如果做容量规划,要把加密算法计算量作为已知的CPU开销单独建模,这样扩容估算会更准。性能调优这件事,很少有一锤子买卖,每次压测都是在帮你摸清系统CPU都花在什么地方。
6. 多轮实战后,关于CPU瓶颈的几个判断准则
6.1 报告里CPU结论怎么写
压测报告里涉及CPU的结论,至少要包含这么几项:监控节点是谁(应用服务器还是数据库)、CPU在哪个时间窗口达到多少、用户态/内核态占比如何、队列长度和上下文切换趋势是什么样的、响应时间相对CPU是滞后还是同步。结论不只是写一句“CPU使用率高,判定为CPU瓶颈”,要写清楚“排除内存换页、排除锁竞争、排除压力机干扰后,应用服务器CPU因业务计算量饱和,表现为CPU瓶颈”。排查路径本身就是报告的重要内容。
LoadRunner的Analysis可以保存视图模板,建议把自己常用的一组关联图存成模板:事务响应时间叠加CPU使用率、CPU叠加队列长度、内存换页叠加磁盘I/O。新项目压测完直接套用,很多异常第一眼就能看出来。模板化的价值在于让你不再每次重新翻菜单找图,而是把精力集中在数据解释上。
6.2 仅从CPU数值不能定结论的三类情况
第一类是平均使用率不高但单核已经打满。多核CPU里如果应用是单线程模型,或者框架把任务都堆到一个核上,其他核再空闲也没用。这时候“平均使用率”会骗人,要看进程在哪个核上、单核使用率多少。第二类是虚拟化环境里CPU steal偏高,宿主机把物理CPU分给了别的虚拟机,你机器上看到的CPU等待其实是“借用不到”时间。这时候加你自己机器的配置毫无作用。第三类是数据库和应用服务器共用一台物理机,监控图上看到的是整机CPU使用率,但应用和数据库在抢同一个CPU池,必须先拆分再定位。
这三类情况如果在现场出现,不需要马上回答“是不是CPU瓶颈”,而是先反问一句“你说的CPU高是哪一层的”。
6.3 当别人说“CPU瓶颈”时,你该问的几个问题
如果有人拿着压测结果跟你说“CPU瓶颈”,我建议你先问四个问题:第一,CPU高的是哪个节点?第二,高的是用户态、内核态还是I/O等待?第三,这是哪段时间窗口内的最高值,平均和峰值差多少?第四,压测脚本的思考时间、集合点、超时重试这些参数是怎么设的?这四个问题问完,大部分的“CPU瓶颈”结论都会自动松动。
在我个人经验里,真正确认CPU资源不足的场景,往往是业务逻辑简单、不存在锁和GC问题、SQL语句已经是索引全覆盖、日志全部削减、压测脚本也贴近真实行为之后,CPU仍然稳定高企且TPS无法再增长。到这一步再谈加核加机器,才算站得住脚。
LoadRunner整套工具用熟之后你会发现,工具本身的操作没那么复杂,复杂的永远是系统资源背后的因果关系。CPU瓶颈不是一个数值,而是一条需要串联起来的证据链。在没有把证据链拼完整之前,先别急着给CPU定罪。