目录
  1. 一、为什么要压测
  2. 二、如何压测
    1. 关注哪些指标
    2. 压测工具
      1. 1. ab
      2. 2. jmeter
        1. 图形化方式
        2. 命令行方式
      3. 3. ab与jmeter的对比
  3. 三、如何制定大促压测目标
    1. 压测接口的选取
    2. 压测并发数的设定
    3. TPS的设定
  4. 四、关注哪些指标
  5. 五、如何去排查问题
    1. 为什么要找瓶颈点
    2. 几个问题(请根据自己的实际认知回答)
    3. 如何排查性能问题
      1. 1. 数据库慢SQL问题
      2. 2. 网络带宽过大
        1. 如何排查网络带宽过大的问题
      3. 3. 数据库CPU过高
      4. 4. 应用服务器磁盘IO问题
      5. 5. GC问题
        1. 如何监测FullGC
        2. 如何排查GC问题
          1. 举个例子
      6. 6. 应用服务器cpu达到100%问题(以下只针对常规业务应用,不考虑追求极端性能应用)
        1. 排查cpu问题的方式
          1. 其他一些方式
      7. 7. 线程阻塞问题
        1. jstack的方式查看
        2. arthas的方式查看
      8. 8. 内存泄漏问题
        1. Heap内存泄漏
        2. metaspace内存泄漏
        3. nonheap内存泄漏
      9. 9. 从业务角度去排查问题
      10. 10. 总结
  6. 六、压测中出现的典型性能问题
    1. 1. Log4j日志阻塞问题
    2. 2. redis大value问题
    3. 3. sql全字段查询问题
    4. 4. sql未加索引问题
    5. 5. sql N+1问题
    6. 6. 正则表达式问题
    7. 7. DateFormat问题
    8. 8. 线程池不正确使用问题
    9. 9. 传说中的问题
双十一压测&Java应用性能问题排查总结

连续参加了两年公司的双十一大促压测项目,遇到了很多问题,也成长了很多,于是在这里对大促压测做一份总结。以及记录一下大促压测过程中出现的一些常见的Java应用性能问题。

一、为什么要压测

  1. 找出应用的性能瓶颈
  2. 探究应用的性能基准
  3. 给大促机器扩容提供参考依据

二、如何压测

关注哪些指标

吞吐率 TPS(每秒响应的请求数量)

响应时长 RT (一般情况下重点关注90%请求的响应时长,我们的大促标准一般是1s以内)

错误率 (看业务的可接受程度,我们的大促标准是不超过2%)

压测工具

现在有很多可以用来进行压测的工具,例如ab、jmeter、wrk等,此处主要介绍一下ab和jmeter。

1. ab

ab是一个命令行工具,使用起来非常简单,

1
2
# -c表示并发数,-n表示请求总数,其他一些参数可以查询手册/相关资料
ab -c 10 -n 200 https://www.baidu.com/

命令执行完成后会得出一份压测结果报告,其中错误请求数、TPS和RT在下图中有标注

image-20191105160037216

2. jmeter

jmeter同时支持图形化界面和命令行方式,

图形化方式

首先在”Test Plan”中添加一个”线程组”,里面可以设置并发数、压测时长等参数。

接下来需要在“线程组”中添加“HTTP请求取样器”,里面是设置HTTP请求的各项参数。

最后添加查看结果用的监听组件,我个人比较常用的有“查看结果树”、“聚合报告”和“TPS曲线图”(需要安装)。

image-20191105163923823

重点来看一下“聚合报告”,(一定要记得每次压测前都要清理一下数据才行[上方的”齿轮+2个扫把”图标],不然回合之前的数据混合在一起)

image-20191105165259837

上面只是一些简单的介绍,事实上jmeter还支持很多复杂的压测场景: jdbc压测、dubbo压测、动态参数压测、自定义响应断言……这些可以自行网上搜索。

命令行方式

命令行方式主要可以用来做一些自动化压测的任务。使用方式如下:

1
jmeter -n -t [jmx脚本] -l [压测请求结果文件] -e -o [压测报告文件夹(会生成一堆网页文件)]

其中jmx脚本可以先通过jmeter图形化界面全部设置好了,然后保存一下就会生成对应的jmx脚本了。

3. ab与jmeter的对比

操作 ab jmeter
操作难度 简单 复杂
命令行 支持,操作简单 支持,操作稍微复杂一些
请求结果列表 无法显示 有详细请求列表
动态参数 不支持 支持
复杂场景支持 极其有限 丰富

基本上,对于一些简单的固定参数请求并且是自测的情况下,使用ab会非常简便。一般情况下jmeter的适用性会更广。

三、如何制定大促压测目标

压测接口的选取

一般情况下,不必要将公司所有的接口都进行压测,压测接口主要包含核心链路接口、访问量大的接口以及新上线的活动接口。获取方式基本是如下两种:

  1. 对核心业务进行抓包
  2. 咨询各业务线负责人

压测并发数的设定

在上面的两种压测工具中,我们都看到了一个参数为并发数,这个参数一般需要根据公司的业务量来进行推算,可以去网上找些资料。不过为了简化压测过程,我们公司的大促统一使用读接口200并发,写接口100并发的标准来执行的。

事实上,我对并发数的设定这块也比较模糊,因此上述描述仅做参考。

TPS的设定

一般是根据大促销售目标、平时各接口qps、各接口访问量按照比例制定出最终的TPS目标,不要忘了最后乘上一个风险系数。具体的算法可以自行设计,大概思路就是这样的。

四、关注哪些指标

对于压测工具的指标上面已经说过了,主要是关注TPS、RT和错误率。

那么还有哪些需要关注的指标呢?其实这个都是根据公司的业务来决定的,例如我们公司主要使用java应用、mysql作为数据库、redis作为缓存中间件,那么我们主要关注的性能数据如下:

监控对象 性能指标
压测指标 TPS、RT、错误率
应用服务器(服务化应用包含下游链路的应用服务器) CPU、网络带宽、磁盘IO、GC
数据库 CPU、网络带宽、慢SQL
REDIS 网络带宽
其他 (根据相应特性自行设定)

每个监控对象都有其特性,所以应该根据实际情况的来制定自己的监控指标。

五、如何去排查问题

由于公司主要使用的是Java8,因此本文也主要是针对Java8应用做分析。

为什么要找瓶颈点

举个例子,如果告诉你一个接口稍微一些压力就能把服务器的cpu跑满了,导致TPS上不去,里面一堆复杂逻辑,而且还有不少远程调用(数据库查询、缓存查询、dubbo调用等)。

你可能对业务非常熟悉,开始大刀阔斧地进行代码修改、增加缓存、业务降级等,也许期望很美好,但是事实上有极大的可能是你做的一切对TPS只能产生轻微的影响。然后只能通过不停地尝试删改代码去查找问题点,那么显然只能带来几个结果: 1.效率低下 2.把代码弄的一团糟 3.不具备可复制性 4.对业务会造成或小或大的影响,最最关键的是改的时候心里也没底、改完之后心里依旧没底。

几个问题(请根据自己的实际认知回答)

  1. 你认为cpu达到100%是好是坏?
  2. 你认为哪些代码对cpu的开销大?你认为大量判断逻辑对cpu的开销大吗?
  3. 你认为大量的网络数据传输对哪些指标的影响大?
  4. 你认为对于Java应用监控服务器内存的必要性有多高?

如何排查性能问题

那么问题来了,我们到底应该怎么去排查问题呢?(以下均为一些个人经验,可能会有不少遗漏,或者会有一些错误,如果有的话,请及时指出)

排查问题的话,首先我们需要先有一些排查的突破点和方向。(无法保证100%找到对应问题,但是大幅提升找到性能问题的效率)

前面有提到,我们压测过程中需要监控各项指标,那么其实我们的突破方向一般就在这些监控指标上了。我们可以对这些指标进行分类,对于每一类都可以有着相对应的排查策略。

1. 数据库慢SQL问题

这个问题是最好排查的一类问题了,只需要对慢SQL进行针对性地分析优化即可,此处不过多讲解。

2. 网络带宽过大

那么一个问题来了,此处的网络带宽到底是指的什么呢?换个问法吧,假设数据库的带宽上限为1Gbps,实际上压测导致数据库的网络带宽占用了800Mbps,那可以说明这个接口是一个问题接口吗?

考虑下面这种情况,这个接口的TPS假设在压测过程中达到了80000,远大于接口实际目标TPS,那该接口将数据库的带宽占到800Mbps是合情合理的。

那么上面的问题的答案也就呼之欲出了,这里的网络带宽,在很多情况下,我们更应该关注的是单个请求的平均占用带宽。

如何排查网络带宽过大的问题

猜一猜,其实不难想象,就是抓包。我常用的抓包方式是通过tcpdump抓包,然后使用wireshark解析抓包内容(如果有更简单的方式,可以留言)。下面讲一下tcpdump+wireshark的方式如何抓包。

为了避免大量的数据混杂在一起,一般情况下,我更喜欢是抓单个请求的数据,而不是在压测中抓包。下面简单介绍一下tcpdump和wireshark如何抓包,

  1. 在服务器上执行命令sudo tcpdump -w xxx.pcap
  2. 然后请求一下接口
  3. Ctrl+C停掉tcpdump
  4. 将xxx.pcap拷到本地,使用wireshark打开
  5. 如下图,找到一个请求 > 右键”Follow” > “TCP Stream”

image-20191106175128011

打开TCP流后通过调整右下方的”Stream”,我们就可以看到应用在请求过程中的网络数据(包含Http请求数据、Mysql请求数据、Redis请求数据……),以下图为例,可以看到这个请求的mysql请求量非常大,接下来就是查看到底是哪些SQL语句导致的。

image-20191106175708652

3. 数据库CPU过高

开启数据库日志,看看压测期间都执行了哪些SQL语句,然后进行针对性的分析即可。一般情况下,全表扫描、不加索引、大表的count这些都比较容易引起cpu问题。绝大多数情况下都可以通过技术手段来优化,但也有可能技术手段无法优化的情况,则可以考虑业务上的优化。

4. 应用服务器磁盘IO问题

绝大多数情况下是由于日志问题导致的,日志问题一般分为如下两种情况:

  1. 压测接口参数/环境有问题,导致接口不停地打印异常
  2. 打印了大量的业务日志

至于其他的磁盘IO问题,则需要根据实际业务去分析了,暂时未遇到过,此处略过。

5. GC问题

一般情况下,我们不太需要去关注YoungGC,更多地只需要关注FullGC就行了,如果只是偶尔出现一次FullGC,那基本上没有太大问题,如果频繁FullGC(几秒就有一次FullGC,甚至可能一秒几次),那就要做相应排查了。

如何监测FullGC

一般可以通过jstat来监测,命令如下:

1
jstat -gccause [PID] 1000

image-20191106141050865

具体的每个参数的含义可以查看man jstat手册。

其实用visualvm装个GC插件然后监测java进程,可以很直观地看到java应用的内存和GC情况,就是操作相对而言比较繁琐。

如何排查GC问题

很多情况下(主要是大对象/大量堆对象导致FullGC的情况),都可以通过将Java堆dump下来,然后通过MAT、jhat等内存分析工具来分析。流程如下:

  1. 首先需要dump堆文件,在服务器上执行如下命令: jmap -dump:format=b,file=heap.bin [PID]
  2. 然后将堆文件拷到本地,使用MAT打开(需要调大内存启动参数,必须要比堆文件大),个人比较常用的MAT功能是”Leak Suspects”和”Histogram”,前者是可能出现内存泄漏的怀疑点,后者是堆中类的直方图
举个例子

此处以一个真实的出现过宕机的Java应用的堆作为举例(加上-XX:+HeapDumpOnOutOfMemoryError这个参数就可以在出现OOM的时候自动将堆dump下来了)

image-20191106144338830

本文简单看一下”Leak Suspects”,至于Histogram则可以自行去研究。

image-20191106145139680

这个堆文件其实还是比较简单的,因为可怀疑点只有一个,八九不离十就是这块出现问题了。点击”Details”可以看到更详细的信息(ps:不是每种怀疑对象都有Details的)。

image-20191106145600510

在详细信息里面基本上可以很明显地看出来,有一个SQL语句查出来了超多的数据,导致内存塞不下了。事实上,最终在数据库日志中找到了这条语句,共查询了200W+条数据。

这个例子比较简单,事实上我们可能会遇到更多复杂的情况,例如怀疑对象特别多,甚至真正原因并不在怀疑对象中,或者metaspace导致的FullGC,这些情况下,我们可能又需要采用其他方式去处理这些问题。

6. 应用服务器cpu达到100%问题(以下只针对常规业务应用,不考虑追求极端性能应用)

还记得之前的有个问题——你认为cpu达到100%是好是坏吗?

那么在这里我揭晓一下答案,如果接口的TPS高,那么我们的服务器的cpu当然是越高越好了,因为这说明了资源被充分利用了。但是,如果接口的TPS低,那么cpu达到100%就说明很有可能是有问题了,很大可能是存在问题代码占据了大量的cpu。

那么还有一个问题就是,你认为哪些代码对cpu的开销大?

  1. 大量的业务逻辑判断——几乎无影响 (上万个if语句可能总的执行时间都不会超过1ms)
  2. 大量的网络传输——几乎无影响 (会有一些cpu开销,但是极其有限,可以找个应用生成火焰图看看)
  3. 线程阻塞——几乎无影响 (如果不考虑极其大量的线程切换的话,那么线程阻塞是不会占用cpu的)
  4. 大量的内存拷贝——几乎无影响 (会有一些cpu开销,但是极其有限,可以找个应用生成火焰图看看)

这些都没有影响,那到底什么才对cpu有影响呢?常见的业务场景总结如下(如有遗漏请留言补充)

  1. 长度特别大的循环,尤其是多层嵌套循环
  2. 字符串处理,常见的消耗cpu的操作是json解析、正则表达式
  3. 日期格式化,Date.format非常消耗cpu
  4. 大量的sql数据处理,sql数据处理量很多的情况下是会大量占用cpu的,这个情况最为常见
  5. 大量的日志输出有时也会占用不少的cpu,不过更多的情况是产生线程阻塞

排查cpu问题的方式

我在大促压测中实践的比较多的方式是perf + perf-map-agent + FlamaGraph工具组合,其中perf是用来监控各个函数的cpu消耗(可以实时监控,也可以记录一段时间的数据),perf-map-agent是用来辅助perf使用的,用来生成java堆的映射文件,FlamaGraph则是用来生成火焰图的。

这套工具的安装使用就不做介绍了,可以参考一下下面这两篇文章,

http://senlinzhan.github.io/2018/03/18/perf/
https://www.jianshu.com/p/bea2b6a1eb6e

主要使用方式,有如下两种:

  1. sudo perf top [-g],可以实时观察cpu的消耗,操作相对比较轻量级
  2. 生成火焰图(一定要用浏览器打开),操作相当繁琐,不过生成的信息也更详细,更易阅读,对于那些无法一眼看出来的问题会有不错的效果

火焰图示例

下面展示一下本次大促压测solr优化过程中生成的火焰图,从图中可以看到YoungGC就占用了将近一半的cpu,

image-20191106155941396

perf-top示例

用这个示例代码做个perf-top的使用示范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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给占用了,(下面还有很多展示不出来的,事实上会更多)

image-20191107113205727

有了这些工具之后,绝大多数问题都已经可以比较容易地找到性能优化点了。

其他一些方式

上面那套组合实际用起来十分繁琐,大促压测结束后又了解到了一些其他工具,不过未经过真实实践,所以列出来仅做参考:

  1. visualvm中的Sampler

  2. jvmtop

  3. https://github.com/oldratlee/useful-scripts ,这个里面有个show-busy-java-threads脚本,试用了一下,感觉超方便,后续考虑在真实排查问题中实践一下

7. 线程阻塞问题

当我们在系统的所有环节都无法找到硬件瓶颈的时候,那往往就是线程产生了阻塞,一般情况下线程阻塞可以使用jstack和arthas来排查,分别举个例子吧,用下面这段样例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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的方式查看

1
jstack [PID]

image-20191106184616989

从图中可以看到大量的线程都卡在Block.sleep()上。一般情况下,jstack可以配合grep来使用,通常关注得比较多的状态更多是BLOCKED。jstack相对而言没那么直观,但是比较轻量级,很多时候也可以比较容易地看出来一些常见的线程阻塞问题。

arthas的方式查看

arthas其实是一个比较全能的jvm性能分析工具,用起来也是各种舒服,而且相对而言也比较轻量,强烈推荐。

此处主要介绍arthas在排查线程阻塞方面的应用,

  1. 执行arthas,命令java -jar arthas-boot.jar
  2. 选择目标java进程
  3. 执行trace [包名.类名] [方法名] -n [数量] '#cost>[执行时间]'就可以查看了,更多参数可以查询arthas的文档

image-20191106185440691

如上图,我们可以看到各个方法的执行时间(包含了阻塞时间),筛选出执行时间长的方法,很大可能就能发现造成线程阻塞的瓶颈点。

8. 内存泄漏问题

内存泄漏问题往往都伴随着宕机,我所遇见的情况有如下几种:

Heap内存泄漏

这种情况属于相对而言比较容易处理的情况,使用-XX:+HeapDumpOnOutOfMemoryError参数可以在应用宕机的时候自动dump下堆文件,然后使用MAT等内存分析工具在绝大多数情况下都可以找到问题原因。

metaspace内存泄漏

这个有见过JVM调用groovy在某些情况下会产生内存泄漏。不过没有真实排查过相关问题,此处略过。

防风有一篇文章可以参考一下GroovyClassLoader 引发的 FullGC

nonheap内存泄漏

nonheap内存泄漏问题属于非常难排查的问题,一般情况下比较难dump下堆文件,即使dump下来了,一般情况下也很难确定原因,之前有用过tcmalloc、jemalloc等工具进行排查过。暂时没找到什么比较通用的套路,一般也是特事特办。根据之前的排查经验来看,如下几种情况会比较容易出现nonheap内存泄漏(如果遗漏,请留言补充):

  1. 图片合成业务中,涉及到Font的创建,可以详见之前的文章记一次Font导致JVM堆外内存泄漏分析
  2. 使用了JNI的情况下很有可能会导致JVM的arena内存区刚好超过机器内存限制 / nonheap内存泄漏(可以参考防风的文章JNI 引发的堆外内存泄露
  3. GZIPStream未关闭的情况会导致nonheap泄漏 (来源于网上资料,未真实遇到过)

9. 从业务角度去排查问题

排查很多问题之前,最好能够先去了解一下相关业务逻辑,因为很多性能问题是由于大量的问题业务代码引起的,很多时候从业务角度去考虑、辅以技术手段往往能够得到更好的效果。

10. 总结

上面的各种方式只是提供一些策略,无法保证100%能够找到问题,甚至可能连70%都保证不了,更多情况下我们需要灵活使用各种工具进行问题分析。总结一下上面的性能分析工具,可以大概如下分类:

类型 工具
全能型分析工具 arthas、visualvm
cpu分析工具 perf、jvmtop
内存分析工具 jmap、jhat、MAT
网络分析工具 tcpdump、wireshark
GC分析工具 jstat、gc日志文件、visualvm
堆栈分析工具 jstack、arthas
Linux底层排查工具 strace、perf、dmesg、journalctl

有些工具甚至有更多的功能,例如arthas和visualvm,可能会漏掉一些分类,每种分类也同样还有着各种各样其他的分析工具,此处就不求尽善尽美了。

六、压测中出现的典型性能问题

以下总结一下我在大促压测过程中所遇到的一些比较典型的性能问题。

1. Log4j日志阻塞问题

公司的部分老应用仍然使用的Log4j,打印日志全部为同步方式,就会导致在并发高且业务日志多的情况下,会造成日志大量阻塞。

2. redis大value问题

有些代码不论有多大的数据都直接往redis里面塞,只要并发稍微一高,就很容易导致redis的带宽达到上限。

3. sql全字段查询问题

很多代码查询mysql的时候,无论什么场景都会将表的所有的字段都查询出来,会导致两个结果:

  1. 网络带宽极大浪费,尤其是查询中包含了不必要的”描述”等超大字段
  2. 极大地消耗cpu资源

4. sql未加索引问题

比较容易犯的问题,一般会产生慢SQL,甚至可能导致数据库cpu消耗严重。

5. sql N+1问题

也是比较容器犯的问题,会对应用本身和数据库都产生或多或少的性能影响,至于具体的影响度暂时还没有直观数据。

6. 正则表达式问题

正则表达式在业务中也是比较常用的,但是有些糟糕的正则表达式可能会导致一些可怕的后果,会严重消耗cpu资源,举个例子,如下

1
2
3
4
5
6
7
8
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秒左右。具体原因可以详见防风的文章 https://www.ffutop.com/posts/2018-11-16-regex-exponential-explosion/

7. DateFormat问题

大量使用DateFormat导致极大地cpu资源消耗,一般情况下请使用FastDateFormat替代SimpleDateFormat,性能能提升一倍以上。对于一些时间点比较规整的且瓶颈点仍在DateFormat上的,可以考虑使用缓存等方案。

8. 线程池不正确使用问题

远程调用时长远大于cpu消耗的业务直接使用默认线程池或着线程数设置太少,很容易导致线程阻塞。

9. 传说中的问题

只听说过,但是我还从未真实见到过,

  1. 大量线程切换导致cpu开销大
  2. 数据库死锁问题
  3. 数据库连接池设置问题(有发生过几次,但是我都不在现场)
  4. … …
文章作者: 谷河
文章链接: https://www.lyytaw.com/java/%E5%8F%8C%E5%8D%81%E4%B8%80%E5%8E%8B%E6%B5%8B&Java%E5%BA%94%E7%94%A8%E6%80%A7%E8%83%BD%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5%E6%80%BB%E7%BB%93/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 谷河|BLOG