View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with this
4    * work for additional information regarding copyright ownership. The ASF
5    * licenses this file to you under the Apache License, Version 2.0 (the
6    * "License"); you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14   * License for the specific language governing permissions and limitations
15   * under the License.
16   */
17  package org.apache.hadoop.hbase.regionserver;
18  
19  import java.io.IOException;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.Collections;
23  import java.util.List;
24  import java.util.Random;
25  import java.util.SortedSet;
26  import java.util.concurrent.Callable;
27  import java.util.concurrent.ConcurrentSkipListSet;
28  import java.util.concurrent.ExecutionException;
29  import java.util.concurrent.ExecutorCompletionService;
30  import java.util.concurrent.ExecutorService;
31  import java.util.concurrent.Executors;
32  import java.util.concurrent.Future;
33  import java.util.concurrent.TimeUnit;
34  import java.util.concurrent.atomic.AtomicLong;
35  
36  import org.apache.commons.cli.CommandLine;
37  import org.apache.commons.cli.CommandLineParser;
38  import org.apache.commons.cli.HelpFormatter;
39  import org.apache.commons.cli.Option;
40  import org.apache.commons.cli.OptionGroup;
41  import org.apache.commons.cli.Options;
42  import org.apache.commons.cli.ParseException;
43  import org.apache.commons.cli.PosixParser;
44  import org.apache.commons.logging.Log;
45  import org.apache.commons.logging.LogFactory;
46  import org.apache.hadoop.conf.Configuration;
47  import org.apache.hadoop.fs.FileSystem;
48  import org.apache.hadoop.fs.Path;
49  import org.apache.hadoop.hbase.HBaseConfiguration;
50  import org.apache.hadoop.hbase.HColumnDescriptor;
51  import org.apache.hadoop.hbase.HRegionInfo;
52  import org.apache.hadoop.hbase.HTableDescriptor;
53  import org.apache.hadoop.hbase.KeyValue;
54  import org.apache.hadoop.hbase.TableName;
55  import org.apache.hadoop.hbase.client.Scan;
56  import org.apache.hadoop.hbase.io.compress.Compression;
57  import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding;
58  import org.apache.hadoop.hbase.io.hfile.BlockCache;
59  import org.apache.hadoop.hbase.io.hfile.CacheConfig;
60  import org.apache.hadoop.hbase.io.hfile.HFile;
61  import org.apache.hadoop.hbase.io.hfile.HFileDataBlockEncoder;
62  import org.apache.hadoop.hbase.io.hfile.HFileDataBlockEncoderImpl;
63  import org.apache.hadoop.hbase.io.hfile.HFilePrettyPrinter;
64  import org.apache.hadoop.hbase.io.hfile.NoOpDataBlockEncoder;
65  import org.apache.hadoop.hbase.util.Bytes;
66  import org.apache.hadoop.hbase.util.LoadTestTool;
67  import org.apache.hadoop.hbase.util.MD5Hash;
68  import org.apache.hadoop.util.StringUtils;
69  
70  /**
71   * Tests HFile read/write workloads, such as merging HFiles and random reads.
72   */
73  public class HFileReadWriteTest {
74  
75    private static final String TABLE_NAME = "MyTable";
76  
77    private static enum Workload {
78      MERGE("merge", "Merge the specified HFiles", 1, Integer.MAX_VALUE),
79      RANDOM_READS("read", "Perform a random read benchmark on the given HFile",
80          1, 1);
81  
82      private String option;
83      private String description;
84  
85      public final int minNumInputFiles;
86      public final int maxNumInputFiles;
87  
88      Workload(String option, String description, int minNumInputFiles,
89          int maxNumInputFiles) {
90        this.option = option;
91        this.description = description;
92        this.minNumInputFiles = minNumInputFiles;
93        this.maxNumInputFiles = maxNumInputFiles;
94      }
95  
96      static OptionGroup getOptionGroup() {
97        OptionGroup optionGroup = new OptionGroup();
98        for (Workload w : values())
99          optionGroup.addOption(new Option(w.option, w.description));
100       return optionGroup;
101     }
102 
103     private static String getOptionListStr() {
104       StringBuilder sb = new StringBuilder();
105       for (Workload w : values()) {
106         if (sb.length() > 0)
107           sb.append(", ");
108         sb.append("-" + w.option);
109       }
110       return sb.toString();
111     }
112 
113     static Workload fromCmdLine(CommandLine cmdLine) {
114       for (Workload w : values()) {
115         if (cmdLine.hasOption(w.option))
116           return w;
117       }
118       LOG.error("No workload specified. Specify one of the options: " +
119           getOptionListStr());
120       return null;
121     }
122 
123     public String onlyUsedFor() {
124       return ". Only used for the " + this + " workload.";
125     }
126   }
127 
128   private static final String OUTPUT_DIR_OPTION = "output_dir";
129   private static final String COMPRESSION_OPTION = "compression";
130   private static final String BLOOM_FILTER_OPTION = "bloom";
131   private static final String BLOCK_SIZE_OPTION = "block_size";
132   private static final String DURATION_OPTION = "duration";
133   private static final String NUM_THREADS_OPTION = "num_threads";
134 
135   private static final Log LOG = LogFactory.getLog(HFileReadWriteTest.class);
136 
137   private Workload workload;
138   private FileSystem fs;
139   private Configuration conf;
140   private CacheConfig cacheConf;
141   private List<String> inputFileNames;
142   private Path outputDir;
143   private int numReadThreads;
144   private int durationSec;
145   private DataBlockEncoding dataBlockEncoding;
146   private boolean encodeInCacheOnly;
147   private HFileDataBlockEncoder dataBlockEncoder =
148       NoOpDataBlockEncoder.INSTANCE;
149 
150   private BloomType bloomType = BloomType.NONE;
151   private int blockSize;
152   private Compression.Algorithm compression = Compression.Algorithm.NONE;
153 
154   private byte[] firstRow, lastRow;
155 
156   private AtomicLong numSeeks = new AtomicLong();
157   private AtomicLong numKV = new AtomicLong();
158   private AtomicLong totalBytes = new AtomicLong();
159 
160   private byte[] family;
161 
162   private long endTime = Long.MAX_VALUE;
163 
164   private SortedSet<String> keysRead = new ConcurrentSkipListSet<String>();
165   private List<StoreFile> inputStoreFiles;
166 
167   public HFileReadWriteTest() {
168     conf = HBaseConfiguration.create();
169     cacheConf = new CacheConfig(conf);
170   }
171 
172   @SuppressWarnings("unchecked")
173   public boolean parseOptions(String args[]) {
174 
175     Options options = new Options();
176     options.addOption(OUTPUT_DIR_OPTION, true, "Output directory" +
177         Workload.MERGE.onlyUsedFor());
178     options.addOption(COMPRESSION_OPTION, true, " Compression type, one of "
179         + Arrays.toString(Compression.Algorithm.values()) +
180         Workload.MERGE.onlyUsedFor());
181     options.addOption(BLOOM_FILTER_OPTION, true, "Bloom filter type, one of "
182         + Arrays.toString(BloomType.values()) +
183         Workload.MERGE.onlyUsedFor());
184     options.addOption(BLOCK_SIZE_OPTION, true, "HFile block size" +
185         Workload.MERGE.onlyUsedFor());
186     options.addOption(DURATION_OPTION, true, "The amount of time to run the " +
187         "random read workload for" + Workload.RANDOM_READS.onlyUsedFor());
188     options.addOption(NUM_THREADS_OPTION, true, "The number of random " +
189         "reader threads" + Workload.RANDOM_READS.onlyUsedFor());
190     options.addOption(NUM_THREADS_OPTION, true, "The number of random " +
191         "reader threads" + Workload.RANDOM_READS.onlyUsedFor());
192     options.addOption(LoadTestTool.OPT_DATA_BLOCK_ENCODING, true,
193         LoadTestTool.OPT_DATA_BLOCK_ENCODING_USAGE);
194     options.addOption(LoadTestTool.OPT_ENCODE_IN_CACHE_ONLY, false,
195         LoadTestTool.OPT_ENCODE_IN_CACHE_ONLY_USAGE);
196     options.addOptionGroup(Workload.getOptionGroup());
197 
198     if (args.length == 0) {
199       HelpFormatter formatter = new HelpFormatter();
200       formatter.printHelp(HFileReadWriteTest.class.getSimpleName(),
201           options, true);
202       return false;
203     }
204 
205     CommandLineParser parser = new PosixParser();
206     CommandLine cmdLine;
207     try {
208       cmdLine = parser.parse(options, args);
209     } catch (ParseException ex) {
210       LOG.error(ex);
211       return false;
212     }
213 
214     workload = Workload.fromCmdLine(cmdLine);
215     if (workload == null)
216       return false;
217 
218     inputFileNames = (List<String>) cmdLine.getArgList();
219 
220     if (inputFileNames.size() == 0) {
221       LOG.error("No input file names specified");
222       return false;
223     }
224 
225     if (inputFileNames.size() < workload.minNumInputFiles) {
226       LOG.error("Too few input files: at least " + workload.minNumInputFiles +
227           " required");
228       return false;
229     }
230 
231     if (inputFileNames.size() > workload.maxNumInputFiles) {
232       LOG.error("Too many input files: at most " + workload.minNumInputFiles +
233           " allowed");
234       return false;
235     }
236 
237     if (cmdLine.hasOption(COMPRESSION_OPTION)) {
238       compression = Compression.Algorithm.valueOf(
239           cmdLine.getOptionValue(COMPRESSION_OPTION));
240     }
241 
242     if (cmdLine.hasOption(BLOOM_FILTER_OPTION)) {
243       bloomType = BloomType.valueOf(cmdLine.getOptionValue(
244           BLOOM_FILTER_OPTION));
245     }
246 
247     encodeInCacheOnly =
248         cmdLine.hasOption(LoadTestTool.OPT_ENCODE_IN_CACHE_ONLY);
249 
250     if (cmdLine.hasOption(LoadTestTool.OPT_DATA_BLOCK_ENCODING)) {
251       dataBlockEncoding = DataBlockEncoding.valueOf(
252           cmdLine.getOptionValue(LoadTestTool.OPT_DATA_BLOCK_ENCODING));
253       // Optionally encode on disk, always encode in cache.
254       dataBlockEncoder = new HFileDataBlockEncoderImpl(
255           encodeInCacheOnly ? DataBlockEncoding.NONE : dataBlockEncoding,
256           dataBlockEncoding);
257     } else {
258       if (encodeInCacheOnly) {
259         LOG.error("The -" + LoadTestTool.OPT_ENCODE_IN_CACHE_ONLY +
260             " option does not make sense without -" +
261             LoadTestTool.OPT_DATA_BLOCK_ENCODING);
262         return false;
263       }
264     }
265 
266     blockSize = conf.getInt("hfile.min.blocksize.size", 65536);
267     if (cmdLine.hasOption(BLOCK_SIZE_OPTION))
268       blockSize = Integer.valueOf(cmdLine.getOptionValue(BLOCK_SIZE_OPTION));
269 
270     if (workload == Workload.MERGE) {
271       String outputDirStr = cmdLine.getOptionValue(OUTPUT_DIR_OPTION);
272       if (outputDirStr == null) {
273         LOG.error("Output directory is not specified");
274         return false;
275       }
276       outputDir = new Path(outputDirStr);
277       // Will be checked for existence in validateConfiguration.
278     }
279 
280     if (workload == Workload.RANDOM_READS) {
281       if (!requireOptions(cmdLine, new String[] { DURATION_OPTION,
282           NUM_THREADS_OPTION })) {
283         return false;
284       }
285 
286       durationSec = Integer.parseInt(cmdLine.getOptionValue(DURATION_OPTION));
287       numReadThreads = Integer.parseInt(
288           cmdLine.getOptionValue(NUM_THREADS_OPTION));
289     }
290 
291     Collections.sort(inputFileNames);
292 
293     return true;
294   }
295 
296   /** @return true if all the given options are specified */
297   private boolean requireOptions(CommandLine cmdLine,
298       String[] requiredOptions) {
299     for (String option : requiredOptions)
300       if (!cmdLine.hasOption(option)) {
301         LOG.error("Required option -" + option + " not specified");
302         return false;
303       }
304     return true;
305   }
306 
307   public boolean validateConfiguration() throws IOException {
308     fs = FileSystem.get(conf);
309 
310     for (String inputFileName : inputFileNames) {
311       Path path = new Path(inputFileName);
312       if (!fs.exists(path)) {
313         LOG.error("File " + inputFileName + " does not exist");
314         return false;
315       }
316 
317       if (fs.getFileStatus(path).isDir()) {
318         LOG.error(inputFileName + " is a directory");
319         return false;
320       }
321     }
322 
323     if (outputDir != null &&
324         (!fs.exists(outputDir) || !fs.getFileStatus(outputDir).isDir())) {
325       LOG.error(outputDir.toString() + " does not exist or is not a " +
326           "directory");
327       return false;
328     }
329 
330     return true;
331   }
332 
333   public void runMergeWorkload() throws IOException {
334     long maxKeyCount = prepareForMerge();
335 
336     List<StoreFileScanner> scanners =
337         StoreFileScanner.getScannersForStoreFiles(inputStoreFiles, false,
338             false);
339 
340     HColumnDescriptor columnDescriptor = new HColumnDescriptor(
341         HFileReadWriteTest.class.getSimpleName());
342     columnDescriptor.setBlocksize(blockSize);
343     columnDescriptor.setBloomFilterType(bloomType);
344     columnDescriptor.setCompressionType(compression);
345     columnDescriptor.setDataBlockEncoding(dataBlockEncoding);
346     HRegionInfo regionInfo = new HRegionInfo();
347     HTableDescriptor htd = new HTableDescriptor(TableName.valueOf(TABLE_NAME));
348     HRegion region = new HRegion(outputDir, null, fs, conf, regionInfo, htd, null);
349     HStore store = new HStore(region, columnDescriptor, conf);
350 
351     StoreFile.Writer writer = store.createWriterInTmp(maxKeyCount, compression, false, true);
352 
353     StatisticsPrinter statsPrinter = new StatisticsPrinter();
354     statsPrinter.startThread();
355 
356     try {
357       performMerge(scanners, store, writer);
358       writer.close();
359     } finally {
360       statsPrinter.requestStop();
361     }
362 
363     Path resultPath = writer.getPath();
364 
365     resultPath = tryUsingSimpleOutputPath(resultPath);
366 
367     long fileSize = fs.getFileStatus(resultPath).getLen();
368     LOG.info("Created " + resultPath + ", size " + fileSize);
369 
370     System.out.println();
371     System.out.println("HFile information for " + resultPath);
372     System.out.println();
373 
374     HFilePrettyPrinter hfpp = new HFilePrettyPrinter();
375     hfpp.run(new String[] { "-m", "-f", resultPath.toString() });
376   }
377 
378   private Path tryUsingSimpleOutputPath(Path resultPath) throws IOException {
379     if (inputFileNames.size() == 1) {
380       // In case of only one input set output to be consistent with the
381       // input name.
382 
383       Path inputPath = new Path(inputFileNames.get(0));
384       Path betterOutputPath = new Path(outputDir,
385           inputPath.getName());
386       if (!fs.exists(betterOutputPath)) {
387         fs.rename(resultPath, betterOutputPath);
388         resultPath = betterOutputPath;
389       }
390     }
391     return resultPath;
392   }
393 
394   private void performMerge(List<StoreFileScanner> scanners, HStore store,
395       StoreFile.Writer writer) throws IOException {
396     InternalScanner scanner = null;
397     try {
398       Scan scan = new Scan();
399 
400       // Include deletes
401       scanner = new StoreScanner(store, store.getScanInfo(), scan, scanners,
402           ScanType.COMPACT_DROP_DELETES, Long.MIN_VALUE, Long.MIN_VALUE);
403 
404       ArrayList<KeyValue> kvs = new ArrayList<KeyValue>();
405 
406       while (scanner.next(kvs) || kvs.size() != 0) {
407         numKV.addAndGet(kvs.size());
408         for (KeyValue kv : kvs) {
409           totalBytes.addAndGet(kv.getLength());
410           writer.append(kv);
411         }
412         kvs.clear();
413       }
414     } finally {
415       if (scanner != null)
416         scanner.close();
417     }
418   }
419 
420   /**
421    * @return the total key count in the files being merged
422    * @throws IOException
423    */
424   private long prepareForMerge() throws IOException {
425     LOG.info("Merging " + inputFileNames);
426     LOG.info("Using block size: " + blockSize);
427     inputStoreFiles = new ArrayList<StoreFile>();
428 
429     long maxKeyCount = 0;
430     for (String fileName : inputFileNames) {
431       Path filePath = new Path(fileName);
432 
433       // Open without caching.
434       StoreFile sf = openStoreFile(filePath, false);
435       sf.createReader();
436       inputStoreFiles.add(sf);
437 
438       StoreFile.Reader r = sf.getReader();
439       if (r != null) {
440         long keyCount = r.getFilterEntries();
441         maxKeyCount += keyCount;
442         LOG.info("Compacting: " + sf + "; keyCount = " + keyCount
443             + "; Bloom Type = " + r.getBloomFilterType().toString()
444             + "; Size = " + StringUtils.humanReadableInt(r.length()));
445       }
446     }
447     return maxKeyCount;
448   }
449 
450   public HFile.Reader[] getHFileReaders() {
451     HFile.Reader readers[] = new HFile.Reader[inputStoreFiles.size()];
452     for (int i = 0; i < inputStoreFiles.size(); ++i)
453       readers[i] = inputStoreFiles.get(i).getReader().getHFileReader();
454     return readers;
455   }
456 
457   private StoreFile openStoreFile(Path filePath, boolean blockCache)
458       throws IOException {
459     // We are passing the ROWCOL Bloom filter type, but StoreFile will still
460     // use the Bloom filter type specified in the HFile.
461     return new StoreFile(fs, filePath, conf, cacheConf,
462         BloomType.ROWCOL, dataBlockEncoder);
463   }
464 
465   public static int charToHex(int c) {
466     if ('0' <= c && c <= '9')
467       return c - '0';
468     if ('a' <= c && c <= 'f')
469       return 10 + c - 'a';
470     return -1;
471   }
472 
473   public static int hexToChar(int h) {
474     h &= 0xff;
475     if (0 <= h && h <= 9)
476       return '0' + h;
477     if (10 <= h && h <= 15)
478       return 'a' + h - 10;
479     return -1;
480   }
481 
482   public static byte[] createRandomRow(Random rand, byte[] first, byte[] last)
483   {
484     int resultLen = Math.max(first.length, last.length);
485     int minLen = Math.min(first.length, last.length);
486     byte[] result = new byte[resultLen];
487     boolean greaterThanFirst = false;
488     boolean lessThanLast = false;
489 
490     for (int i = 0; i < resultLen; ++i) {
491       // Generate random hex characters if both first and last row are hex
492       // at this position.
493       boolean isHex = i < minLen && charToHex(first[i]) != -1
494           && charToHex(last[i]) != -1;
495 
496       // If our key is already greater than the first key, we can use
497       // arbitrarily low values.
498       int low = greaterThanFirst || i >= first.length ? 0 : first[i] & 0xff;
499 
500       // If our key is already less than the last key, we can use arbitrarily
501       // high values.
502       int high = lessThanLast || i >= last.length ? 0xff : last[i] & 0xff;
503 
504       // Randomly select the next byte between the lowest and the highest
505       // value allowed for this position. Restrict to hex characters if
506       // necessary. We are generally biased towards border cases, which is OK
507       // for test.
508 
509       int r;
510       if (isHex) {
511         // Use hex chars.
512         if (low < '0')
513           low = '0';
514 
515         if (high > 'f')
516           high = 'f';
517 
518         int lowHex = charToHex(low);
519         int highHex = charToHex(high);
520         r = hexToChar(lowHex + rand.nextInt(highHex - lowHex + 1));
521       } else {
522         r = low + rand.nextInt(high - low + 1);
523       }
524 
525       if (r > low)
526         greaterThanFirst = true;
527 
528       if (r < high)
529         lessThanLast = true;
530 
531       result[i] = (byte) r;
532     }
533 
534     if (Bytes.compareTo(result, first) < 0) {
535       throw new IllegalStateException("Generated key " +
536           Bytes.toStringBinary(result) + " is less than the first key " +
537           Bytes.toStringBinary(first));
538     }
539 
540     if (Bytes.compareTo(result, last) > 0) {
541       throw new IllegalStateException("Generated key " +
542           Bytes.toStringBinary(result) + " is greater than te last key " +
543           Bytes.toStringBinary(last));
544     }
545 
546     return result;
547   }
548 
549   private static byte[] createRandomQualifier(Random rand) {
550     byte[] q = new byte[10 + rand.nextInt(30)];
551     rand.nextBytes(q);
552     return q;
553   }
554 
555   private class RandomReader implements Callable<Boolean> {
556 
557     private int readerId;
558     private StoreFile.Reader reader;
559     private boolean pread;
560 
561     public RandomReader(int readerId, StoreFile.Reader reader,
562         boolean pread)
563     {
564       this.readerId = readerId;
565       this.reader = reader;
566       this.pread = pread;
567     }
568 
569     @Override
570     public Boolean call() throws Exception {
571       Thread.currentThread().setName("reader " + readerId);
572       Random rand = new Random();
573       StoreFileScanner scanner = reader.getStoreFileScanner(true, pread);
574 
575       while (System.currentTimeMillis() < endTime) {
576         byte[] row = createRandomRow(rand, firstRow, lastRow);
577         KeyValue kvToSeek = new KeyValue(row, family,
578             createRandomQualifier(rand));
579         if (rand.nextDouble() < 0.0001) {
580           LOG.info("kvToSeek=" + kvToSeek);
581         }
582         boolean seekResult;
583         try {
584           seekResult = scanner.seek(kvToSeek);
585         } catch (IOException ex) {
586           throw new IOException("Seek failed for key " + kvToSeek + ", pread="
587               + pread, ex);
588         }
589         numSeeks.incrementAndGet();
590         if (!seekResult) {
591           error("Seek returned false for row " + Bytes.toStringBinary(row));
592           return false;
593         }
594         for (int i = 0; i < rand.nextInt(10) + 1; ++i) {
595           KeyValue kv = scanner.next();
596           numKV.incrementAndGet();
597           if (i == 0 && kv == null) {
598             error("scanner.next() returned null at the first iteration for " +
599                 "row " + Bytes.toStringBinary(row));
600             return false;
601           }
602           if (kv == null)
603             break;
604 
605           String keyHashStr = MD5Hash.getMD5AsHex(kv.getKey());
606           keysRead.add(keyHashStr);
607           totalBytes.addAndGet(kv.getLength());
608         }
609       }
610 
611       return true;
612     }
613 
614     private void error(String msg) {
615       LOG.error("error in reader " + readerId + " (pread=" + pread + "): "
616           + msg);
617     }
618 
619   }
620 
621   private class StatisticsPrinter implements Callable<Boolean> {
622 
623     private volatile boolean stopRequested;
624     private volatile Thread thread;
625     private long totalSeekAndReads, totalPositionalReads;
626 
627     /**
628      * Run the statistics collector in a separate thread without an executor.
629      */
630     public void startThread() {
631       new Thread() {
632         @Override
633         public void run() {
634           try {
635             call();
636           } catch (Exception e) {
637             LOG.error(e);
638           }
639         }
640       }.start();
641     }
642 
643     @Override
644     public Boolean call() throws Exception {
645       LOG.info("Starting statistics printer");
646       thread = Thread.currentThread();
647       thread.setName(StatisticsPrinter.class.getSimpleName());
648       long startTime = System.currentTimeMillis();
649       long curTime;
650       while ((curTime = System.currentTimeMillis()) < endTime &&
651           !stopRequested) {
652         long elapsedTime = curTime - startTime;
653         printStats(elapsedTime);
654         try {
655           Thread.sleep(1000 - elapsedTime % 1000);
656         } catch (InterruptedException iex) {
657           Thread.currentThread().interrupt();
658           if (stopRequested)
659             break;
660         }
661       }
662       printStats(curTime - startTime);
663       LOG.info("Stopping statistics printer");
664       return true;
665     }
666 
667     private void printStats(long elapsedTime) {
668       long numSeeksL = numSeeks.get();
669       double timeSec = elapsedTime / 1000.0;
670       double seekPerSec = numSeeksL / timeSec;
671       long kvCount = numKV.get();
672       double kvPerSec = kvCount / timeSec;
673       long bytes = totalBytes.get();
674       double bytesPerSec = bytes / timeSec;
675 
676       // readOps and preadOps counters get reset on access, so we have to
677       // accumulate them here. HRegion metrics publishing thread should not
678       // be running in this tool, so no one else should be resetting these
679       // metrics.
680       totalSeekAndReads += HFile.getReadOps();
681       totalPositionalReads += HFile.getPreadOps();
682       long totalBlocksRead = totalSeekAndReads + totalPositionalReads;
683 
684       double blkReadPerSec = totalBlocksRead / timeSec;
685 
686       double seekReadPerSec = totalSeekAndReads / timeSec;
687       double preadPerSec = totalPositionalReads / timeSec;
688 
689       boolean isRead = workload == Workload.RANDOM_READS;
690 
691       StringBuilder sb = new StringBuilder();
692       sb.append("Time: " +  (long) timeSec + " sec");
693       if (isRead)
694         sb.append(", seek/sec: " + (long) seekPerSec);
695       sb.append(", kv/sec: " + (long) kvPerSec);
696       sb.append(", bytes/sec: " + (long) bytesPerSec);
697       sb.append(", blk/sec: " + (long) blkReadPerSec);
698       sb.append(", total KV: " + numKV);
699       sb.append(", total bytes: " + totalBytes);
700       sb.append(", total blk: " + totalBlocksRead);
701 
702       sb.append(", seekRead/sec: " + (long) seekReadPerSec);
703       sb.append(", pread/sec: " + (long) preadPerSec);
704 
705       if (isRead)
706         sb.append(", unique keys: " + (long) keysRead.size());
707 
708       LOG.info(sb.toString());
709     }
710 
711     public void requestStop() {
712       stopRequested = true;
713       if (thread != null)
714         thread.interrupt();
715     }
716 
717   }
718 
719   public boolean runRandomReadWorkload() throws IOException {
720     if (inputFileNames.size() != 1) {
721       throw new IOException("Need exactly one input file for random reads: " +
722           inputFileNames);
723     }
724 
725     Path inputPath = new Path(inputFileNames.get(0));
726 
727     // Make sure we are using caching.
728     StoreFile storeFile = openStoreFile(inputPath, true);
729 
730     StoreFile.Reader reader = storeFile.createReader();
731 
732     LOG.info("First key: " + Bytes.toStringBinary(reader.getFirstKey()));
733     LOG.info("Last key: " + Bytes.toStringBinary(reader.getLastKey()));
734 
735     KeyValue firstKV = KeyValue.createKeyValueFromKey(reader.getFirstKey());
736     firstRow = firstKV.getRow();
737 
738     KeyValue lastKV = KeyValue.createKeyValueFromKey(reader.getLastKey());
739     lastRow = lastKV.getRow();
740 
741     byte[] family = firstKV.getFamily();
742     if (!Bytes.equals(family, lastKV.getFamily())) {
743       LOG.error("First and last key have different families: "
744           + Bytes.toStringBinary(family) + " and "
745           + Bytes.toStringBinary(lastKV.getFamily()));
746       return false;
747     }
748 
749     if (Bytes.equals(firstRow, lastRow)) {
750       LOG.error("First and last row are the same, cannot run read workload: " +
751           "firstRow=" + Bytes.toStringBinary(firstRow) + ", " +
752           "lastRow=" + Bytes.toStringBinary(lastRow));
753       return false;
754     }
755 
756     ExecutorService exec = Executors.newFixedThreadPool(numReadThreads + 1);
757     int numCompleted = 0;
758     int numFailed = 0;
759     try {
760       ExecutorCompletionService<Boolean> ecs =
761           new ExecutorCompletionService<Boolean>(exec);
762       endTime = System.currentTimeMillis() + 1000 * durationSec;
763       boolean pread = true;
764       for (int i = 0; i < numReadThreads; ++i)
765         ecs.submit(new RandomReader(i, reader, pread));
766       ecs.submit(new StatisticsPrinter());
767       Future<Boolean> result;
768       while (true) {
769         try {
770           result = ecs.poll(endTime + 1000 - System.currentTimeMillis(),
771               TimeUnit.MILLISECONDS);
772           if (result == null)
773             break;
774           try {
775             if (result.get()) {
776               ++numCompleted;
777             } else {
778               ++numFailed;
779             }
780           } catch (ExecutionException e) {
781             LOG.error("Worker thread failure", e.getCause());
782             ++numFailed;
783           }
784         } catch (InterruptedException ex) {
785           LOG.error("Interrupted after " + numCompleted +
786               " workers completed");
787           Thread.currentThread().interrupt();
788           continue;
789         }
790 
791       }
792     } finally {
793       storeFile.closeReader(true);
794       exec.shutdown();
795 
796       BlockCache c = cacheConf.getBlockCache();
797       if (c != null) {
798         c.shutdown();
799       }
800     }
801     LOG.info("Worker threads completed: " + numCompleted);
802     LOG.info("Worker threads failed: " + numFailed);
803     return true;
804   }
805 
806   public boolean run() throws IOException {
807     LOG.info("Workload: " + workload);
808     switch (workload) {
809     case MERGE:
810       runMergeWorkload();
811       break;
812     case RANDOM_READS:
813       return runRandomReadWorkload();
814     default:
815       LOG.error("Unknown workload: " + workload);
816       return false;
817     }
818 
819     return true;
820   }
821 
822   private static void failure() {
823     System.exit(1);
824   }
825 
826   public static void main(String[] args) {
827     HFileReadWriteTest app = new HFileReadWriteTest();
828     if (!app.parseOptions(args))
829       failure();
830 
831     try {
832       if (!app.validateConfiguration() ||
833           !app.run())
834         failure();
835     } catch (IOException ex) {
836       LOG.error(ex);
837       failure();
838     }
839   }
840 
841 }