HBase PerformanceEvaluation机制分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/bryce123phy/article/details/77905538

本文我们来分析一下hbase自带的测试工具——performanceEvaluation。该工具是hbase自带的性能压测工具,基本原因是是用多线程模拟多用户同时访问集群的情况。

运行hbase org.apache.hadoop.hbase.PerformanceEvaluation,可以看到关于performanceEvaluation的用法介绍,如下图中所示:



介绍一下关键的几个参数:
-nomapred,mo mapreduce,加入此参数表示采用本地多线程的方式去读写数据,默认不加,此时本地启动mapreduce任务方式去测试;
-table,待测试的标明;
-rows,在本地多线程读写的模式下,指定每个线程处理的数据行数;
-startRow,每个线程操作的起始数据key;
-compression,-blockEncoding,前者指明了压缩方式,默认是NONE,后者指定了block的encoding策略,默认也是none;
-writeToWAL,写入数据时的WLog落盘策略,包括SYNC_WAL和SKIP_WAL两种;
-multiGet,在RandomRead中,如果指定大于1,则一次返回多条数据,默认一次返回一条;
-inmemory,会将数据尽量放在内存中,默认是false,也即读操作是从磁盘返回,为了保证pe能够准确获取测量结果,建议保持为false;
-presplit和-splitPolicy,两者搭配使用,用于对测试表进行预切割;
-filterAll,这个参数我的理解是在测试scan功能时,加上此参数,则server端scan出来的结果不再返回给client端,用于单纯测试server端的性能;
performanceEvaluation提供了如下几种测试用例:



scanRange10,指定范围内(最多10个数内)的随机scan,其他与此相同,这里的startkey是totalRows之内随机生成的数,如下是startkey和endkey的生成代码:

protected Pair<byte[], byte[]> generateStartAndStopRows(int maxRange) {
          int start = this.rand.nextInt(Integer.MAX_VALUE) % opts.totalRows;
          int stop = start + maxRange;
          return new Pair<byte[],byte[]>(format(start), format(stop));
}
sequentialRead/sequentialWrite,顺序读测试,顺序写测试;
randomRead/randomWrite,随机读&随机写,读写的数据量是上文中每个线程发送的数据量,每次读写的rowkey是totalRows内随机生成的数;
scan,scan测试,与普通scan相同,读出所有的数据,testScan的核心代码如下所示:
void testRow(final int i) throws IOException {
      if (this.testScanner == null) {
        Scan scan = new Scan(format(opts.startRow));
        scan.setCaching(opts.caching);
        if (opts.addColumns) {
          scan.addColumn(FAMILY_NAME, QUALIFIER_NAME);
        } else {
          scan.addFamily(FAMILY_NAME);
        }
        if (opts.filterAll) {
          scan.setFilter(new FilterAllFilter());
        }
       this.testScanner = table.getScanner(scan);
      }
      Result r = testScanner.next();
      updateValueSize(r);
}
注意这里加入的FilterAllFilter,如果指定了filterAll,那么scan出来的结果并不会返回给客户端,而是直接shortCut返回。

上面介绍了performanceEvaluation的用法,那么hbase是如何设计pe的线程模型,使得它可以对server端的性能进行压测的呢,这样的设计又对我们平时设计代码有什么样的启示,接着我们分析一下performanceEvaluation的编程实现。

程序的入口在performanceEvaluation的main方法中,main方法很简单,代码就两行,构造一个PerformanceEvaluation对象并作为参数传入ToolRunner的run方法中,该run方法会解析用户通过终端传入的参数,变成String[]类型的参数传入cool内部实现的run方法,这里就是performanceEvaluation中的run方法。
 public int run(String[] args) throws Exception {
    try {
      LinkedList<String> argv = new LinkedList<String>();
      argv.addAll(Arrays.asList(args));
      TestOptions opts = parseOpts(argv); 

      //检查参数&检查client线程数

      Class<? extends Test> cmdClass = determineCommandClass(opts.cmdName);
      if (cmdClass != null) {
        runTest(cmdClass, opts);
        errCode = 0;
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    return errCode;
}
run方法首先根据传入的String[]构造Options,完成参数和线程数数量的简单检查后,执行runTest进行测试,runTest中首先会检查table的状态,如果命令传入的table descriptor与table自身的属性不同(主要是检查table的split分区是否与用户指定的相同),那么就会将table删除重建,紧接着会根据用户是否指定了nomapred以决定是启用本地多线程执行测试还是运行mapreduce任务执行测试,runTest的代码如下所示:
 private void runTest(final Class<? extends Test> cmd, TestOptions opts) throws IOException,
      InterruptedException, ClassNotFoundException {
    try(Connection conn = ConnectionFactory.createConnection(getConf());
        Admin admin = conn.getAdmin()) {
      checkTable(admin, opts);
    }
    if (opts.nomapred) {
      doLocalClients(opts, getConf());
    } else {
      doMapReduce(opts, getConf());
    }
}
doMapReduece就是构造出mr任务然后提交到yarn上去运行,这里不再详述。我们重点看doLocalClients是如何实现的。doLocalClients是一个比较典型的多线程运行模型,关键的代码我们列在下面:
static RunResult[] doLocalClients(final TestOptions opts, final Configuration conf)
      throws IOException, InterruptedException {
    final Class<? extends Test> cmd = determineCommandClass(opts.cmdName);
    assert cmd != null;
    @SuppressWarnings("unchecked")
    Future<RunResult>[] threads = new Future[opts.numClientThreads];
    RunResult[] results = new RunResult[opts.numClientThreads];
    ExecutorService pool = Executors.newFixedThreadPool(opts.numClientThreads,
           new ThreadFactoryBuilder().setNameFormat("TestClient-%s").build());
    final Connection con = ConnectionFactory.createConnection(conf);
    for (int i = 0; i < threads.length; i++) {
      final int index = i;
      threads[i] = pool.submit(new Callable<RunResult>() {
        @Override
        public RunResult call() throws Exception {
          TestOptions threadOpts = new TestOptions(opts);
          if (threadOpts.startRow == 0) threadOpts.startRow = index * threadOpts.perClientRunRows;
          RunResult run = runOneClient(cmd, conf, con, threadOpts, new Status() {
            @Override
            public void setStatus(final String msg) throws IOException {
              LOG.info(msg);
            }
          });
          return run;
        }
      });
    }
    pool.shutdown();

    for (int i = 0; i < threads.length; i++) {
      try {
        results[i] = threads[i].get();
      } catch (ExecutionException e) {
        throw new IOException(e.getCause());
      }
    }
    final String test = cmd.getSimpleName();
    Arrays.sort(results);
    long total = 0;
    for (RunResult result : results) {
      total += result.duration;
    }

    con.close();
    return results;
}
初始化了大小为numClientThreads的一个线程池pool,接着向线程池pool中通过submit投了numClientThreads个线程,这里多说一句,线程池的submit接口即可以接受Callable的线程也可以接受Runnable的线程,且无论是Callable还是Runnable为参数,调用submit之后,都可以返回一个Future对象,将来用户可以从Future中获知线程是否执行结束。

不同的是,Callable中的call方法可以返回一个结果,而Runnable的run则不能返回结果,如上面的代码所示,call方法中进一步调用了runOneClient,并将调用的结果包装为一个对象RunResult返回给客户端,客户端调用Future的get方法获得返回结果,根据RunResult的值计算avg、min&max等统计值。

重点看runOneClient的实现,runOneClient是每个单独线程所执行的方法,其中通过传入的参数反射构造了一个Test类,执行Test的test方法,也就是用户通过传入参数制定的test case用例,如ScanRange10、sequenceWrite等等,统计计算结果,并将各统计值构造一个RunResult类作为当前线程的返回结果返回给客户端。
static RunResult runOneClient(final Class<? extends Test> cmd, Configuration conf, Connection con,
                           TestOptions opts, final Status status) throws IOException, InterruptedException {
    long totalElapsedTime;
    final Test t;
    try {
      Constructor<? extends Test> constructor =
        cmd.getDeclaredConstructor(Connection.class, TestOptions.class, Status.class);
      t = constructor.newInstance(con, opts, status);
    } catch (NoSuchMethodException e) {
      //省略
    } catch (Exception e) {
      //省略
    }
    totalElapsedTime = t.test();
    return new RunResult(totalElapsedTime, t.getLatency());
}
这一部反射构造test case实例实在是精彩,Test是抽象类,每个不同的Test用例都是Test类的一个实现,实现的关键在于testRow方法,文章前面已经例举了两个不同测试用例的testRow方法实现,这里不再赘述。

hbase的performanceEvaluation虽然只有一个类,但是同样用到了多线程、反射等java程序设计中经典的方法,对于java学习者研究其中的设计技巧受益很大。

猜你喜欢

转载自blog.csdn.net/bryce123phy/article/details/77905538