双十一压测&Java应用性能问题排查总结

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

1、为何要压测

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

2、如何压测

关注哪些指标

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

响应时长 RT (通常状况下重点关注90%请求的响应时长,咱们的大促标准通常是1s之内)git

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

压测工具

如今有不少能够用来进行压测的工具,例如ab、jmeter、wrk等,此处主要介绍一下ab和jmeter。正则表达式

1. ab

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

# -c表示并发数,-n表示请求总数,其余一些参数能够查询手册/相关资料
ab -c 10 -n 200 https://www.baidu.com/
复制代码

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

image-20191105160037216

2. jmeter

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

图形化方式

首先在"Test Plan"中添加一个"线程组",里面能够设置并发数、压测时长等参数。数据库

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

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

image-20191105163923823

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

image-20191105165259837

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

命令行方式

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

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

其中jmx脚本能够先经过jmeter图形化界面所有设置好了,而后保存一下就会生成对应的jmx脚本了。

###3. ab与jmeter的对比

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

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

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

压测接口的选取

通常状况下,没必要要将公司全部的接口都进行压测,压测接口主要包含核心链路接口、访问量大的接口以及新上线的活动接口。获取方式基本是以下两种:

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

压测并发数的设定

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

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

TPS的设定

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

4、关注哪些指标

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

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

监控对象 性能指标
应用服务器(服务化应用包含下游链路的应用服务器) CPU、网络带宽、磁盘IO、GC
数据库 CPU、网络带宽、慢SQL
REDIS 网络带宽

每一个监控对象都有其特性,因此应该根据实际状况的来制定本身的监控指标。

5、如何去排查问题

因为公司主要使用的是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来监测,命令以下:

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则是用来生成火焰图的。

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

senlinzhan.github.io/2018/03/18/… www.jianshu.com/p/bea2b6a1e…

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

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

火焰图示例

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

image-20191106155941396

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给占用了,(下面还有不少展现不出来的,事实上会更多)

image-20191107113205727

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

其余一些方式

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

  1. jvmtop
  2. github.com/oldratlee/u… ,这个里面有个show-busy-java-threads脚本,试用了一下,感受超方便,后续考虑在真实排查问题中实践一下

7. 线程阻塞问题

当咱们在系统的全部环节都没法找到硬件瓶颈的时候,那每每就是线程产生了阻塞,通常状况下线程阻塞可使用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的方式查看

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

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

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

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

1. Log4j日志阻塞问题

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

2. redis大value问题

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

3. sql全字段查询问题

不少代码查询mysql的时候,不管什么场景都会将表的全部的字段都查询出来,会致使两个结果:

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

4. sql未加索引问题

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

5. sql N+1问题

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

6. 正则表达式问题

正则表达式在业务中也是比较经常使用的,可是有些糟糕的正则表达式可能会致使一些可怕的后果,会严重消耗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-…

7. DateFormat问题

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

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

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

9. 传说中的问题

只据说过,可是我还从未真实见到过,

  1. 大量线程切换致使cpu开销大
  2. 数据库死锁问题
  3. 数据库链接池设置问题(有发生过几回,可是我都不在现场)
  4. ... ...
相关文章
相关标签/搜索