Java应用性能调优套路

压测前准备

我们应对单台应用服务器做压力测试,你只有知道了单台能够承受多少才能知道集群能承受多少。

然后要确定单台应用服务器性能目标:

如果客户要求吞吐量为2000rps,能提供2台服务器,那么每台的吞吐量则为1000rps。

如果客户要求延迟P99 <= 2秒,那么和服务器就没有关系了,你需要优化程序算法。

压测时的观察和调优

下面我们对单台应用服务器开始压力测试。

保证CPU用满

压测期间我们首先要保证的是CPU利用率接近N * 100%(N为CPU核心数),如果CPU利用率不满那么压测报告就没有意义,因为机器并未全力运转。

发现CPU没有用满,那么有这么几种可能

保证CPU花在非GC上

好了现在CPU用满了,那么我们要通过jstat -gcutil来观察JVM是否把CPU花在了GC上,你也可以添加-XX:+PrintGC -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/path/to/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=1 -XX:GCLogFileSize=20M JVM参数得到更详细的GC日志。

重点关注Full GC的次数和占用时间,如果发现Full GC很频繁,有三个解决思路:

解决阻塞

用jstack导出thread dump来分析。

线程阻塞是常见的CPU利用率不满的元凶,因为阻塞时不占用CPU,你可以在压测期间用jstack收集几次堆栈数据,然后观察里面的BLOCKED、WAITING、TIMED_WAITING状态的线程的堆栈,找到线索。

如果发现阻塞不可避免,那么可以通过增加线程数的方式来利用CPU。

如果阻塞发生在连接池相关,那么调整连接池大小。如果阻塞发生在执行SQL有关,那么优化SQL语句。如果阻塞发生在其他地方,那么做针对性优化。

观察慢SQL

慢SQL是常见阻塞原因,找出这些慢SQL,对它进行优化,或者对数据库表做优化,提升程序响应速度,提高CPU利用率。

观察执行次数异常的SQL

执行次数异常的SQL也是很重要的一点,抓住这些SQL,对代码进行优化。

数据库连接池

连接池不够也是常见的阻塞元凶,线程在等待连接池导致CPU利用率上不去,不过还是那句话,不要盲目调整,你应该在jstack里看到大量获取jdbc connection的阻塞线程之后才去调整它。

线程池

按照理论上来说,如果线程不阻塞,那么只需要N个线程就能把CPU占满(N是CPU核心数)。线程阻塞占用比例越大,就需要越多线程来占用空闲CPU时间。

如果你优化之后把阻塞比例降低了,那么你也需要相应调小线程池尺寸。

过多的线程池不会带来更多好处,白白占用内存而已。

服务器异常日志

有时候服务器异常日志也会提供给你很好的线索,记得观察。特别是如果异常特别多的话,会直接影响性能的。

观察、优化、验证的循环

当你做了一个优化点后,你再压一遍,看看是否有改善,同时需要调整其他相关参数,比如前面提到的调小线程池。

同时,有些时候做了一个优化点之后,会发现新的问题,这个问题可能在之前被那个占大部分因素的性能瓶颈遮蔽掉了,现在大问题解决了,那这个小问题就显现出来了。此时,你需要针对这个新问题再收集报告,然后再优化。举个例子,原来是SQL慢,优化好之后会发现程序算法也有问题。

一些工具

GC分析

https://gceasy.io 是一个在线分析GC日志的工具。把得到的gc.log日志。

heap dump分析

利用下面命令得到heap dump,然后放到MAT中分析

jmap -dump:live,format=b,file=heap.bin <pid>

有些时候你需要把垃圾一起dump下来,比如GC很频繁,那么去掉live参数:

jmap -dump:format=b,file=heap.bin <pid>

thread dump分析

利用jstack得到thread dump,然后放到 https://fastthread.io/ 分析