ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

性能测试实战:内存溢出定位与TPS曲线分析

性能测试实战:内存溢出定位与TPS曲线分析 1. 站在性能测试的深水区为什么你总是“压测一时爽调优火葬场”做性能测试最怕的不是压测本身跑不起来而是压力一上来系统就像个开始漏水的桶——TPS一会儿高一会儿低内存像坐了滑梯一样往下掉然后某一次请求直接甩给你一个OutOfMemoryError。你翻日志、查监控、问开发忙活大半天还是一头雾水。这几年我大大小小做了几十个项目的压测从传统单体应用到分布式微服务从物理机部署到容器化K8s踩过的内存溢出的坑、分析过的TPS曲线比看过的技术文档还多。说实话性能测试这个活儿很多人以为就是拿Jmeter把线程数调大看个聚合报告然后写个“平均响应时间多少、TPS多少、错误率多少”就完事了。但真正的问题恰恰藏在你没看的地方TPS为什么掉下去了内存为什么一直涨GC为什么频繁得像呼吸一样这些才是压测的本质。这篇内容我打算一次性把内存溢出问题定位和TPS分析这两件大事讲透。适用的人群很广刚入行想系统学性能测试的测试工程师、写代码但被线上OOM折磨的后端开发、以及那些被领导要求“压一下看看性能”但完全不知道从哪下手的运维同学。本文会结合我实际用Jmeter、LoadRunner做压测的经验手把手拆解从发现OOM到定位根因再到通过TPS曲线反推系统瓶颈的完整思路。看完你至少能明确一件事下次再遇到内存溢出或者TPS异常你该按什么顺序去看、去查、去解决而不是只会重启服务。2. 先搞清楚内存溢出的常见类型再谈定位2.1 堆内存溢出Java Heap Space——90%的OOM都是它在讲定位技巧之前我们得先把内存溢出这头大象分清楚。服务端应用尤其是Java技术栈最常见的就是堆内存溢出报错大概是这样的java.lang.OutOfMemoryError: Java heap space。遇到这个错误意味着你JVM的堆内存已经被对象占满了新对象分配不出空间了。堆内存溢出的根因无非两类对象真的太多超出了堆的容量。比如压测时并发量太大每个请求都产生大量对象堆默认大小扛不住。对象本该被回收但一直被引用也就是内存泄漏。比如静态集合类不断放入数据、IO流未关闭、ThreadLocal使用不当等GC回收不了可用内存一点点被蚕食。我的建议是遇到Java heap space先不要慌也别急着把-Xmx调大。调大堆内存只能暂时缓解症状如果根因是泄漏跑一段时间照样爆。正确姿势是先看GC日志和堆转储文件分析对象分布确认是“对象真的多”还是“对象被不该持有的地方持有”。2.2 元空间溢出Metaspace与直接内存溢出Direct Buffer Memory除了堆内存溢出还有两类间接相关的OOM容易被忽视。元空间Metaspace存放的是类的元数据。如果压测中频繁动态生成类比如CGLIB代理、反射生成类、热部署加载类元空间就会持续增长最终报java.lang.OutOfMemoryError: Metaspace。这属于“永久代/元空间”层面的问题排查思路是看加载了哪些类、由哪个ClassLoader加载的。直接内存溢出通常表现为java.lang.OutOfMemoryError: Direct buffer memory。使用NIO或者Netty这类框架时堆外内存申请过多也会OOM。这类问题比较隐蔽因为堆内存看起来很正常但是堆外内存占用飙升最终进程被系统杀掉。压测时发现程序莫名其妙crash先看看是不是堆外内存的锅。2.3 Jmeter性能测试步骤中如何触发与监控OOM顺便说一个测试侧的现象如果你用Jmeter压测自己电脑上先OOM了多半不是被测系统的问题而是Jmeter本身内存不够。很多新手压测时会把Jmeter的堆调得很小结果并发一高Jmeter自己先挂了然后得出“被测系统性能差”的错误结论。正确的Jmeter压测姿势是修改jmeter.bat或jmeter.sh里的HEAP参数一般建议设到4G以上压测机内存够大的话甚至可以给8G。使用-n命令行模式跑压测不要开着GUI压GUI模式本身就要吃掉大量内存和CPU。监控Jmeter自身的GC情况压测过程中如果Jmeter的Available Memory持续下降或者日志里频繁出现GC那说明压测机已经是瓶颈了需要改用分布式压测。也就是说做性能测试的第一步是保证压测工具本身不拖后腿否则你测出来的数据全是“假数据”。这个细节往往被很多人忽略。3. TPS分析曲线背后藏着的秘密3.1 TPS到底是怎么算出来的TPS全称是Transactions Per Second每秒事务数。对HTTP接口来说一个事务通常就是一个完整请求从发起到响应完成。在Jmeter里TPS可以在聚合报告或者通过ServerAgent插件直接查看在LoadRunner里则是通过Controller的分析图看“Transaction Response Time”和“Hits per Second”等指标。但很多人对TPS的理解过于表面。它不是一个孤立的数字而是和并发用户数、响应时间、错误率连在一起的。它们之间的关系可以用一句话概括TPS是并发数除以平均响应时间。比如说100个并发用户平均响应时间0.5秒那理论TPS就是200。这只是一个粗略估算真实场景中由于锁竞争、线程上下文切换、网络开销等原因实际TPS会明显低于这个理论值。压测过程中看TPS不能只看“最高值”更要看TPS曲线的形态。TPS曲线大致有几类平稳型随着并发增加TPS线性上升然后趋于稳定这是健康的曲线。先升后降型TPS在某个并发点达到峰值之后再增加并发反而下降说明系统已经有瓶颈了。剧烈波动型TPS高一下低一下像锯齿一样往往是线程阻塞、GC暂停、或者后端依赖不稳定导致的。3.2 通过TPS曲线反推内存问题这里想重点强调一个经验TPS曲线的变化往往能提前告诉你内存要出事了。举个例子压测一个Web应用200线程跑10分钟前5分钟TPS稳定在500左右。但从第6分钟开始TPS开始断崖式下跌降到200、100同时响应时间飙涨。这个时候如果你去查JVM的GC日志大概率能发现GC越来越频繁每次Full GC的耗时也从几百毫秒涨到了几秒。为什么会这样因为内存中的垃圾对象累积越来越多GC线程要花费大量时间回收导致业务线程被STWStop The World阻塞请求自然处理不过来了。这是一个典型的“TPS下降—GC频繁—对象堆积—最终OOM”的链条。所以我的习惯是压测过程中一旦发现TPS掉头向下第一反应不是去看代码逻辑而是立刻拉三组数据当前JVM堆内存使用率曲线GC频率和GC耗时曲线线程池活跃线程数和阻塞线程数如果这三组数据里有两个以上异常那基本可以确定问题出在JVM内存管理层面而不是业务逻辑层面。反之如果堆内存和GC都正常TPS却下降那可能是数据库连接池满了、线程池队列积压、或者是下游接口变慢导致的。3.3 性能测试指标整体对照响应时间、并发数与TPS的综合判断再补充一下性能测试中几大核心指标的联动分析方法。团队里经常有人拿了压测报告问我“TPS是800响应时间平均300ms这个性能算好吗”我没法直接回答因为判断性能好坏永远不是看单个指标而是看它们在不同压力阶梯下的变化趋势。一个相对完整的判断方法是把压测分为“性能测试”“负载测试”“压力测试”三个阶段。性能测试阶段验收系统能否在预期并发下满足SLA比如“300并发下TPS不低于1000平均RT不高于500ms”负载测试阶段只增加并发数观察系统何时达到TPS峰值以及曲线如何变化压力测试阶段在超过峰值的并发下继续压看系统降级行为、错误率、以及是否会崩溃。各指标联动关系我一般用这个表格辅助判断现象组合可能瓶颈排查方向TPS高、RT低、并发持续增加系统仍有富余能力继续加压找拐点TPS低、RT高、CPU高计算密集型瓶颈代码或算法问题做CPU火焰图定位热点TPS低、RT高、CPU低外部依赖阻塞或锁等待查数据库慢SQL、Redis超时、锁竞争TPS下降、堆内存上涨、GC频繁存在内存泄漏或对象堆积做堆转储分析大对象TPS波动剧烈、RT偶尔飙升容器或宿主机资源争抢、线程池队列满查容器监控、线程池配置这张表我建议收藏排查时直接对照着来能省掉很多盲目试错的时间。4. 性能测试实操从环境准备到完整压测的步骤拆解4.1 LoadRunner和Jmeter的选型分析既然标题涉及了LoadRunner和Jmeter两种主流工具我先聊聊选型。这两者我都深度使用过各有优劣不存在谁完全取代谁。LoadRunner是老牌商业工具功能全、报表丰富、协议支持广比如支持SAP、Citrix等老协议适合大型企业合规化性能测试。缺点是贵而且本身的学习曲线陡峭安装配置也比较繁琐。如果你身在重视“正规军”的团队或者被测系统用的是老旧的客户端/服务器协议LoadRunner依然是合理选项。Jmeter则完全开源免费社区活跃插件生态丰富如jpgc性能监控插件集做HTTP/HTTPS接口压测非常顺手。对绝大多数互联网公司来说Jmeter是最具性价比的选择。我需要特别提一句Jmeter的聚合报告虽然常用但看TPS趋势还是建议配合后端监听器将结果写入InfluxDB然后用Grafana展示曲线这样你才能看到完整的趋势变化而不是只看一个平均值。4.2 Jmeter性能测试步骤脚本编写、参数化与场景设计用Jmeter做压测我有一套固定的操作流程这里完整分享出来。第一步设计测试计划。先明确要压哪个接口、压多少并发、跑多长时间、验证什么指标。比如“对登录接口做300并发、持续15分钟的稳定性测试期望TPS不低于500RT低于300ms错误率为0%”。没有明确目标的压测做完了也是白做。第二步编写脚本。用HTTP请求取样器配置好协议、域名、端口、路径和请求体。这里提醒一句如果需要登录态要添加HTTP Cookie管理器或者用HTTP头管理器加Token不然压测时大量请求因为鉴权失败而返回401TPS全是假的。第三步参数化。千万别让所有线程都发一模一样的请求这不符合真实场景。用CSV数据文件设置把用户数据做成参数化比如不同的用户名、商品ID等更贴近真实负载还能避免服务端命中缓存导致测试失真。第四步添加监听器。至少加三个查看结果树调试时用正式压测要关掉否则IO开销极大、聚合报告看整体TPS和RT、后端监听器写入InfluxDB配合Grafana看实时曲线。第五步设定压测场景。用线程组的调度器设定持续时间。我个人比较推荐“阶梯加压”先用Jmeter插件Ultimate Thread Group设置每隔2分钟增加50个并发直到达到目标并发然后维持10分钟。这样能观察到系统在压力缓慢增长过程中的性能拐点而不是一次性灌进去200个并发直接把系统打死。4.3 LoadRunner性能测试步骤从脚本录制到场景执行的关键点再简单说下LoadRunner的操作路径。用Vuser Generator录制脚本或者写脚本用Controller设计场景通常是手动场景设置虚拟用户数、加载策略和持续时间最后用Analysis分析结果。LoadRunner里最常用也最实用的是Trees视图和Graphs视图。Transaction Response Time图和Running Vusers图叠加看如果Vuser还在涨但RT已经明显变慢说明饱和度快到了如果两者都下跌那系统可能已经过载了。我做LoadRunner压测的习惯是除了内置的分析器一定会把原始数据导出来用Excel再画一遍趋势图。因为LoadRunner默认的分析图有时太“平滑”把毛刺都抹掉了反而看不到那些瞬间的抖动。4.4 性能测试环境核对清单压测前必须确认的3件事在正式开始压测之前还有一份环境核对清单写下来给所有做性能测试的同学被测环境是否和生产环境等同如果测试环境是缩减版配置压测结果只能作为相对参考不能直接拍板生产容量。压测数据是否充分空库和全量数据的查询性能天差地别索引是否和生产一致这直接决定了接口性能。监控体系是否就绪服务端的基础监控CPU、内存、磁盘IO、网络、JVM监控堆内存、GC、线程数、中间件监控Tomcat线程数、连接池使用率都要在压测启动前就绪。缺失任何一个维度问题定位都会困难重重。这三点看似基础但据我观察超过一半的团队都会在其中一个环节上翻车。5. 内存溢出定位实战从jmap、jstat到MAT的完整流程5.1 定位OOM的三板斧日志、jstat、堆转储假设压测过程中被测系统报了OOM接下来你该做什么我的顺序是这样的。第一板斧看日志。检查应用日志和-XX:HeapDumpOnOutOfMemoryError参数配置这个参数一定要在压测前就加上否则OOM发生时不会有堆转储文件落下你就少了一条最重要的线索。同时在启动参数里加上-XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/data/logs/gc.log把GC日志单独输出。第二板斧用jstat实时观察JVM。jstat -gcutil pid 1000每秒输出一次观察Eden区、Old区、Full GC次数和耗时。如果发现Old区持续增长且Full GC不断说明老年代对象只增不减这是内存泄漏的典型信号。第三板斧也是最重要的一步——生成堆转储文件然后用MAT或者JProfiler分析。堆转储生成有两种方式一是利用OOM参数自动生成二是用jmap -dump:formatb,fileheap.hprof pid手动生成。如果系统还没OOM但内存居高不下我倾向于手动Dump一份快照来做离线分析。使用MAT分析时核心是看三块内容Leak Suspects ReportMAT自动识别最可能的泄漏点直接告诉我从哪个对象入手。Dominator Tree查看大对象和持有引用链定位到具体是哪段业务代码创建了这些对象。Histogram按类统计对象实例数通常能看到各个类的对象数量。对比多次Dump的结果如果某个业务类的实例数随时间只增不减那基本锁定了泄漏源。5.2 动态监控利器Arthas在OOM定位中的实战用法堆转储文件的分析是事后行为但很多时候我们希望在压测“现场”就能看到问题。这里我要重点推荐阿里开源的Arthas用好了它OOM定位效率能提升一个量级。Arthas的dashboard命令可以实时展示线程、内存、GC状态类似jstat的增强版不用登录服务器输命令就能看到全貌。至于thread命令查线程状态和阻塞情况很好用。thread -n 3直接列出CPU占用最高的前3个线程并给出他们正在执行的栈。如果压测时CPU飙高用这个命令一瞬间就能看到热点线程。最有意思的是tt命令可以记录方法调用时的入参、返回值、异常等。比如怀疑某个接口创建了大对象用tt跟踪一下就能看到每次调用的详细情况。Arthas还支持ognl表达式直接执行代码可以现场查看某个静态Map里装了多少对象、多大的对象。这种“钻进JVM里看”的能力对定位某些模棱两可的内存问题帮助非常大。5.3 东方通内存溢出文件分析的特殊套路如果你使用的中间件是东方通TongWeb那OOM分析会有些特殊之处。东方通在OOM时通常会在logs目录下生成heapdump.hprof等文件但要注意它的JVM参数配置路径和Oracle/OpenJDK默认的不太一样需要去bin目录下的setenv或类似脚本中查看具体的JVM参数设置。东方通环境下做OOM定位我建议重点检查两个方向部署在TongWeb上的应用是否因为反复热部署导致类加载器泄漏。这种情况在老项目的开发/测试环境尤其常见每次重部署都产生一批新类老类一直被某个全局变量引用着最终Metaspace爆掉。连接池和Session管理。TongWeb的Session存储如果配置不当高并发下会不断创建Session对象长期不清理堆内存被Session占满。分析东方通的堆转储文件时思路和应用服务器是一样的——用MAT打开看Dominator Tree中是否有大量com.tongweb.*包下的对象。如果有说明是中间件层面的问题如果没有再往业务类的引用链上去追。5.4 POI增量写入Excel导致堆内存溢出的案例复盘很多业务系统都有导出Excel的功能压测时导出接口是最容易OOM的重灾区。这里用一个我实际解决过的案例来说明定位思路。某系统在做接口压测的时候并发导出Excel的功能一跑就OOM。报错是堆内存溢出而且只在导出接口上出现。第一反应是导出数据量太大Workbook对象把内存撑爆了。但开发说他们用的是POI的SXSSFWorkbook流式写入按理说不会一次性把所有行都放内存里。用jmap抓了堆转储用MAT分析后发现内存中有大量org.apache.poi.xssf.streaming.SXSSFWorkbook$SheetDataWriter对象每个对象又引用了大量字符串数组。再深入一看问题出在代码里虽然用了SXSSFWorkbook但开发在循环里手动维护了一个Map缓存了所有行的Cell数据用于后续的业务计算。这个Map的key和value都是String几百个线程同时导出每个线程几万行数据Map里的字符串对象就把堆吃完了。这个案例的教训是内存溢出的表象在POI根因却在业务代码的缓存逻辑。只分析技术框架是远远不够的要从引用链往业务代码上追找到真正持有大量对象的那个类。5.5 Impala内存溢出的原因分析与应对思路大数据组件里Impala的内存溢出是出了名的难排查。Impala使用C编写它的内存管理和Java完全不同通常不会出现Java那种OutOfMemoryError报错更多是查询失败返回Memory limit exceeded错误。Impala内存溢出的常见原因查询的中间结果集过大。比如多表Join时没有合理的过滤条件下推整个大表的数据都进内存了。分区策略不合理。分区数过多导致每个查询需要打开的扫描线程过多内存被元数据和句柄吃光。并发查询数量超过可用内存。应对思路一般是先检查impalad的内存配置在--mem_limit参数里调整单查询内存上限再从SQL优化入手避免全表扫描和大表Join最后是规划好内存队列控制同时运行的查询数。这类问题的定位需要结合Impala Web UI和/proc/pid/status等系统层面的内存数据去看因为你没法用Java那套工具链来分析C进程。6. 常见问题排查与效率提升技巧6.1 内存溢出排查的常见误区与避坑指南做了这么多年的性能测试和OOM排查我总结出几个常见误区每个都是真金白银换来的教训。误区一一看到OOM就怀疑堆内存设置太小。这是最典型的“头痛医头”思维。堆内存占用高不等于堆内存设置小可能只是你的代码写得不合理。盲目调大堆内存只是延迟了OOM爆发时间治标不治本。误区二只看free -m判断内存是否充足。Linux的free命令显示的内存使用情况是包含Page Cache的它不能直接反映JVM堆内的对象占用情况。判断JVM内存还是要看JVM自己的监控数据。误区三压缩压测时间用“短平快”的方式跑3分钟就算完事。内存泄漏类问题往往是长时间运行后才暴露的。我见过很多稳定性测试跑8个小时都不爆但跑24小时就OOM的案例。如果测试目标是验证稳定性建议至少跑12小时以上或者通过加大压力让问题提前暴露。误区四压测数据过多依赖平均值。TPS和RT的“平均值”往往掩盖了长尾问题。比如平均RT是200ms但P99可能是3秒。用户体感上的问题基本都来自尾延迟分析时必须同时关注P90、P95、P99指标。6.2 TPS分析中容易遇到的假象与困惑TPS曲线分析里也有几个常见的坑这里一并说清楚。首先TPS高并不代表系统健康。有时候TPS很高但错误率也高得离谱——实际上是服务端快速拒绝了大多数请求返回了错误码。这种“虚假TPS”会严重误导判断。所以分析TPS时一定要搭配错误率看两者结合才有意义。其次压测结果和网络环境强相关。本机压测和跨机房压测的TPS差异可能达到倍数级。如果压测过程中网络出现抖动TPS曲线会突然掉头。不要一看到曲线异常就去查应用先确认压测机和被压测机之间的基础网络是否稳定。第三Jmeter的“线程数”不等于“并发数”。设置300个线程不代表系统同时有300个并发请求在跑。如果线程只是处在等待响应的状态实际并发请求数可能远低于线程数。要评估真正的并发压力得看服务端的活跃连接数。6.3 常用排查工具速查表操作系统层面的内存和CPU排查我一般用top、vmstat、dstatJVM层面的排查用jps、jinfo、jstat、jmap、jstack、jcmd线程和堆分析用Arthas、MAT、VisualVM压测分析工具用Grafana InfluxDB来可视化Jmeter监控数据。如果是容器环境还要看kubectl top和具体Pod的cgroup内存限制容器内存溢出Pod OOMKilled和JVM堆OOM的表现不一样前者是进程被杀后者有Java报错栈不要混淆。对于接口调用链的问题建议用Arthas的trace命令跟踪一次完整调用的耗时分布看看是Controller层耗时多还是DAO层耗时多。如果DAO层耗时多再用showSQL执行计划来排查慢SQL。6.4 压测数据记录规范最后分享一个我个人坚持的记录规范每次压测都要记录完整的环境信息和参数配置。包括压测工具版本、Jmeter线程组配置、被测应用的JVM启动参数、中间件版本、数据库连接池大小、压测时间等。别小看这个动作很多问题在当天查不出来过了一周再回头分析数据时如果当时没有留下完整的参数记录就只能两眼一抹黑重新压一遍。规范化的记录不仅是专业性的体现更是高效排查的基石。7. 最后再分享一点实操倾向回头看看这些年做性能测试踩过的坑最大的体会就是性能测试不是“测完给报告”就结束了它是一个对系统深层次认知的过程。内存溢出和TPS分析这两件事其实是一体两面——TPS掉下去往往是内存或资源出了问题内存持续上涨又一定会体现在TPS曲线上。把两者联动起来分析才能做到真正的根因定位。再分享一个小技巧这是我实际工作中觉得最划算的一个习惯每次压测前先打开GC日志和系统监控然后录一段“初值快照”包括当前堆内存使用量、Full GC次数、线程数。压测结束后再打一份“终值快照”两份对比一下很多问题的答案就已经摆在面前了。不要一上来就翻代码翻日志先看数据走向再顺藤摸瓜。这套方法论无论是用Jmeter还是LoadRunner无论是测Java服务还是测大数据组件都适用。
返回列表