From cf5a65c923c7f9b6428580c83f2ee5f4363eb0f6 Mon Sep 17 00:00:00 2001 From: Nick Dimiduk Date: Fri, 18 Oct 2013 16:06:43 -0700 Subject: [PATCH] HBASE-9806 Add PerfEval tool for BlockCache Initial sketch for a perf tool. $ ./bin/hbase org.apache.hadoop.hbase.BlockCachePerformanceEvaluation --help usage: bin/hbase org.apache.hadoop.hbase.BlockCachePerformanceEvaluation Options: -b,--blocksize Override the default blocksize (65536). -h,--help Show usage -i,--iterations The number of times to run a test (default: 100). -s,--seed Specify a seed value for the random generator. --- .../hbase/BlockCachePerformanceEvaluation.java | 445 +++++++++++++++++++++ 1 file changed, 445 insertions(+) create mode 100644 hbase-server/src/test/java/org/apache/hadoop/hbase/BlockCachePerformanceEvaluation.java diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/BlockCachePerformanceEvaluation.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/BlockCachePerformanceEvaluation.java new file mode 100644 index 0000000..ebc0ff1 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/BlockCachePerformanceEvaluation.java @@ -0,0 +1,445 @@ +/** + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryManagerMXBean; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.commons.math.random.RandomData; +import org.apache.commons.math.random.RandomDataImpl; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hbase.io.hfile.BlockCache; +import org.apache.hadoop.hbase.io.hfile.CacheConfig; +import org.apache.hadoop.hbase.io.hfile.HFile; +import org.apache.hadoop.hbase.io.hfile.HFileContext; +import org.apache.hadoop.hbase.io.hfile.HFileContextBuilder; +import org.apache.hadoop.hbase.io.hfile.HFileScanner; +import org.apache.hadoop.hbase.util.AbstractHBaseTool; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.util.StringUtils; + +/** + * This class runs performance benchmarks for {@link BlockCache}. + */ +public class BlockCachePerformanceEvaluation extends AbstractHBaseTool { + + private static final int ROW_LENGTH = 1000; + private static final int DEFAULT_ITERATIONS = 100; + static final Log LOG = LogFactory.getLog(BlockCachePerformanceEvaluation.class.getName()); + + private int blockSize; + private int iterations; + private RandomDataImpl rand; + private Map results; + + static byte [] format(final int i) { + String v = Integer.toString(i); + return Bytes.toBytes("0000000000".substring(v.length()) + v); + } + + protected void logMemoryStats() { + MemoryMXBean memStats = ManagementFactory.getMemoryMXBean(); + long totalHeap = memStats.getHeapMemoryUsage().getMax(); + long totalOffHeap = memStats.getNonHeapMemoryUsage().getMax(); + + LOG.info("Memory stats [" + + " totalHeap: " + StringUtils.humanReadableInt(totalHeap) + + " (" + totalHeap + ")" + + " totalOffHeap: " + StringUtils.humanReadableInt(totalOffHeap) + + " (" + totalOffHeap + ")" + + "]"); + } + + @Override + public int doWork() throws Exception { + if (results == null) { + results = new HashMap(); + } + + // populate the cache while data is generated. + conf.setBoolean("hbase.rs.cacheblocksonwrite", true); + final FileSystem fs = FileSystem.get(conf); + final Path mf = fs.makeQualified(new Path("performanceevaluation.blockcache")); + try { + if (fs.exists(mf)) { + fs.delete(mf, true); + } + + // populate the cache + final SequentialWriteBenchmark wb = + new SequentialWriteBenchmark(conf, mf, rand, blockSize); + wb.run(); + int rowsWritten = wb.getRowsWritten(); + + RowOrientedBenchmark[] suite = new RowOrientedBenchmark[] { + new UniformRandomSmallScanBenchmark(conf, mf, rand, rowsWritten), + new UniformRandomReadBenchmark(conf, mf, rand, rowsWritten), + new GaussianRandomReadBenchmark(conf, mf, rand, rowsWritten), + new SequentialReadBenchmark(conf, mf, rand, rowsWritten), + }; + + for (final RowOrientedBenchmark b : suite) { + runBenchmark(b); + } + } finally { + if (fs.exists(mf)) { + fs.delete(mf, true); + } + } + + for (Map.Entry e : results.entrySet()) { + BigDecimal sum = BigDecimal.ZERO; + for (long val : e.getValue()) sum = sum.add(BigDecimal.valueOf(val)); + BigDecimal avg = sum.divide(BigDecimal.valueOf(e.getValue().length)); + StringBuilder msg = new StringBuilder() + .append(e.getKey()).append(": ") + .append(Arrays.toString(e.getValue())) + .append(", avg: ").append(avg).append(" ms."); + LOG.info(msg); + } + return 0; + } + + protected void runBenchmark(RowOrientedBenchmark benchmark) throws Exception { + String name = benchmark.getClass().getSimpleName(); + if (!results.containsKey(name)) { + results.put(name, new long[iterations]); + } + + LOG.info("Running " + name + " " + iterations + " time(s)."); + for (int i = 0; i < iterations; i++) { + long elapsedTime = benchmark.run(); + results.get(name)[i] = elapsedTime; + LOG.debug(name + "[" + (i + 1) + "/" + iterations + "] took " + elapsedTime + "ms."); + } + } + + static abstract class RowOrientedBenchmark { + + protected static final int LOG_INTERVAL = 100 * 1000; + + protected final Configuration conf; + protected final Path mf; + protected final RandomData rand; + protected final CacheConfig cacheConfig; + protected final BlockCache cache; + + public RowOrientedBenchmark(Configuration conf, Path mf, RandomData rand) { + this.conf = conf; + this.mf = mf; + this.rand = rand; + this.cacheConfig = new CacheConfig(this.conf); + this.cache = this.cacheConfig.getBlockCache(); + } + + protected void logCacheStats() { + if (LOG.isDebugEnabled()) { + LOG.debug("BlockCache stats [" + + " currentSize: " + StringUtils.humanReadableInt(cache.getCurrentSize()) + + " (" + cache.getCurrentSize() + ")" + + " freeSize: " + StringUtils.humanReadableInt(cache.getFreeSize()) + + " (" + cache.getFreeSize() + ")" + + " size: " + cache.size() + + " blockCount: " + cache.getBlockCount() + + " evictedCount: " + cache.getEvictedCount() + + "]"); + } + } + + void setUp() throws Exception { + logCacheStats(); + } + + abstract void doRow(int i) throws Exception; + + void tearDown() throws Exception { + logCacheStats(); + } + + /** Run benchmark */ + abstract long run() throws Exception; + } + + /** Use me to populate the cache */ + static class SequentialWriteBenchmark extends RowOrientedBenchmark { + protected HFile.Writer writer; + private byte[] bytes = new byte[ROW_LENGTH]; + private int blockSize; + private int rowsWritten = -1; + + public SequentialWriteBenchmark(Configuration conf, Path mf, RandomData rand, int blockSize) { + super(conf, mf, rand); + this.blockSize = blockSize; + } + + @Override + void setUp() throws Exception { + super.setUp(); + FileSystem fs = FileSystem.get(conf); + HFileContext hFileContext = new HFileContextBuilder().withBlockSize(blockSize).build(); + writer = + HFile.getWriterFactory(conf, cacheConfig) + .withPath(fs, mf) + .withFileContext(hFileContext) + .withComparator(new KeyValue.RawBytesComparator()) + .create(); + } + + /** Run benchmark */ + @Override + long run() throws Exception { + long elapsedTime; + setUp(); + int i = 0; + long startTime = System.currentTimeMillis(); + try { + // write blocks until cache is full. TODO: this is an imperfect approximation. + while (cache.getEvictedCount() <= 0) { + if (i > 0 && i % LOG_INTERVAL == 0) { + LOG.debug("Processed " + i + " rows."); + } + doRow(i++); + assert i >= 0 : "rowcounter overflow!"; + } + elapsedTime = System.currentTimeMillis() - startTime; + } finally { + this.rowsWritten = i; + tearDown(); + } + return elapsedTime; + } + + @Override + void doRow(int i) throws Exception { + writer.append(format(i), generateValue()); + } + + private byte[] generateValue() { + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) rand.nextInt(0, Byte.MAX_VALUE); + } + return bytes; + } + + @Override + void tearDown() throws Exception { + writer.close(); + super.tearDown(); + } + + public int getRowsWritten() { + assert rowsWritten > 0 : "Test hasn't been run yet."; + return rowsWritten; + } + } + + static abstract class ReadBenchmark extends RowOrientedBenchmark { + + protected final int totalRows; + protected HFile.Reader reader; + + public ReadBenchmark(Configuration conf, Path mf, RandomData rand, int totalRows) { + super(conf, mf, rand); + this.totalRows = totalRows; + } + + @Override + void setUp() throws Exception { + super.setUp(); + FileSystem fs = FileSystem.get(conf); + reader = HFile.createReader(fs, mf, cacheConfig); + this.reader.loadFileInfo(); + } + + long run() throws Exception { + long elapsedTime; + setUp(); + long startTime = System.currentTimeMillis(); + try { + for (int i = 0; i < totalRows; i++) { + if (i > 0 && i % LOG_INTERVAL == 0) { + LOG.debug("Processed " + StringUtils.humanReadableInt(i) + " rows."); + } + doRow(i); + } + elapsedTime = System.currentTimeMillis() - startTime; + } finally { + tearDown(); + } + return elapsedTime; + } + + @Override + void tearDown() throws Exception { + reader.close(); + super.tearDown(); + } + } + + static class SequentialReadBenchmark extends ReadBenchmark { + private HFileScanner scanner; + + public SequentialReadBenchmark(Configuration conf, Path mf, RandomData rand, int totalRows) { + super(conf, mf, rand, totalRows); + } + + @Override + void setUp() throws Exception { + super.setUp(); + this.scanner = this.reader.getScanner(true, false); + this.scanner.seekTo(); + } + + @Override + void doRow(int i) throws Exception { + if (this.scanner.next()) { + ByteBuffer k = this.scanner.getKey(); + PerformanceEvaluationCommons.assertKey(format(i + 1), k); + ByteBuffer v = scanner.getValue(); + PerformanceEvaluationCommons.assertValueSize(v.limit(), ROW_LENGTH); + } + } + } + + static class UniformRandomReadBenchmark extends ReadBenchmark { + + public UniformRandomReadBenchmark(Configuration conf, Path mf, RandomData rand, int totalRows) { + super(conf, mf, rand, totalRows); + } + + @Override + void doRow(int i) throws Exception { + HFileScanner scanner = this.reader.getScanner(true, true); + byte [] b = getRandomRow(); + if (scanner.seekTo(b) < 0) { + LOG.info("Not able to seekTo " + new String(b)); + return; + } + ByteBuffer k = scanner.getKey(); + PerformanceEvaluationCommons.assertKey(b, k); + ByteBuffer v = scanner.getValue(); + PerformanceEvaluationCommons.assertValueSize(v.limit(), ROW_LENGTH); + } + + private byte [] getRandomRow() { + return format(rand.nextInt(0, totalRows - 1)); + } + } + + static class UniformRandomSmallScanBenchmark extends ReadBenchmark { + + public UniformRandomSmallScanBenchmark(Configuration conf, Path mf, RandomData rand, int totalRows) { + super(conf, mf, rand, totalRows / 10); + } + + @Override + void doRow(int i) throws Exception { + HFileScanner scanner = this.reader.getScanner(true, false); + byte [] b = getRandomRow(); + if (scanner.seekTo(b) != 0) { + LOG.info("Nonexistent row: " + new String(b)); + return; + } + ByteBuffer k = scanner.getKey(); + PerformanceEvaluationCommons.assertKey(b, k); + LOG.trace("Found row: " + new String(b)); + for (int ii = 0; ii < 30; ii++) { + if (!scanner.next()) { + LOG.info("NOTHING FOLLOWS"); + return; + } + ByteBuffer v = scanner.getValue(); + PerformanceEvaluationCommons.assertValueSize(v.limit(), ROW_LENGTH); + } + } + + private byte [] getRandomRow() { + return format(rand.nextInt(0, totalRows - 1)); + } + } + + static class GaussianRandomReadBenchmark extends ReadBenchmark { + + private RandomData randomData = new RandomDataImpl(); + + public GaussianRandomReadBenchmark(Configuration conf, Path mf, RandomData rand, int totalRows) { + super(conf, mf, rand, totalRows); + } + + @Override + void doRow(int i) throws Exception { + HFileScanner scanner = this.reader.getScanner(false, true); + byte[] gaussianRandomRowBytes = getGaussianRandomRowBytes(); + if (scanner.seekTo(gaussianRandomRowBytes) < 0) { + LOG.info("Not able to seekTo " + new String(gaussianRandomRowBytes)); + return; + } + for (int ii = 0; ii < 30; ii++) { + if (!scanner.next()) { + LOG.info("NOTHING FOLLOWS"); + return; + } + scanner.getKey(); + scanner.getValue(); + } + } + + private byte [] getGaussianRandomRowBytes() { + int r = (int) randomData.nextGaussian((double) totalRows / 2.0, (double) totalRows / 10.0); + return format(r); + } + } + + @Override + protected void addOptions() { + addOptWithArg("i", "iterations", + "The number of times to run a test (default: " + DEFAULT_ITERATIONS + ")."); + addOptWithArg("b", "blocksize", + "Override the default blocksize (" + HConstants.DEFAULT_BLOCKSIZE + ")."); + addOptWithArg("s", "seed", "Specify a seed value for the random generator."); + } + + @Override + protected void processOptions(CommandLine cmd) { + this.iterations = + Integer.parseInt(cmd.getOptionValue("iterations", "" + DEFAULT_ITERATIONS)); + this.blockSize = + Integer.parseInt(cmd.getOptionValue("blocksize", "" + HConstants.DEFAULT_BLOCKSIZE)); + long seed = Long.parseLong(cmd.getOptionValue("seed", "" + System.currentTimeMillis())); + this.rand = new RandomDataImpl(); + this.rand.reSeed(seed); + LOG.info("Using random seed: " + seed); + } + + public static void main(String[] args) throws Exception { + new BlockCachePerformanceEvaluation().doStaticMain(args); + } +} -- 1.8.3.4