前言:

今天测试部门的小梦找到我,委屈巴巴的说我写的接口有问题,因为她对这个接口进行压力测试时,发现系统的吞吐量一直上不去,并且 应用服务器 (部署接口项目的服务器) 的CPU、内存等资源的使用率也一直很低,导致一直无法测试出这个接口的压力峰值。

 

听小梦说完后,自己心想接口都测试了好几遍了, 接口代码 绝对不可能有问题的,再说了,有问题也不能承认呀,看来得往别的地方上扯扯呀;然后我说道,接口应该是没问题的,可能是项目环境部署时有些参数没进行调优吧,例如:连接数大小设置、JVM参数设置、数据库参数优化等;

 

然后我接着说道,项目是谁给你部署配置的呀,小梦说是小王部署的,然后今天小王也没来上班;我说道,小王没来上班呀,没事,让我来,我这bug绝缘体质,任何问题遇到我都会退避三舍的。

 

上面的情景,我相信大家都可能会遇到过的;接下来我们就通过这次排查压测问题来聊聊一个单体系统 的性能优化应该考虑哪些点,以及对这些点该怎么进行调优 。

本文主线如下图:

 

项目部署环境:

在进行这次压测问题排查前,先通过下图了解下 接口项目的情况、项目部署的环境、JMeter测试脚本 的配置情况;

 

上图中的接口项目、Redis、MySql 都是单机安装部署的 。

注意:在进行下文之前,我们还需要记住一个前提:本项目中的接口代码是不存在问题的,并且数据库查询SQL都是最优的,都是走索引查询的,本次压力测试问题不是由于代码导致的,而是由于各种参数未调优造成的;代码调优和SQL调优在编码阶段就已经完成了。

简单性能调优的点:

在上文中的项目部署情况和测试脚本的配置情况下进行压力测试时,就出现了小梦说的系统吞吐量上不去,以及应用服务器(部署接口项目的服务器)的 CPU 等资源使用率都很低的情况;

查看 JMeter 测试时系统的吞吐量如下图:

 

查看应用服务器(部署接口项目的服务器)的CPU使用率很低:

 

按照测试脚本的50个并发以及接口中含有计算密集型操作(加解密和验签)的情况,CPU是不应该这么低的;哎!什么鬼嘛?

 

别急,下面我们就展开具体的排查过程,并在排查过程中逐步了解各个调优的点;

排查过程就是按照文章开头中的 本文主线 图片中 简单性能调优的点 展开的 。

排查Tomcat连接器参数配置:

Tomcat 连接器参数配置只需要考虑以下几个方面:

  • Connector使用哪种 protocol 协议HTTP/1.1:默认值,使用的协议与Tomcat版本有关建议使用 NIO2 :org.apache.coyote.http11.Http11Nio2Protocol除了NIO2协议外,还有 BIO、NIO、APR 协议,可以自行去查阅资料;
  • acceptCount:等待队列的长度;当等待队列中连接的个数达到acceptCount时,说明队列已满,再进来的请求一律被拒绝;默认值是100。
  • maxConnections:Tomcat 在任意时刻接收和处理的最大连接数;当连接数达到最大值maxConnections后,Tomcat会继续接收连接,直到accept等待队列填满。如果最大连接数设置为-1,则表示禁用maxconnections功能,表示不限制tomcat容器的连接数;最大连接数默认值与连接器使用的 protocol 协议有关:NIO的默认值是10000;APR/native的默认值是8192;BIO的默认值为maxThreads(如果配置了Executor,则默认值是Executor的maxThreads);
  • maxThreads:每一次HTTP请求到达 Tomcat,都会创建一个线程来处理该请求,那么最大线程数决定了Tomcat 可以同时处理多少个请求。maxThreads 默认200;建议:maxThreads应该设置大些,以便能够充分利用CPU;当然,也不是越大越好,如果maxThreads过大,那么CPU会花费大量的时间用于线程的上下文切换,整体效率可能会降低;这需要根据自己项目的情况和服务器硬件配置情况配置合适的值即可;

下面就是本次压测时配置的Tomcat连接器参数:

<Connector
         port="8080" 
         protocol="org.apache.coyote.http11.Http11Nio2Protocol"
         maxThreads="1000" 
         maxConnections="2000"
         acceptCount="1000"
         connectionTimeout="20000" 
         redirectPort="8443" 
         enableLookups="false" 
         maxPostSize="10485760" 
         compression="on" 
         disableUploadTimeout="true" 
         compressionMinSize="2048" 
         acceptorThreadCount="2" 
         compressableMimeType="text/html,text/xml,text/css,text/javascript" 
         URIEncoding="utf-8" />

上面连接器中配置参数:的

  • 连接器使用的 protocol 协议是 NIO2
  • 最大线程数 maxThreads="1000"
  • 最大连接数 maxConnections="2000"
  • 等待队列大小 acceptCount="1000"

根据上面连接器配置参数,和压测脚本配置的并发请求才50个,确定压测问题应该不是由于连接器参数造成的。

其实在查看Tomcat配置文件 server.xml 中的连接器参数前,可以使用下面这个命令先直接统计出 JMeter与Tomcat建立的Http连接数: 注意 8080 指的是 Tomcat 端口号,使用前改成你的Tomcat配置的端口

netstat -pan | grep 8080 | wc -l

如果统计出的连接数与测试脚本中配置的并发数的话差不多一致的话,说明当前连接器是可以满足连接请求处理的,初步可以判断不是由于连接器参数造成的本次压测问题;然后再可以去查看配置文件中的参数进行确认下。

 

排查 JVM 参数配置:

注意:JVM 参数配置比较复杂,这块的参数调优需要根据具体项目情况、服务器硬件配置等进行合理配置。

查看当前压测的接口项目 JVM 配置参数:

  • 直接使用命令查看:ps -ef | grep tomcat结果如下:
  • 也可以直接去 catalina.sh 配置文件中查看:JAVA_OPTS=" -server -Xms6000m -Xmx6000m "
  • 也可以直接通过 jmap 命令查看 JVM 的堆的配置信息:注意:下面命令中的129761 是tomcat的 pid 进程号;jmap -heap 129761结果如下:

看到这,应该会有人问,为什么你的初始堆大小和最大堆大小(-Xms6000m -Xmx6000m)设置为一样呢?

因为如果虚拟机启动时设置的初始堆内存比较小,这个时候又需要初始化很多对象数据,那么虚拟机就必须不断地扩大堆内存,这样也会产生一些消耗的。

排查下是否由于JVM参数配置的不合理导致:

  • 使用上面提到的 jmap 命令查看下当前堆内存的使用情况jmap -heap 129761

通过查看上面的堆内存的使用情况,发现还有很多空间使用呢,应该不是由于JVM参数不合理导致的压测问题;

但是咱别着急,咱再查看下JVM中垃圾回收GC的情况;

  • 使用命令 jstat 查看垃圾回收GC的情况:注意:129761 为Tocamt的pid; 1000 表示间隔时间毫秒,10 表示输出10次GC情况jstat -gc 129761 1000 10

看了上面的GC情况,发现垃圾回收也没什么异常,FGC总共就3次,通过查看的堆使用情况和GC垃圾回收的情况可以确认压测的问题不是由于JVM的参数导致的。

看来这次压测的问题挺棘手呀,到现在还没有确认出问题原因呢!难道我这bug绝缘体质由于今天吃的比较多 ,导致失效了吗? 皱眉 ing . . . . .

 

看来还得接着排查,咱直接导出当前接口服务应用进程的 线程堆栈信息 看看吧。

  • 使用命令jstack导出线程栈信息:注意:129761 为Tomcat 的pid, /usr/local/test.log 未生成的线程堆栈文件 test.log 的存放地址jstack 129761 >>/usr/local/test.log
  • 导出线程堆栈信息后,使用命令搜索出状态为 WAITING 的线程;注意:找 State 为WAITING 状态的线程;命令如下:test.log 文件为刚刚导出的 线程堆栈信息文件cat test.log | grep -n20 " WAITING "在命令执行后的搜索结果中发现了下图内容:

除了线程状态为 WAITING 的之外,还特别要注意 WAITING 状态线程的方法调用栈中存在项目业务代码的;

  • 上面图片中就展示了线程状态为 WAITING 等待的调用方法栈中有业务代码;
  • 业务代码是去Redis中查询数据,上图中第二个圈中的内容就是业务代码,并且这行代码上方是去Redis连接池中获取连接,由此确定是由于Redis连接获取不到,导致线程处于 WAITING 等待状态;
  • 由于接口服务系统中很多线程获取不到Redis连接导致阻塞,从而无法充分利用CPU;所以在压测时,服务器CPU的使用率也压不上去,CPU的使用率很低;

至此,终于排查出了一处问题地方,不容易呀,感觉排查问题比写代码困难多了!

 

上面通过线程的堆栈信息排查出 Redis 连接不够用,应该是 Redis 连接池中的最大连接数配置的比较小,那接下来就确定下连接池的配置数据。

排查连接池参数配置:

上文中,在线程的堆栈信息中排查出存在很多线程由于获取不到Redis连接导致处于等待阻塞状态;

接下来,咱们就来具体统计下现在处于压测中的应用服务器(部署接口项目的服务器)与Redis建立的连接数,顺带一起再排查下项目中使用的MySql 的连接池配置是否也存在问题?

  • 使用命令查看当前压测中的应用服务器与Redis建立的连接数注意:6379 为 redis的端口号netstat -pan | grep 6379 | wc -l执行命令查询出与Redis建立的连接数为 10,这在测试脚本50个并发请求下是一定不够用的呀,应该是配置的Redis连接池的最大连接数比较小;然后通过看接口代码中的配置文件发现,确实Redis连接池中的最大连接数配置是 10 个;最后修改了最大连接数为 300 个;注意:这个最大连接数需要根据项目情况和测试计划来设置,不用设置的太大。
  • 接着,使用命令顺带统计下当前处于压测中的应用服务器(部署接口项目的服务器)与MySql 建立的连接数注意:3306 为MySql 的端口号netstat -pan | grep 3306 | wc -l执行命令发现已经建立的MySql 连接数是 50,然后通过查看配置文件的连接池中最大的连接数为100,发现还没有达到最大连接数,说明 JDBC 连接够用,本次压测问题不是由于JDBC连接不够用导致的;并且在上文中查看线程的堆栈信息时,也没有发现调用 JDBC 操作的线程处于 WAITING 等待状态。

排查完了数据库连接池了,咱们就再排查下MySql 数据库是否进行了参数调优;

如果MySql 数据库没有进行调优的话,那在压测时就会出现数据库的读写性能比较差,也就是项目中在进行 JDBC 增删改查操作时会比较慢,进而导致存在JDBC操作的线程执行慢,无法充分利用CPU的并发计算能力,所以也会导致应用服务器的CPU使用率比较低,最终压测时整个接口应用系统的吞吐量也很低 。

 

排查MySql 数据库调优配置:

排查前先聊一个由于之前MySql 数据库未进行优化,而导致的压测问题。

MySql 数据库未调优,引发的压测问题 简述:

之前压测时的场景还原:

在压测前所有该调优的地方都调了,就单独忘记对MySql 数据库进行调优了,最终导致对应用压测时,发现应用服务器(部署接口项目的服务器)的CPU的使用率一直浮动在60%左右,始终压不上去,即使加大压测时的并发请求数,发现还是不行;

最终通过查看MySql 服务器的磁盘IO的情况和MySql 数据库进程的CPU、内存等使用率发现MySql 没有被优化,所以导致MySql 数据库的读写能力被限制,进而导致应用并发处理能力下降,从而导致应用服务器的CPU使用率一直升不上去。

  • 使用命令统计下压测时 MySql 数据库所部署在的服务器的磁盘IO性能:注意: 1 指的是 间隔时间1秒, 5 指的是统计5次 IO 的情况;iostat -xdk 1 5

结果如下:

 

只需要注意图中圈起来的地方;await 指的是 磁盘读写时的等待时间,这个值越小越好;%util 指的是磁盘整体的负载情况,值越大说明磁盘负载快达到极限了。

  • 使用命令查看MySql 进程的CPU、内存的使用率:注意: 14208 是MySql 的pid;top -p 14208结果如图:

注意:

当时就是通过这两条命令统计出的数据,发现数据库服务器磁盘IO的负载一直处于90%,快达到极限了;但是MySql 进程的CPU、内存的使用率却很低;

通过这两条数据可以初步确定MySql 没有进行优化;如果MySql 进行了调优,那么MySql 的进程在压测时的CPU、内存等使用率不可能这么低,并且磁盘 IO 的负载也不会达到了极限;

得出初步结论后,然后去查看MySql 数据库的配置文件 my.cnf, 发现确实没有进行一些参数的优化,只是简单配置了最大连接数为 500;注意,MySql 数据库默认的最大连接数是 100 个;

最后在 my.cnf 配置文件中简单配置了下 innodb 存储引擎缓冲池大小 等;这里就不具体描述调优的参数了,因为调优参数都是需要根据项目情况、服务器硬件配置等综合考虑配置的;大家可以自行网上查阅MySql 数据库调优的相关资料;

配置好后,重启了MySql 数据库,然后重新进行压测,发现应用服务器(部署接口项目的服务器)的CPU使用率上去了,MySql 数据库进程的CPU、内存等使用率也升上去了,并且磁盘IO的负载也降下来了,最后整个应用系统的吞吐量也上去了,也支持更大的并发数了。

说完了上面之前遇到的一个压测小场景,咱们接着按照上面的排查步骤排查这次压测的问题,发现MySql 数据库已经进行了调优,磁盘IO的读写速度也是OK的,所以说本次压测问题与MySql 数据库参数配置无关。

排查完了MySql 数据库后,咱接着排查下Linux服务器的参数是否进行过调优吧!

 

排查Linux服务器参数配置:

服务器参数调优这块目前了解的不多,只知道配置下服务器的 文件句柄数 fd ;建议将服务器的文件句柄数设置的大些;为什么设置大些呢?

因为主流操作系统的设计是将 TCP/UDP 连接采用与文件一样的方式去管理, 即一个连接对应于一个文件描述符(文件句柄) ;

主流的 Linux 服务器默认所支持最大 文件句柄数 数量为 1024,当并发连接数很大时很容易因为 文件句柄数 不足而出现 “open too many files” 错误,导致新的连接无法建立。

建议将 Linux 服务器所支持的最大句柄数调高数倍(具体与服务器的内存数量相关)。

  • 使用命令查看当前服务器的文件句柄数:ulimit -a结果如图:

上图中的文件句柄数是已经修改过的;设置文件句柄数可以设置临时值,也可以设置永久值,建议设置成永久值,因为临时值遇到一些情况会失效的。

配置文件句柄数可以自行去网上查询,资料很多的,本文就不做过多描述了。

聊完服务器参数调优配置后,咱最后再聊聊 JMeter 测试工具在压测时有没有该注意的点!

 

JMeter 分布式测试:

这里主要聊聊 JMeter 分布式测试的内容;什么时候需要进行分布式测试,当你的单机 JMeter 服务器由于硬件配置限制无法构建出足够的并发压力时,这时就需要进行分布式部署测试了;

怎么判断 单机的 JMeter 服务器 已经无法构建出足够的并发压力了呢?

  • 通过查看 JMeter 测试服务器的CPU的使用率情况,当 JMeter 测试服务器的CPU使用率达到 90% 多时,此时就可以初步确认CPU无法再提供足够的计算能力来构建并发压力了;
  • 所以此时可以将单机的JMeter改为分布式JMeter进行测试,只有构建出足够的压力,才能将应用服务器(部署接口项目的服务器)的CPU、内存等资源的使用率压上去,从而测试出接口的压力峰值。

所以,当单机 JMeter 压测时发现应用服务器(部署接口项目的服务器)CPU、内存等使用率压不上去的话,也可以去排查下是否是由于JMeter服务器硬件配置不足造成的;

本次压力问题,发现 JMeter 测试服务器在压测时 CPU 的使用率达到了 80%多,说明此台测试服务器由于硬件限制也是无法构建更大的并发压力了,所以建议将单机 JMeter 测试改为 分布式测试;

这里 JMeter 分布式测试网上教程很多,本文就不做过多描述了。

到这里,本人认为的所有该调优的点都排查完; 今天老百姓真呀真呀真高兴!

 

本次排查总结:

排查完上面的几个点后,最终发现了导致本次压测问题的原因:

  • Redis 连接池的最大连接数配置的偏小
  • JMeter 单机测试无法构造出足够的并发压力

正是由于上面的两个问题导致了本次压测出现了下面的问题:

  • 压测时应用系统的吞吐量偏低
  • 并且应用服务器(部署接口项目的服务器)的CPU、内存等使用率偏低;

在此,再说明下本次压测项目就是一个简单的单体项目,项目中没有涉及到分库分表、各种中间件以及分布式等,所以排查起来还相对简单些 ,否则问题排查会更加困难的。

由于本人水平有限,如果有未提及到的调优的点,请评论留言呀!

扩展

在排查问题时,如果有一些 排查工具 的话,将极大的方便我们排查问题,从而使我们快速的定位到问题原因!

下面就介绍两个在排查问题非常好用的辅助工具:

  • Arthas : 阿里开源的线上Java应用诊断工具
  • jvisualvm :JDK中自带的 JVM 运行状态监控工具

原文链接:https://segmentfault.com/a/1190000038467733

如果觉得本文对你有帮助,可以关注一下我公众号,回复关键字【面试】即可得到一份Java核心知识点整理与一份面试大礼包!另有更多技术干货文章以及相关资料共享,大家一起学习进步!