zoukankan      html  css  js  c++  java
  • 基于JMH的Benchmark解决方案

    原始Benchmark做法

    在设计新框架的时候,往往需要评估待接入的组件的性能,这个时候我们可能会利用UnitTest来进行,写一个方法,然后在循环里面跑,利用System.CurrentTimeMillis()来评估组件性能。然而这种机制,只是跑在了主线程中,无法将组件的性能全部测算出来。当单线程测算的性能已经到达极限的瑟吉欧鸡皮,无论怎么增加循环次数,OPS都不会有显著的提升。

    上面的方案不怎么靠谱后,我们转向了多线程测算。一般都是在本地开几个线程,然后循环处理。之后再利用System.CurrentTimeMillis()的差值来评估组件性能。此种方法虽然更为靠谱了一些,但是依然面临着样本循环次数小,统计难度大,统计分类不全的特点。如果想测算的更精细,怕是没有个一时半会,得不到什么有效结果。

    很显然你,上面的方法,是生产力低下的做法,那么有什么方法能够一劳永逸呢?

    JMH Benchmark做法

    今天我们将会讲解基于openjdk构建的jmh benchmark的做法,此种做法在github上很流行,很多开源代码都会在readme中附带上自己的benchmark,通俗易懂,而且让我们对性能有大概的了解。究竟如何做到的呢?

    首先,我们需要引入maven包:

            <!--bench mark-->
            <dependency>
                <groupId>org.openjdk.jmh</groupId>
                <artifactId>jmh-core</artifactId>
                <version>1.19</version>
                <!--<scope>test</scope>-->
            </dependency>
            <dependency>
                <groupId>org.openjdk.jmh</groupId>
                <artifactId>jmh-generator-annprocess</artifactId>
                <version>1.19</version>
                <!--<scope>test</scope>-->
            </dependency>

    引入jmh-core和jmh-generator-annprocess的作用是利用其做真正的benchmark操作。注意,在运行的时候,需要注释掉scope,然后先clean,然后package,最后install,一定要进行install操作,否则会提示配置文件找不到的问题。benchmark类不要放到unittest目录下,否则启动报错。

    然后,编写我们的benchmark代码,这里我以local cache为例来做介绍:

    package com.jd.limitbuy.common.cache.offheap.local;
    
    import org.openjdk.jmh.annotations.*;
    import org.openjdk.jmh.runner.Runner;
    import org.openjdk.jmh.runner.RunnerException;
    import org.openjdk.jmh.runner.options.Options;
    import org.openjdk.jmh.runner.options.OptionsBuilder;
    import java.util.UUID;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author shichaoyang
     * @Description: local cache组件的benchmark
     * @date 2018-08-16 11:04
     */
    
    @BenchmarkMode(Mode.Throughput)
    @OutputTimeUnit(TimeUnit.SECONDS)
    @State(Scope.Thread)
    public class localCacheBenchmark {
    
        private LocalCacheBuilder localCacheBuilder;
    
        private LocalCacheWrapper localCacheWrapper;
    
        @Setup
        public void init() {
    
            localCacheBuilder = new LocalCacheBuilder();
            localCacheBuilder.Init();
    
            localCacheWrapper = new LocalCacheWrapper(localCacheBuilder);
    
            localCacheWrapper.set("my_benchmark_key", "this is my first benchmark!!!!!", "localMap1");
    
            localCacheWrapper.hset("my_benchmark_key_hash", "my_hash_key", "this is my first benchmark!!!!!", "localMap1");
    
            localCacheWrapper.sadd("my_benchmark_key_set", "this is my hash benchmark!!!!!", "localMap1");
    
            localCacheWrapper.zadd("my_benchmark_key_zset", "start_val", 100, "localMap1");
    
            localCacheWrapper.zadd("my_benchmark_key_zset", "end_val", 101, "localMap1");
        }
    
        /**
         * GroupThreads 并发线程数设置为3,可以打出接口最大的ops
         */
    
        @Benchmark
        @GroupThreads(4)
        public String testLocalCacheSet() {
            localCacheWrapper.set(UUID.randomUUID().toString(), "this is my first benchmark!!!!!", "localMap1");
            return "ok";
        }
    
        /**
         * GroupThreads 并发线程数设置为3,可以打出接口最大的ops
         */
        @Benchmark
        @GroupThreads(4)
        public String testLocalCacheGet() {
            return localCacheWrapper.get("my_benchmark_key", "localMap1");
        }
    
        @Benchmark
        @GroupThreads(4)
        public String testLocalCacheHSet() {
            localCacheWrapper.hset(UUID.randomUUID().toString(), UUID.randomUUID().toString(), "this is my hash benchmark!!!!!", "localMap1");
            return "ok";
        }
    
        @Benchmark
        @GroupThreads(4)
        public String testLocalCacheHGet() {
            return localCacheWrapper.hget("my_benchmark_key_hash", "my_hash_key", "localMap1");
        }
    
        @Benchmark
        @GroupThreads(4)
        public String testLocalCacheHGetAll() {
            localCacheWrapper.hgetAll("my_benchmark_key_hash", "localMap1");
            return "ok";
        }
    
        @Benchmark
        @GroupThreads(4)
        public String testLocalCacheSAdd() {
            localCacheWrapper.sadd("slkfjskldfjsdklf", UUID.randomUUID().toString(), "localMap1");
            return "ok";
        }
    
        @Benchmark
        @GroupThreads(4)
        public String testLocalCacheSmember() {
            localCacheWrapper.smembers("my_benchmark_key_set", "localMap1");
            return "ok";
        }
    
        @Benchmark
        @GroupThreads(4)
        public String testLocalCacheZAdd() {
            localCacheWrapper.zadd(UUID.randomUUID().toString(), UUID.randomUUID().toString(), 100, "localMap1");
            return "ok";
        }
    
        @Benchmark
        @GroupThreads(4)
        public String testLocalCacheZRange() {
            localCacheWrapper.zrange("my_benchmark_key_zset", "start_val", "end_val", "localMap1");
            return "ok";
        }
    
        public static void main(String[] args) throws RunnerException {
            Options opt = new OptionsBuilder()
                    .include(localCacheBenchmark.class.getSimpleName())
                    .forks(1)
                    .build();
            new Runner(opt).run();
        }
    }

    BenchmarkMode(Mode.Throughput)设置,主要是为了测试方法的ops性能。

    OutputTimeUnit(TimeUnit.SECONDS) 设置,主要是以秒为单位进行输出,其实就是ops(operation per second)。

    Setup设置,主要是为了对进行benchmark的类进行初始化操作。 注意,要进行benchmark测试的类,必须使用带参构造注入方式来进行,不能使用@Resource或者@Autowired等方式来进行注入,否则运行起来的时候会报NullPointer Exception,因为jmh不支持这种方式。所谓的带参构造注入,就是形如下面的方式:

    public class OffheapCacheWrapper implements OffheapCacheStrategy {
    
        /**
         * 构造注入
         * @param offheapCacheBuilder
         */
        public OffheapCacheWrapper(OffheapCacheBuilder offheapCacheBuilder) {
            this.offheapCacheBuilder = offheapCacheBuilder;
        }
    
        /**
         * 缓存构建器
         */
        private OffheapCacheBuilder offheapCacheBuilder;
    }

    然后在使用的时候,就可以按照benchmark代码中的方式进行实例初始化了。

    Benchmark设置,主要是为了表明,此方法要进行测算。

    GroupThreads设置,主要是对当前方法使用的并发数,如果机器为4核,那么这个数设置为4是最合适的。这和我们本地开启4个多线程测试的原理是一样的。

    最后就是main方法了。每个benchmark测算类里面都要包含一个main的入口方法。入口方法的写法可以按照如上的写法进行书写即可。之后可以运行此main方法,就可以看到benchmark开始了,显示日志如下:

    # JMH version: 1.19
    # VM version: JDK 1.8.0_162, VM 25.162-b12
    # VM invoker: C:Program FilesJavajdk1.8.0_162jreinjava.exe
    # VM options: -Dvisualvm.id=95286622200089 -javaagent:D:softIntelliJ IDEA 2017.2.4libidea_rt.jar=51200:D:softIntelliJ IDEA 2017.2.4in -Dfile.encoding=UTF-8
    # Warmup: 20 iterations, 1 s each
    # Measurement: 20 iterations, 1 s each
    # Timeout: 10 min per iteration
    # Threads: 4 threads, will synchronize iterations
    # Benchmark mode: Throughput, ops/time
    # Benchmark: com.jd.limitbuy.common.cache.offheap.local.localCacheBenchmark.testLocalCacheGet
    
    # Run progress: 0.00% complete, ETA 00:06:00
    # Fork: 1 of 1
    # Warmup Iteration   1: 4626030.786 ops/s
    # Warmup Iteration   2: 5915177.466 ops/s
    # Warmup Iteration   3: 5250390.707 ops/s
    # Warmup Iteration   4: 5821984.889 ops/s
    # Warmup Iteration   5: 5878264.192 ops/s
    # Warmup Iteration   6: 5958235.775 ops/s
    # Warmup Iteration   7: 5872995.249 ops/s
    # Warmup Iteration   8: 5776545.647 ops/s
    # Warmup Iteration   9: 5698557.365 ops/s
    # Warmup Iteration  10: 5408015.908 ops/s
    # Warmup Iteration  11: 5369501.297 ops/s
    # Warmup Iteration  12: 5656644.350 ops/s
    # Warmup Iteration  13: 5927929.754 ops/s
    # Warmup Iteration  14: 4925956.931 ops/s
    # Warmup Iteration  15: 5073723.984 ops/s
    # Warmup Iteration  16: 5562728.644 ops/s
    # Warmup Iteration  17: 5404073.901 ops/s
    # Warmup Iteration  18: 5710289.068 ops/s
    # Warmup Iteration  19: 5279941.519 ops/s
    # Warmup Iteration  20: 5313558.528 ops/s
    Iteration   1: 5479700.075 ops/s
    Iteration   2: 5435900.429 ops/s
    Iteration   3: 5644384.753 ops/s
    Iteration   4: 5439492.270 ops/s
    Iteration   5: 4821232.721 ops/s
    Iteration   6: 5255550.541 ops/s
    Iteration   7: 5328415.572 ops/s
    Iteration   8: 5303100.251 ops/s
    Iteration   9: 5608949.378 ops/s
    Iteration  10: 5493709.321 ops/s
    Iteration  11: 5656755.883 ops/s
    Iteration  12: 5342198.063 ops/s
    Iteration  13: 5356092.929 ops/s
    Iteration  14: 5448346.884 ops/s
    Iteration  15: 5594615.720 ops/s
    Iteration  16: 5263648.663 ops/s
    Iteration  17: 5820217.743 ops/s
    Iteration  18: 3766476.832 ops/s
    Iteration  19: 5430792.407 ops/s
    Iteration  20: 5607185.081 ops/s
    
    
    Result "com.jd.limitbuy.common.cache.offheap.local.localCacheBenchmark.testLocalCacheGet":
      5354838.276 ±(99.9%) 371083.594 ops/s [Average]
      (min, avg, max) = (3766476.832, 5354838.276, 5820217.743), stdev = 427340.418
      CI (99.9%): [4983754.682, 5725921.870] (assumes normal distribution)

    上面就是testLocalCacheGet方法的完整benchmark效果,我们可以看到起了4个线程,遍历了20次,每次都有一个ops。 最后的统计部分可以看到ops的具体值和偏差部分。可以说非常详尽。

    当所有的方法都测算完毕之后,会汇总统计数据如下:

    Benchmark                                   Mode  Cnt        Score        Error  Units
    localCacheBenchmark.testLocalCacheGet      thrpt   20  5367451.445 ± 299325.857  ops/s
    localCacheBenchmark.testLocalCacheHGet     thrpt   20  1878476.142 ±  44977.163  ops/s
    localCacheBenchmark.testLocalCacheHGetAll  thrpt   20  2597442.245 ± 148661.259  ops/s
    localCacheBenchmark.testLocalCacheHSet     thrpt   20    39059.991 ±  60782.779  ops/s
    localCacheBenchmark.testLocalCacheSAdd     thrpt   20   231858.138 ±  80494.757  ops/s
    localCacheBenchmark.testLocalCacheSet      thrpt   20   168179.683 ± 145495.126  ops/s
    localCacheBenchmark.testLocalCacheSmember  thrpt   20  2650997.831 ±  97273.369  ops/s
    localCacheBenchmark.testLocalCacheZAdd     thrpt   20    56061.015 ±  73744.916  ops/s
    localCacheBenchmark.testLocalCacheZRange   thrpt   20  1682366.032 ±  75684.334  ops/s

    这样我们就可以评估每个方法的ops性能了。

    同样,如果想评估方法的tp50,tp99,tp999性能,只需要将BenchmarkMode改成Mode.AverageTime即可。非常方便。

    注意,如果你使用idea,需要下载jmh-plugin插件支持。

  • 相关阅读:
    C#中的global::system***命名空间别名限定符
    返回一个整数数组中最大子数组的和
    敏捷开发概述
    单词查找排序输出
    关于电梯调度的设计
    关于电梯调度的一些想法
    C#中抽象类和接口的区别
    SharePoint2010列表表单:用后台代码生成表单
    外刊IT评论:远离.net
    程序员:编程给你现实生活带来了哪些坏习惯
  • 原文地址:https://www.cnblogs.com/scy251147/p/9493009.html
Copyright © 2011-2022 走看看