善用 ApacheAB 和 VisualVM, 做开发阶段代码性能调优

     在开发阶段, 开发人员往往很难意识到代码对性能的影响, 很多时候需要最终的压力测试来逐一排除. 但是作为创业公司, 往往没有专门的压力测试流程, 那么核心开发人员在核心业务逻辑的开发阶段就需要对代码造成的性能问题作出预判和解决, 这里提供开发本地可操作的, 基于 ApacheAB 和 VisualVM 的性能调优方案.

     通常 Linux 系统都是自带 Apache 的, 因此也自带了 AB, 只要是 JDK 环境, 都是自带 VisualVM 的, 所以两者的获取成本极低, 甚至不需要专门准备. 我的机器上路径如下, 各个环境可能不同. 
VisualVM/Library/Java/JavaVirtualMachines/jdk1.8.0_31.jdk/Contents/Home/bin/jvisualvm
ApacheAB/Applications/MAMP/bin/apache2/bin/ab 


1: ApacheAB 基本使用
     ApacheAB是随着 Apache 服务器一起诞生的, 极为稳定成熟, 但是功能也相对较弱, 比如只支持 HTTP 调用, 没有预热过程, 不提供流量增长和变化侧率. 然而, 作为一个命令行工具, 它极其简单易用.
     这里先给出典型的使用方式: 
Get请求ab -n 100 -c 2 http://127.0.0.1:8080/api/v1/home/city/179
 Post 请求ab -n 100 -c 2 -p /var/post.file -T ‘application/json’ http://127.0.0.1:8080/api/v1/pub/search 

其中 -n 表示总共发出多少个请求, -c 表示总共多少并发用户, -p 表示 post请求的参数所在的文件位置, 设置 -p 的时候还需要设置 -T, 用于指定 Content-type.
     
     调用完成之后, AB 还会给出一个调用报告:
> ab -n 100 -c 2 http://127.0.0.1:8080/api/v1/home/city/179
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient).....done
Completed 100 requests

Server Software:        Apache-Coyote/1.1
Server Hostname:      127.0.0.1
Server Port:            8080

Document Path:          /api/v1/home/city/179
Document Length:        11419 bytes

Concurrency Level:      2
Time taken for tests:   1.894 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      1178500 bytes
HTML transferred:       1141900 bytes
Requests per second:    52.79 [#/sec] (mean)
Time per request:       37.888 [ms] (mean)
Time per request:       18.944 [ms] (mean, across all concurrent requests)
Transfer rate:          607.52 [Kbytes/sec] received

Connection Times (ms)
                      min  mean[+/-sd]   median   max
Connect:         0      0       0.3          0           3
Processing:    25     37    12.9        34          100 
Waiting:          24     35    10.9        33          93
Total:              25      38    12.9       35         100

Percentage of the requests served within a certain time (ms)
  50%     35
  66%     38
  75%     40
  80%     43
  90%     50
  95%     65
  98%     95
  99%    100
 100%    100 (longest request)
其中有一些关键指标值得解读:

Concurrency Level:   2 即有多少个并发
    AB 通过多线程来请求配置的 URL, 它们会同一时间发送请求, 然后完成一个之后继续下一个, 直到 -n 配置的总请求数量消耗完为止. 
    并发数量, 并不等于服务端的实际处理能力, 因为在这些线程刚起的瞬间, 多少个用户就有多少个并发, 但是由于 Server 处理能力有限, 这些请求会阻塞在 Server 端排队处理, 这就导致了并发线程不是无限制不停的发送请求的, 而发送的时候也不一定都是同步的.
     并发用户越多, 那么对系统造成的压力越大, 因为阻塞在 Server 端的请求会越多, 由于线程轮转, 导致每个线程抢到 CPU 的概率降低, 从而响应时间增加, 如过无限增加, 甚至会出现系统无法给出返回的情况, 造成吞吐量为0. 这种情况其实最能提现单线程的优势, 因为没有 CPU 轮转, 所以处理效率更高. 
     当然系统级别有优化措施, 即线程池, 一次性调度进入 CPU 处理的总数量是有限的, 多出来的就一直等在队列中.  这样就造成了系统的处理能力一直是固定的, 但是单个请求从进入队列到最终处理完的相应时间变长. 
     理解了这一点, 就能理解并发和 QPS 的关系.

Requests per second:    52.79 [#/sec] (mean), 即平均 QPS(Request Per Second) 为52.79
    上面也分析过, Request 到了 Server 端之后会排队处理, 而单位时间处理的能力是有限的, 总共的请求数量以及从第一个请求发出到最后一个请求结束的总时间, 进行评价, 就得到每秒的系统处理能力, 即吞吐量.
     吞吐量跟系统处理能力, 一次抢占 CPU 的线程池大小都是相关的.


Percentage of the requests served within a certain time (ms) 响应时间的百分位数
    由于系统的吞吐能力固定, 所以并发用户越多, 那么单个请求的响应时间越长, 用百分位数来描述请求响应时间比较合适. 常见的统计指标中 median中位数, 即第50个百分位数.
     90%     50 指的是90%的请求在50毫秒内返回. 
    如果我们定义用户体验指标, 那么就可以用百分位数和响应时间来确认, 如 90%的用户请求在1秒以内完成. 定义了指标之后, 就可以测出在这个指标下系统的并发能力. 通过不断调整 -n 参数并查看结果, 找到更指标最接近的那个值, 就是系统在指标下的并发能力. 



2: VisualVM 基本使用
    VisualVM 是基于 JMX 来对 JVM 进行数据采集的, 所以开发的时候需要 将项目启动为 Debug 模式这样 JMX 端口才会被开启, 否则VisualVM并不能找到这个实例. 
    VisualVM 的基本功能有: JVM概览, 监视, 线程, 抽样器, Profiler. 通过安装插件可以扩展部分功能, 如 MBeans 和 VisualGC. 双击一个实例之后, 点击打开使用界面
    主要指标: CPU, 内存使用, 类总数, 线程数等等. 其中:
        如果做 JVM 调优, 那么可以观察堆的GC 状况.
        线程, 用于查看所有的线程状况, 可以非常方便看到多线程之间的运行状况. 这里甚至可以查看线程很锁的关系, 方便进行死锁检测. 这个地方就能看出来对线程认真命名的重要性了,否则很难简单的判断线程为何而生。
        抽象器可以对内存和 CPU 的使用状况进行实时监测, 而且还能创建实时 Dump文件. 如果对代码进行调优, 这个部分是核心功能. 这里就可以分析某些调用占用了太多CPU,而某些类占用了太多内存。


3: 调优案例
/Applications/MAMP/bin/apache2/bin/ab -n 5000 -c 200 -T 'application/json' -p /tmp/ab.post http://127.0.0.1:8080/api/v1/teacher/pub/search
/tmp/ab.post 为 POST 所发出的数据, 内容为:
{
  "lon": 30.288,
  "lat": 120.131,
  "lessonId": 1,
  "cityCode": 179
}
本接口是某项目中基于 LBS查找老师列表的核心接口, 由于是基于 LBS 的, 所以很难做缓存(当然不是不能走, 如果是地理方格, 是可以做到缓存的, 但是这个跟本部分无关)

核心代码为: 
SearchResponse response = searchClient.getClient().prepareSearch(indices).setTypes(teacherType)
        .addSort(searchSort)
        .setQuery(bqb).setPostFilter(filterQb)
        .setFrom(pageable.getOffset())
        .setSize(pageable.getPageSize()).execute().actionGet();
long total = response.getHits().getTotalHits();
List<TeacherSummary> beanList = new ArrayList<>();
if (total > 0) {
    for (SearchHit hit : response.getHits().getHits()) {
        //Handle the hit...
        TeacherSummary bean = new TeacherSummary();
        org.apache.commons.beanutils.BeanUtils.copyProperties(bean, hit.getSource());
        if (form.getSortType() == TeacherSearchForm.SortType.DISTANCE) {
            double distance = this.getDistance(hit);
            bean.setDistance(distance);
        } else {
            double[] location = bean.getLocation();
            if (location == null || location.length == 0) {
                location = new double[]{city.getLongitude(), city.getLatitude()};
            }
            //手动计算
            double distance = Distance.show(form.getLon(), form.getLat(), location[0], location[1]);
            bean.setDistance(distance);
        }
        beanList.add(bean);
    }
}
Page<TeacherSummary> page = new PageImpl<TeacherSummary>(beanList, pageable, total);
form.setCityId(city.getId());
form.setPage(pageable.getPageNumber());
form.setPageSize(pageable.getPageSize());
form.setSearchSource(SearchLog.SearchSource.TEACHER);
addSearchLog(form);



这段代码的核心意思就是, 通过 ES 找到结果, 然后 Mapping 成 ResultObject, 之后返回给前端.

发起 AB 请求之后, 观察 VisualVM 的测试结果:


上图显示, 进行了两次测试, 两次测试中 CPU 使用率飙升, 同时内存进行了频繁的 GC, 线程数稳定上升. 
可以通过优化 JVM 的方式来尽量减少 GC 的次数..

此外,通过抽样器:


发现占用 CPU 最大的竟然是打 Log 的过程, 那么先将 DebugLog 去除掉. 不让日志的过程影响测试. 






通过分析, 可以看到, BeanUtil这段代码调用过程中, 出现了大量的反射和 JDBC 操作, 回头观察代码, 
org.apache.commons.beanutils.BeanUtils.copyProperties(bean, hit.getSource());
form.setCityId(city.getId());
第一段, 实际上是将 ElasticSearch 中的结果映射成 Object, 然后再返回给前端. 实际上,  hit.getSource() 得到的是一个 Map<String, Object>, 直接转化成 JSON 传给前端, 效果是一样的, 那么完全可以省去这个过程. 即原来的过程是 Map<String, Object> ->  TeacherSummary -> JSON, 直接变成 Map<String, Object> -> JSON, 这个过程 Spring 同样可以自动完成. 省去了一个反射生成 TeacherSummary 的过程直接得到结果. 
第二段, 从 JPA/Hibernate 的基本原理可以知道, City 对象本身只是一个代理, 只有去具体拿值的时候才会去 DB 取出原始数据. 观察 SQL 输出, 发现的确每次返回结果都有一条 SQL 打印出来, 可知这个过程完全不必要重复进行, 直接缓存即可. 
@Cacheable(value= Constants.SPRING_DEFAULT_CACHE,key="'skwy:city_by_code:'+#code")
public City findByCode(String code) {
    return cityRepository.findByCode(code);
}

返回来看我们的优化过程, 
首先, 去除了对控制台打 Log 进行的影响, 然后分析抽样器的 CPU 和内存占用情况, 之后结合代码分析, 得出两点可以优化的地方, 然后进行改进. 


附录: ApacheAB 命令详解
Usage: ab [options] [http[s]://]hostname[:port]/path
Options are:
    -n requests     Number of requests to perform
    -c concurrency  Number of multiple requests to make at a time
    -t timelimit    Seconds to max. to spend on benchmarking  This implies -n 50000
    -s timeout      Seconds to max. wait for each response  Default is 30 seconds
    -b windowsize   Size of TCP send/receive buffer, in bytes
    -B address      Address to bind to when making outgoing connections
    -p postfile     File containing data to POST. Remember also to set -T
    -u putfile      File containing data to PUT. Remember also to set -T
     -i              Use HEAD instead of GET
    -T content-type Content-type header to use for POST/PUT data, eg. 'application/x-www-form-urlencoded’  Default is 'text/plain'
    -v verbosity    How much troubleshooting info to print
    -w              Print out results in HTML tables
    -x attributes   String to insert as table attributes
    -y attributes   String to insert as tr attributes
    -z attributes   String to insert as td or th attributes
    -C attribute    Add cookie, eg. 'Apache=1234'. (repeatable)
    -H attribute    Add Arbitrary header line, eg. 'Accept-Encoding: gzip’ Inserted after all normal header lines. (repeatable)
    -A attribute    Add Basic WWW Authentication, the attributes are a colon separated username and password.
    -P attribute    Add Basic Proxy Authentication, the attributes are a colon separated username and password.
    -X proxy:port   Proxyserver and port number to use
    -V              Print version number and exit
    -k              Use HTTP KeepAlive feature
    -d              Do not show percentiles served table.
    -S              Do not show confidence estimators and warnings.
    -q              Do not show progress when doing more than 150 requests
    -l              Accept variable document length (use this for dynamic pages)
    -g filename     Output collected data to gnuplot format file.
    -e filename     Output CSV file with percentages served
    -r              Don't exit on socket receive errors.
    -m method  Method name
    -h              Display usage information (this message)
    -Z ciphersuite  Specify SSL/TLS cipher suite (See openssl ciphers)
    -f protocol     Specify SSL/TLS protocol (SSL3, TLS1, TLS1.1, TLS1.2 or ALL) 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值