连续参加了两年公司的双十一大促压测项目,遇到了很多问题,也成长了很多,于是在这里对大促压测做一份总结。以及记录一下大促压测过程中出现的一些常见的Java应用性能问题。
吞吐率 TPS(每秒响应的请求数量)
响应时长 RT (一般情况下重点关注90%请求的响应时长,我们的大促标准一般是1s以内)
错误率 (看业务的可接受程度,我们的大促标准是不超过2%)
现在有很多可以用来进行压测的工具,例如ab、jmeter、wrk等,此处主要介绍一下ab和jmeter。
ab是一个命令行工具,使用起来非常简单,
# -c表示并发数,-n表示请求总数,其他一些参数可以查询手册/相关资料 ab -c 10 -n 200 https://www.baidu.com/ 复制代码
命令执行完成后会得出一份压测结果报告,其中错误请求数、TPS和RT在下图中有标注
jmeter同时支持图形化界面和命令行方式,
首先在"Test Plan"中添加一个"线程组",里面可以设置并发数、压测时长等参数。
接下来需要在“线程组”中添加“HTTP请求取样器”,里面是设置HTTP请求的各项参数。
最后添加查看结果用的监听组件,我个人比较常用的有“查看结果树”、“聚合报告”和“TPS曲线图”(需要安装)。
重点来看一下“聚合报告”,(一定要记得每次压测前都要清理一下数据才行[上方的"齿轮+2个扫把"图标],不然回合之前的数据混合在一起)
上面只是一些简单的介绍,事实上jmeter还支持很多复杂的压测场景: jdbc压测、dubbo压测、动态参数压测、自定义响应断言……这些可以自行网上搜索。
命令行方式主要可以用来做一些自动化压测的任务。使用方式如下:
jmeter -n -t [jmx脚本] -l [压测请求结果文件] -e -o [压测报告文件夹(会生成一堆网页文件)] 复制代码
其中jmx脚本可以先通过jmeter图形化界面全部设置好了,然后保存一下就会生成对应的jmx脚本了。
###3. ab与jmeter的对比
ab | jmeter | |
---|---|---|
操作难度 | 简单 | 复杂 |
命令行 | 支持,操作简单 | 支持,操作稍微复杂一些 |
请求结果列表 | 无法显示 | 有详细请求列表 |
动态参数 | 不支持 | 支持 |
复杂场景支持 | 极其有限 | 丰富 |
基本上,对于一些简单的固定参数请求并且是自测的情况下,使用ab会非常简便。一般情况下jmeter的适用性会更广。
一般情况下,不必要将公司所有的接口都进行压测,压测接口主要包含核心链路接口、访问量大的接口以及新上线的活动接口。获取方式基本是如下两种:
在上面的两种压测工具中,我们都看到了一个参数为并发数,这个参数一般需要根据公司的业务量来进行推算,可以去网上找些资料。不过为了简化压测过程,我们公司的大促统一使用读接口200并发,写接口100并发的标准来执行的。
事实上,我对并发数的设定这块也比较模糊,因此上述描述仅做参考。
一般是根据大促销售目标、平时各接口qps、各接口访问量按照比例制定出最终的TPS目标,不要忘了最后乘上一个风险系数。具体的算法可以自行设计,大概思路就是这样的。
对于压测工具的指标上面已经说过了,主要是关注TPS、RT和错误率。
那么还有哪些需要关注的指标呢?其实这个都是根据公司的业务来决定的,例如我们公司主要使用java应用、mysql作为数据库、redis作为缓存中间件,那么我们主要关注的性能数据如下:
监控对象 | 性能指标 |
---|---|
应用服务器(服务化应用包含下游链路的应用服务器) | CPU、网络带宽、磁盘IO、GC |
数据库 | CPU、网络带宽、慢SQL |
REDIS | 网络带宽 |
每个监控对象都有其特性,所以应该根据实际情况的来制定自己的监控指标。
由于公司主要使用的是Java8,因此本文也主要是针对Java8应用做分析。
举个例子,如果告诉你一个接口稍微一些压力就能把服务器的cpu跑满了,导致TPS上不去,里面一堆复杂逻辑,而且还有不少远程调用(数据库查询、缓存查询、dubbo调用等)。
你可能对业务非常熟悉,开始大刀阔斧地进行代码修改、增加缓存、业务降级等,也许期望很美好,但是事实上有极大的可能是你做的一切对TPS只能产生轻微的影响。然后只能通过不停地尝试删改代码去查找问题点,那么显然只能带来几个结果: 1.效率低下 2.把代码弄的一团糟 3.不具备可复制性 4.对业务会造成或小或大的影响,最最关键的是改的时候心里也没底、改完之后心里依旧没底。
##如何排查性能问题
那么问题来了,我们到底应该怎么去排查问题呢?(以下均为一些个人经验,可能会有不少遗漏,或者会有一些错误,如果有的话,请及时指出)
排查问题的话,首先我们需要先有一些排查的突破点和方向。(无法保证100%找到对应问题,但是大幅提升找到性能问题的效率)
前面有提到,我们压测过程中需要监控各项指标,那么其实我们的突破方向一般就在这些监控指标上了。我们可以对这些指标进行分类,对于每一类都可以有着相对应的排查策略。
这个问题是最好排查的一类问题了,只需要对慢SQL进行针对性地分析优化即可,此处不过多讲解。
那么一个问题来了,此处的网络带宽到底是指的什么呢?换个问法吧,假设数据库的带宽上限为1Gbps,实际上压测导致数据库的网络带宽占用了800Mbps,那可以说明这个接口是一个问题接口吗?
考虑下面这种情况,这个接口的TPS假设在压测过程中达到了80000,远大于接口实际目标TPS,那该接口将数据库的带宽占到800Mbps是合情合理的。
那么上面的问题的答案也就呼之欲出了,这里的网络带宽,在很多情况下,我们更应该关注的是单个请求的平均占用带宽。
猜一猜,其实不难想象,就是抓包。我常用的抓包方式是通过tcpdump抓包,然后使用wireshark解析抓包内容(如果有更简单的方式,可以留言)。下面讲一下tcpdump+wireshark的方式如何抓包。
为了避免大量的数据混杂在一起,一般情况下,我更喜欢是抓单个请求的数据,而不是在压测中抓包。下面简单介绍一下tcpdump和wireshark如何抓包,
sudo tcpdump -w xxx.pcap
打开TCP流后通过调整右下方的"Stream",我们就可以看到应用在请求过程中的网络数据(包含Http请求数据、Mysql请求数据、Redis请求数据……),以下图为例,可以看到这个请求的mysql请求量非常大,接下来就是查看到底是哪些SQL语句导致的。
开启数据库日志,看看压测期间都执行了哪些SQL语句,然后进行针对性的分析即可。一般情况下,全表扫描、不加索引、大表的count这些都比较容易引起cpu问题。绝大多数情况下都可以通过技术手段来优化,但也有可能技术手段无法优化的情况,则可以考虑业务上的优化。
绝大多数情况下是由于日志问题导致的,日志问题一般分为如下两种情况:
至于其他的磁盘IO问题,则需要根据实际业务去分析了,暂时未遇到过,此处略过。
一般情况下,我们不太需要去关注YoungGC,更多地只需要关注FullGC就行了,如果只是偶尔出现一次FullGC,那基本上没有太大问题,如果频繁FullGC(几秒就有一次FullGC,甚至可能一秒几次),那就要做相应排查了。
一般可以通过jstat来监测,命令如下:
jstat -gccause [PID] 1000 复制代码
具体的每个参数的含义可以查看 man jstat
手册。
其实用visualvm装个GC插件然后监测java进程,可以很直观地看到java应用的内存和GC情况,就是操作相对而言比较繁琐。
很多情况下(主要是大对象/大量堆对象导致FullGC的情况),都可以通过将Java堆dump下来,然后通过MAT、jhat等内存分析工具来分析。流程如下:
jmap -dump:format=b,file=heap.bin [PID]
此处以一个真实的出现过宕机的Java应用的堆作为举例(加上-XX:+HeapDumpOnOutOfMemoryError这个参数就可以在出现OOM的时候自动将堆dump下来了)
本文简单看一下"Leak Suspects",至于Histogram则可以自行去研究。
这个堆文件其实还是比较简单的,因为可怀疑点只有一个,八九不离十就是这块出现问题了。点击"Details"可以看到更详细的信息(ps:不是每种怀疑对象都有Details的)。
在详细信息里面基本上可以很明显地看出来,有一个SQL语句查出来了超多的数据,导致内存塞不下了。事实上,最终在数据库日志中找到了这条语句,共查询了200W+条数据。
这个例子比较简单,事实上我们可能会遇到更多复杂的情况,例如怀疑对象特别多,甚至真正原因并不在怀疑对象中,或者metaspace导致的FullGC,这些情况下,我们可能又需要采用其他方式去处理这些问题。
还记得之前的有个问题——你认为cpu达到100%是好是坏吗?
那么在这里我揭晓一下答案,如果接口的TPS高,那么我们的服务器的cpu当然是越高越好了,因为这说明了资源被充分利用了。但是,如果接口的TPS低,那么cpu达到100%就说明很有可能是有问题了,很大可能是存在问题代码占据了大量的cpu。
那么还有一个问题就是,你认为哪些代码对cpu的开销大?
这些都没有影响,那到底什么才对cpu有影响呢?常见的业务场景总结如下(如有遗漏请留言补充)
我在大促压测中实践的比较多的方式是perf + perf-map-agent + FlamaGraph工具组合,其中perf是用来监控各个函数的cpu消耗(可以实时监控,也可以记录一段时间的数据),perf-map-agent是用来辅助perf使用的,用来生成java堆的映射文件,FlamaGraph则是用来生成火焰图的。
这套工具的安装使用就不做介绍了,可以参考一下下面这两篇文章,
senlinzhan.github.io/2018/03/18/… www.jianshu.com/p/bea2b6a1e…
主要使用方式,有如下两种:
下面展示一下本次大促压测solr优化过程中生成的火焰图,从图中可以看到YoungGC就占用了将近一半的cpu,
用这个示例代码做个perf-top的使用示范:
import java.text.SimpleDateFormat; import java.util.Date; public class Cpu { private static final int LIMIT = 100000000; public static void main(String[] args) { simple(); } private static void simple() { int count = 0; long startTime = System.currentTimeMillis(); while (count < LIMIT) { Date date = new Date(); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS"); String sd = df.format(date); if (sd.length() == 23) { count++; } } System.out.println(System.currentTimeMillis() - startTime); } } 复制代码
首先使用 perf-map-agent/bin/create-java-perf-map.sh [PID]
生成JVM映射文件,然后使用 sudo perf top
可以看到cpu基本上都被SimpleDateFormat给占用了,(下面还有很多展示不出来的,事实上会更多)
有了这些工具之后,绝大多数问题都已经可以比较容易地找到性能优化点了。
上面那套组合实际用起来十分繁琐,大促压测结束后又了解到了一些其他工具,不过未经过真实实践,所以列出来仅做参考:
当我们在系统的所有环节都无法找到硬件瓶颈的时候,那往往就是线程产生了阻塞,一般情况下线程阻塞可以使用jstack和arthas来排查,分别举个例子吧,用下面这段样例代码:
public class Main { public static void main(String[] args) { for (int i = 0; i < 30; i++) { new Thread(() -> { while (true) { run(); } }, "myThread-" + i); } } private static void run() { Integer x = 1; for (int i = 0; i < 100000; i++) { x *= i; } System.out.println(x); sleep(10); } private static void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } } 复制代码
jstack [PID] 复制代码
从图中可以看到大量的线程都卡在 Block.sleep()
上。一般情况下,jstack可以配合grep来使用,通常关注得比较多的状态更多是BLOCKED。jstack相对而言没那么直观,但是比较轻量级,很多时候也可以比较容易地看出来一些常见的线程阻塞问题。
arthas其实是一个比较全能的jvm性能分析工具,用起来也是各种舒服,而且相对而言也比较轻量,强烈推荐。
此处主要介绍arthas在排查线程阻塞方面的应用,
java -jar arthas-boot.jar trace [包名.类名] [方法名] -n [数量] '#cost>[执行时间]'
如上图,我们可以看到各个方法的执行时间(包含了阻塞时间),筛选出执行时间长的方法,很大可能就能发现造成线程阻塞的瓶颈点。
内存泄漏问题往往都伴随着宕机,我所遇见的情况有如下几种:
这种情况属于相对而言比较容易处理的情况,使用-XX:+HeapDumpOnOutOfMemoryError参数可以在应用宕机的时候自动dump下堆文件,然后使用MAT等内存分析工具在绝大多数情况下都可以找到问题原因。
这个有见过JVM调用groovy在某些情况下会产生内存泄漏。不过没有真实排查过相关问题,此处略过。
防风有一篇文章可以参考一下 GroovyClassLoader 引发的 FullGC
nonheap内存泄漏问题属于非常难排查的问题,一般情况下比较难dump下堆文件,即使dump下来了,一般情况下也很难确定原因,之前有用过tcmalloc、jemalloc等工具进行排查过。暂时没找到什么比较通用的套路,一般也是特事特办。根据之前的排查经验来看,如下几种情况会比较容易出现nonheap内存泄漏(如果遗漏,请留言补充):
排查很多问题之前,最好能够先去了解一下相关业务逻辑,因为很多性能问题是由于大量的问题业务代码引起的,很多时候从业务角度去考虑、辅以技术手段往往能够得到更好的效果。
上面的各种方式只是提供一些策略,无法保证100%能够找到问题,甚至可能连70%都保证不了,更多情况下我们需要灵活使用各种工具进行问题分析。总结一下上面的性能分析工具,可以大概如下分类:
类型 | 工具 |
---|---|
全能型分析工具 | arthas、visualvm |
cpu分析工具 | perf、jvmtop |
内存分析工具 | jmap、jhat、MAT |
网络分析工具 | tcpdump、wireshark |
GC分析工具 | jstat、gc日志文件、visualvm |
堆栈分析工具 | jstack、arthas |
有些工具甚至有更多的功能,例如arthas和visualvm,可能会漏掉一些分类,每种分类也同样还有着各种各样其他的分析工具,此处就不求尽善尽美了。
以下总结一下我在大促压测过程中所遇到的一些比较典型的性能问题。
公司的部分老应用仍然使用的Log4j,打印日志全部为同步方式,就会导致在并发高且业务日志多的情况下,会造成日志大量阻塞。
有些代码不论有多大的数据都直接往redis里面塞,只要并发稍微一高,就很容易导致redis的带宽达到上限。
很多代码查询mysql的时候,无论什么场景都会将表的所有的字段都查询出来,会导致两个结果:
比较容易犯的问题,一般会产生慢SQL,甚至可能导致数据库cpu消耗严重。
也是比较容器犯的问题,会对应用本身和数据库都产生或多或少的性能影响,至于具体的影响度暂时还没有直观数据。
正则表达式在业务中也是比较常用的,但是有些糟糕的正则表达式可能会导致一些可怕的后果,会严重消耗cpu资源,举个例子,如下
public class Regex { public static void main(String[] args) { String regex = "(//w+,?)+"; String val = "abcdefghijklmno,abcdefghijklmno+"; System.out.println(val.matches(regex)); } } 复制代码
就这么一段看上去简单的代码,会一直保持着cpu单核100%的状态,而且会执行15秒左右。具体原因可以详见防风的文章 www.ffutop.com/posts/2018-…
大量使用DateFormat导致极大地cpu资源消耗,一般情况下请使用FastDateFormat替代SimpleDateFormat,性能能提升一倍以上。对于一些时间点比较规整的且瓶颈点仍在DateFormat上的,可以考虑使用缓存等方案。