连续参加了两年公司的双十一大促压测项目,遇到了不少问题,也成长了不少,因而在这里对大促压测作一份总结。以及记录一下大促压测过程当中出现的一些常见的Java应用性能问题。java
吞吐率 TPS(每秒响应的请求数量)mysql
响应时长 RT (通常状况下重点关注90%请求的响应时长,咱们的大促标准通常是1s之内)git
错误率 (看业务的可接受程度,咱们的大促标准是不超过2%)github
如今有不少能够用来进行压测的工具,例如ab、jmeter、wrk等,此处主要介绍一下ab和jmeter。正则表达式
ab是一个命令行工具,使用起来很是简单,redis
# -c表示并发数,-n表示请求总数,其余一些参数能够查询手册/相关资料
ab -c 10 -n 200 https://www.baidu.com/
复制代码
命令执行完成后会得出一份压测结果报告,其中错误请求数、TPS和RT在下图中有标注算法
jmeter同时支持图形化界面和命令行方式,sql
首先在"Test Plan"中添加一个"线程组",里面能够设置并发数、压测时长等参数。数据库
接下来须要在“线程组”中添加“HTTP请求取样器”,里面是设置HTTP请求的各项参数。json
最后添加查看结果用的监听组件,我我的比较经常使用的有“查看结果树”、“聚合报告”和“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示例
用这个示例代码作个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>[执行时间]'
就能够查看了,更多参数能够查询arthas的文档如上图,咱们能够看到各个方法的执行时间(包含了阻塞时间),筛选出执行时间长的方法,很大可能就能发现形成线程阻塞的瓶颈点。
内存泄漏问题每每都伴随着宕机,我所碰见的状况有以下几种:
这种状况属于相对而言比较容易处理的状况,使用-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上的,能够考虑使用缓存等方案。
远程调用时长远大于cpu消耗的业务直接使用默认线程池或着线程数设置太少,很容易致使线程阻塞。
只据说过,可是我还从未真实见到过,