什么是压力测试压力测试,即压力测试,是建立系统稳定性的测试手段。以及可能存在的隐患。压力测试主要是用来测试服务器的容量,包括用户容量,即有多少用户同时使用系统而不影响质量和流量容量。另外,通过疲劳测试可以发现系统的一些稳定性问题,如连接池中的连接是否耗尽、内存耗尽、线程池耗尽等。这些只能通过疲劳测试找到和定位。为什么要进行压力测试?压测的目的是通过模拟真实用户的行为来衡量机器的性能(QPS,单机的TPS),从而计算出系统需要承受多少机器算力来承载指定的用户数(100瓦)。耽误。因此,在进行压力测试时,需要提前设置好压力测试的目标值。这个值不能太小也不能太大,要根据业务当前预估的增长情况进行合理的评估。压力测试是一个预估(pre-drill),为了应对未来上线前可能触及的用户数量。压力测试后,优化程序性能或准备足够的机器来保证用户体验。压力测试还可以检测应用系统在出现交易泛滥时的稳定性,以及一些可能出现的问题,找到应用系统的薄弱环节,更有针对性地进行加固。这些类型的压力测试可以穿插进行。一般在压力试验各项性能指标达标后安排耐久试验。压力测试术语解释常用压力测试工具abApacheBench是Apache服务器自带的Web压力测试工具,简称ab。ab是另一个命令行工具,对发起加载的本地机器要求很低。根据ab命令,可以创建多个并发访问线程来模拟多个访问者同时访问某个URL地址,因此可以用来测试目标服务器的负载压力。总体来说,ab工具小巧简单,上手很快。它可以提供所需的基本性能指标,但没有图形结果,无法监控。JmeterApacheJMeter是由Apache组织开发的基于Java的压力测试工具。用于压力测试软件,最初设计用于Web应用程序测试,但后来扩展到其他测试领域。JMeter通过创建带有断言的脚本来验证您的程序是否返回您期望的结果,从而启用应用程序的功能/回归测试。JMeter的功能太强大了,这里就不介绍使用了,可以查询相关文档使用(推荐参考文献中的教程文档)LoadRunnerLoadRunner是HP(Mercury)出品的一款性能测试工具,非常强大,而很多企业客户在使用中,具体请参考官网链接。阿里云PTS性能测试PTS(PerformanceTestingService)是一款性能测试工具。支持按需发起压测任务,提供百万并发、千万级TPS流量发起能力,100%兼容JMeter。提供场景编排、API调试、流量定制、流量记录等功能,可快速创建业务压测脚本,准确模拟不同层次的用户访问业务系统,帮助业务快速提升系统性能和稳定性。PTS作为阿里巴巴内部使用多年的性能测试工具,具有以下特点:免运维、开箱即用。基于SaaS的压力,最大支持百万级并发,千万级TPS流量自助启动能力。支持多协议HTTP1.1/HTTP2/JDBC/MQTT/Kafka/RokectMq/Redis/Websocket/RMTP/HLS/TCP/UDP/SpringCloud/Dubbo/Grpc等主流协议。支持流量定制。全球压力区定制/运营商流量定制/IPv6流量定制。稳定安全。阿里巴巴自研引擎,多年双十一场景打磨,支持VPC网络压力测试。性能压力测试一站式解决方案。**0编码构建复杂压测场景,涵盖压测场景构建、压测模型设置、发起压力、分析定位问题、生成压测报告的完整压测生命周期。100%兼容开源JMeter。提供安全无侵入的生产环境写压测方案。压测工具对比如何选择压测工具这个世界上没有最好的工具,只有最适合的工具,工具千千万万,最重要的是选择适合自己的,有各种各样的场景在实际使用中,读者可以结合压测步骤来确定适合自己的工具:确定性能压测目标:性能压测目标可能来源于项目计划、业务需求等确定性能压测环境:为了最大限度发挥性能压测的作用,压测环境尽量与线上环境一致确定性能压测通过标准:根据性能压测目标和选择的性能压测测试环境,制定性能压力测试通过标准,针对性能压力测试环境与线上环境不同,通过标准还应适度放宽设计性能压测:安排压测链路,构建性能压测数据,尽可能模拟真实请求链路和请求负载进行性能压测:使用性能压测工具进行性能压测测试分析性能压测结果报告:对性能压测结果报告进行分析解读,判断性能压测是否达到预期目标。很多环节,从场景设计到压力到分析,缺一不可。工欲善其事必先利其器,而合适的性能工具意味着我们可以在最短的时间内完成合理的性能压力测试,达到事半功倍的效果。JAVA应用性能故障排除指南问题分类问题形形色色,形形色色。对其进行抽象和分类是非常必要的。这里我们将从两个维度对性能问题进行分类。第一个维度是资源维度,第二个维度是频率维度。资源维度类的问题:CPU高,内存使用不当,网络过载。频率维度的问题:事务持续慢,事务偶尔慢。每一类问题都有相应的解决方案。方法或工具使用不当会导致无法快速准确地排查和定位问题。定位和调优压测性能问题是一项需要多方面综合能力结合的技术性工作。需要个人的技术能力、经验,有时还需要一些直觉和灵感,还需要一定的沟通能力,因为有时候问题并没有被定位问题的人发现,所以需要通过不断的沟通来寻找一些端倪。所涉及的技术知识远不止于编程语言本身,可能还需要扎实的技术基础,比如操作系统原理、网络、编译原理、JVM等知识。不是简单的了解,而是真正的掌握,比如TCP/IP就必须深入掌握。JVM必须对内存组成、内存模型、GC的一些算法有深刻的理解。这也是为什么一些初中级技术人员在遇到性能问题时会傻眼,不知从何下手。如果你的技术基础扎实,再加上一些实战经验,形成自己的打法,就能在遇到问题时保持冷静,迅速拨开迷雾,最终找到问题的症结所在。本文作者还带来了一些在实际工作中发现和调查的性能问题的典型案例。每个案例都会介绍问题的相关背景、一线人员提供的问题现象和初步调查定位结论,以及经过笔者介入后看到的问题现象,结合一些常用的问题定位工具,介绍了发现和定位问题的全过程,问题的根源等。在分析思维的框架下遇到性能问题,首先要从各种表象和一些简单的工具上对问题进行定义和分类,然后做进一步的定位分析。可以参考图1笔者总结的一张决策图,这张图是笔者最近在金融行业的几个ToB项目中对性能定位和调优过程的总结。可能不适合所有问题,但至少涵盖了近期项目中遇到的性能问题的排查流程。在接下来的大篇幅中,每一类问题都会展开,并附上一些真实的经典案例。这些案例真实且具有代表性,其中有不少是客户定位了很长时间没有找到问题的根源。其中,GC类型的问题本文不再过多分析。对于GC类型的问题,有时间专门写一篇文章展开。内存溢出内存溢出问题根据问题出现的频率又可以分为堆内存溢出、栈内存溢出、Metaspace内存溢出、native内存溢出。下面对每种溢出情况进行详细分析。堆内存溢出相信大家或多或少都接触过这种问题。问题的根本原因是应用程序申请的堆内存超过了Xmx参数设置的值,导致JVM基本处于无法使用的状态。如图2所示,示例代码模拟了一次堆内存溢出。堆大小在运行时设置为1MB。运行后的结果如图3所示,抛出OutOfMemoryError错误异常,对应的Message是Java堆空间,代表溢出的部分是堆内存。栈内存溢出问题主要是方法调用太深,递归方法调用不正确,或者Xss参数设置不当造成的。如图4所示,一个简单的无限递归调用会导致栈内存溢出,错误结果如图5所示,会抛出StackOverflowError错误异常。Xss参数可以设置每个线程栈内存的最大大小。JDK8的默认大小是1MB。一般情况下,一般不需要修改该参数。如果遇到StackOverflowError错误,需要引起注意。需要检查是程序问题还是参数设置的问题,如果方法调用的深度真的很深,默认的1MB不够用,那就需要加大Xss参数。Native内存溢出当JVM使用堆外内存超过进程支持的最大内存限制,或者堆外内存超过MaxDirectMemorySize参数指定的值时,就会发生Native内存溢出。如图6所示,需要配置MaxDirectMemorySize参数。如果不配置这个参数,估计很难模拟这个问题。笔者的机器是64位机器,堆外内存的大小可想而知。程序运行结果如图7所示,抛出的异常也是OutOfMemoryError,和堆内存异常类似,但是Message是Directbuffermemory,区别于堆内存溢出的Message。请特别关注此Message,这对于问题的精准定位非常重要。Metaspace内存溢出Metaspace只出现在JDK8中。在以前的版本中,它被称为Perm空间,其用途可能没有太大区别。模拟元空间溢出的方法很简单。如图8所示,类通过cglib动态创建并加载到JVM中。这些类的信息存储在元空间内存中。这里为了快速模拟问题,将MaxMetaspaceSize设置为10MB。执行结果如图9所示,仍然抛出了OutOfMemoryError的错误异常,但是Message变成了Metaspace。这些是四种最常见的JVM内存溢出。如果知道每次内存溢出的原因,就可以快速准确地定位。以下是遇到的一些真实经典案例的分析。案例:堆外内存溢出也更容易调查。前提是当堆内存溢出时,必须自动将堆内存转储到一个文件中。如果堆内存转储是在压力测试过程中被kill-3或jmap命令触发的。然后使用一些堆内存分析工具,比如IBM的HeapAnalyzer,找出哪个对象占用内存最多,最终找出问题的原因。如果需要在OOM时自动转储堆内存,需要在启动参数中加入如下参数:-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/usr/local/oom如果需要手动获取线程转储或者内存转储,然后使用kill-3命令,或者使用jstack和jmap命令。jstack-lpid>stackinfo,该命令可以将线程信息转储到一个文本文件中,下载到本地使用IBMCorefileanalyze等工具进行分析。jmap-dump:format=b,file=./jmap.hprofpid,该命令可以将堆内存信息放入当前目录下的jmap.hprof文件中,下载到本地,然后使用IBM等堆内存分析工具HeapAnalyze经过分析,根据第28定律,找到最耗内存的对象可以解决80%的问题。图10是一个真实的案例。该问题的现象如下。压测开始后,前十分钟一切正常,但大约十分钟后,TPS逐渐下降,直到无法建立客户端的TCP连接。有一次,客户端认为是服务器端Linux网络栈的参数设置有问题,导致TCP无法建立连接。给出的证据是服务器端存在大量TIME_WAIT状态的连接,然后要求调整Linux内核网络参数,减少TIME_WAIT状态的连接数。什么是TIME_WAIT?这时候,我们就不得不祭出祖传的TCP状态机图,如图11所示。通过这张图对比,就可以知道TIME_WAIT的来龙去脉了。TIME_WAIT主要发生在一方主动关闭连接的时候。当然,如果双方刚好同时关闭了连接,那么双方都会出现TIME_WAIT状态。在执行关闭连接的四次握手协议时,最后的ACK由主动关闭端发送。如果这个最终的ACK丢失,服务器将重新发送最终的FIN,因此客户端必须维护状态信息以允许它重新发送最终的ACK。如果不维护此状态信息,客户端将响应一个RST小节,服务器会将此小节解释为错误(在java中将抛出连接重置的SocketException)。因此,为了实现TCP全双工连接的正常终止,需要处理终止序列四段中任意一段的丢失,主动关闭的客户端必须维护状态信息并进入TIME_WAIT状态。图10真实堆内存溢出案例1图11TCP状态机根据客户提供的信息,我查看了压测客户端。使用HTTP协议,开启keep-alive,使用连接池。服务器端交互。理论上服务器端不应该有那么多TIME_WAIT连接。猜测一种可能是客户端开始压测的时候TPS比较高,占用的连接数比较多。后续性能下来后,连接数空闲,来不及和服务器进行keep-alive处理,导致连接被服务器主动关闭,但这只是猜测。为了更准确地定位问题,我决定到一线现场查看情况。当TPS严重下降时,我用top、vmstat等命令进行了初步检测,发现CPU占比不是很高,大概在70%左右。但是JVM占用的内存几乎接近Xmx参数配置的值,然后使用jstat-gcutil-h10pid5s100命令查看GC情况。是异常的GC数据。先是老年代占比接近100%,随后5秒内进行了7次FullGC,eden区占比100%。因为oldgeneration满了,所以younggeneration的GC也满了。停滞不前,这显然是不正常的。趁着JVM还活着,快速执行jmap-dump:format=b,file=./jmap.hprofpid,对整个heap文件进行快照,一共5G,拿下来后分析heap通过IBM的HeapAnalyzer工具查看文件,结果如图10所示。经过一番查找,发现是某个对象占了特别大的比例,占了98%。继续跟踪持有的对象,最终定位到问题所在。我申请了某个资源,但是一直没有发布。修改后完美解决问题。经过8小时的耐久测试,再次没有发现问题。TPS一直很稳定。图12GC情况统计分析我们看一下为什么会有这么多TIME_WAIT连接,这和一开始的猜测是一致的,因为有大量空闲连接被服务器主动关闭,所以才会有这么多TIME_WAIT连接。高CPU案例某金融银行客户在压测过程中发现问题,导致TPS极低,交易响应时间甚至接近惊人的30S,严重不达标。服务响应时间如图23所示,这是应用日志的tracer,显示的耗时很悲观。应用使用SOFA构建,部署在专用云容器上,容器规格为4C8G,使用OceanBase数据库。在缓慢的交易过程中,客户使用top和vmstat命令获取对应容器中的OS信息,发现内存占用正常,但CPU接近100%。通过jstack命令获取threaddump文件,如图22所示,客户发现大量线程Stuck在获取数据库连接上,大量获取DB连接失败的错误日志应用日志中报错,导致客户认为连接池的连接数不够,于是继续增加MaxActive参数,DB连接池使用Druid。增加参数后,性能没有任何提升,仍然无法获取连接的问题。在排查问题两周左右没有实质性进展后,客户开始向阿里GTS的同学寻求帮助。笔者刚好在客户现场,参与了性能问题的定位。与客户沟通后,查看了历史位置信息记录,根据以往的经验,这个问题肯定不是连接池最大连接数不足导致的,因为此时客户已经调整了MaxActive参数。已经达到了恐怖的500,但是问题依旧。在图22中还是可以看到一些有用的信息,比如waiting线程数高达908,Runnable线程数高达295,都是吓人的数字。大量线程处于Runnable状态,CPU忙着切换线程上下文,CPU呼啸而过,实际上并没有做多少有意义的事情。经查询,客户将SOFA业务处理线程数调整为1000,默认为200。图22线程卡在获取DB连接池中的连接图23事务慢的截图基本可以得出结论:客户陷入了“头痛医头,医脚医脚”和“治标不治本”的困境。与客户进一步沟通后,果然。一开始是因为SOFA报错线程池满了,后来客户不断增加SOFA业务线程池的最大线程数,最后加到1000,性能提升不明显,然后报错无法获取数据库连接,客户认为是数据库连接不够用,通过调大Druid的MaxActive参数,怎么调都提升不了性能。业务DO对象被填满,客户一度以为内存泄漏。对于这种问题,只要是数据库连接池不够用,或者从连接池连接超时,或者线程池耗尽,只要参数设置在合理范围内,那么九出of10只是事务处理本身太慢了。后来经过进一步调查,最终确定是某条SQL语句和一些内部处理不当导致事务变慢。修正后TPS正常。最后将线程池的maximumsize参数和DB连接池的参数回调到最佳实践推荐的值。再次压测后,TPS保持在正常水平,问题终于得到解决。图24业务领域内存被对象填满的情况——虽然是CPU高、事务连续慢等典型问题,但实际上如本案例所述,定位和定位时很容易卡住调优是一种治标不治本的困境,很容易被一些表象所迷惑。如何透过云雾看到月亮,笔者的看法是5分靠经验,1分靠灵感和运气,4分靠不断的分析。没有经验怎么办?那只能静下心来分析相关的性能文件了,不管是threaddump文件还是JFR,或者其他采集性能信息的采集工具,反正一点线索都不放过,最后求助有经验的高手如果没有问题排除故障并解决。使用JMC+JFR定位问题。如果偶尔出现超长的问题,这里有一个比较简单也很实用的方法。使用JMC+JFR,可以参考链接使用。但是使用前必须开启JMX和JFR特性,启动时需要修改启动参数。具体参数如下。不要将此参数投入生产。另外,如果容器所属宿主机的端口也暴露在和jmxremote.port相同的端口,下面的例子如果是32433,也可以使用JConsole或者JVisualvm工具观察虚拟机的状态是实时的,这里就不详细介绍了。-Dcom.sun.management.jmxremote.port=32433-Dcom.sun.management.jmxremote.ssl=false-Dcom.sun.management.jmxremote.authenticate=false-XX:+UnlockCommercialFeatures-XX:+FlightRecorder取JFR实例例如。首先,启用JMX和JFR功能,需要在启动参数中添加JMX启用参数和JFR启用参数,如上所述,然后在容器中执行如下命令,执行后会显示“Startedrecordingpid.resultwillbewritetoxxxx”,表示已经开始录制,此时开始压测。下面命令中的duration为90秒,表示90秒后停止录制。录制完成后,将文件下载到本地,使用jmc工具进行分析,如果没有这个工具,也可以使用IDEA进行分析,jcmdpidJFR.startname=testduratinotallow=90sfilename=output。jfr分析火焰图,具体如何查看火焰图可以参考链接,通过这张图我们可以看出主要耗时在哪个方法上,为我们分析问题提供了极大的方便。您还可以检查调用树,一个d你可以看到耗时主要发生在什么地方。JMC工具下载地址:JDKMissionControl(JMC)8Downloads(oracle.com)最后再介绍一个工具,阿里巴巴开源的arthas,也是性能分析定位的利器。具体使用这里就不介绍了。可以参考arthas官网。如何定位消耗CPU时间过多的线程和方法首先找到JAVA进程的PID,然后执行top-H-ppid,这样就可以找到最耗时的线程,如下图。然后用printf"%x\n"17880把线程号转成16进制,最后用这个16进制值去jstack线程转储文件中找哪个线程占用CPU最高。其他问题案例出现此类问题时,JVM表现如止水,CPU和内存占用正常,但事务缓慢。这类问题可以参考CPU高的问题来解决。使用线程转储文件或使用JFR记录JVM运行记录。出现这类问题的原因是大部分线程卡在了某个IO或者被某个block锁住了。下面也带来一个真实的案例。案例一某金融保险头部客户对一笔交易的响应速度非常慢,往往响应时间在10秒以上。应用部署在公有云容器上,容器规格为2C4G,数据库为OceanBase。每次都可以重现问题。分布式链接工具只能定位某个服务的慢,不能准确判断是哪个方法卡住了。在慢事务期间,通过top和vmstat命令查看OS的状态,CPU和内存资源处于正常水位。因此,有必要查看线程在事务处理过程中的状态。在事务执行缓慢的过程中,dump事务的线程,如图29所示,可以定位对应的线程卡在了哪个方法上。在这种情况下,线程卡在了执行socket读取数据的阶段。从堆栈可以判断是Stuckonreadingthedatabase。如果这种方法还是不好用,那么也可以使用抓包的方法来定位。图29交易挂起案例Case2某金融银行客户压力测试时,发现TPS上不去,响应时间甚至低于10TPS。经过一段时间的培训和适应,客户已经具备了一定的绩效定位能力。反馈的信息是SQL执行时间、CPU和内存占用都正常。客户敲了一个threaddump文件,发现大部分线程都卡在使用RedissionLock的分布式锁上,如图30所示,发现客户没有合理使用分布式锁。解决问题后,TPS提升了20倍。图30分布式锁使用不当导致的问题示例这两种情况其实并不复杂,也很容易排查。我只是想重申一下解决此类问题的总体思路和方法。如果事务比较慢,资源使用正常,可以通过分析threaddump文件或者JFR文件来定位问题。这种问题一般是IO瓶颈或者lockedblocks引起的。问题千千万万,但只要练够了深厚的内功,就可以形成自己的一套排错思路和方法,再加上一套支持排错的工具,靠的是自己已有的经验和偶尔出现的问题有了那点点启发,我相信一切问题都会迎刃而解。
