HBase性能深度分析

From HBase性能深度分析.

HBase作为BigTable的一个开源实现,随着其应用的普及,用户对它的性能数据愈发关注。本文将为您揭开HBase性能测试的一角,邀您一起参与到对云计算模块性能调优的深度思考中。

对于BigTable类型的分布式数据库应用来说,用户往往会对其性能状况有极大的兴趣,这其中又对实时数据插入性能更为关注。HBase作为BigTable的一个实现,在这方面的性能会如何呢?这就需要通过测试数据来说话了。

趋势科技中国研发中心资深QA,在六年职业 生涯中,开发和测试的经验恰好参半。2009年 加入趋势科技至今,一直从事基于HBase的分 布式存储数据库的研发。
刘星 趋势科技中国研发中心资深QA,2009年 加入趋势科技至今,一直从事基于HBase的分布式存储数据库的研发。

数据插入性能测试的设计场景是这样的:取随机值的Rowkey长度为2000字节,固定值的Value长度为4000字节,由于单行Row插入速度太快,系统统计精度不够,所以将插入500行Row做一次耗时统计。

这里要对HBase的特点做个说明,首先是Rowkey值为何取随机数,这是因为HBase是对Rowkey进行排序的,随机Rowkey将被分配到不同的region上,这样才能发挥出分布式数据库的性能优点。而Value对于HBase来说不会进行任何解析,其数据是否变化,对性能是不应该有任何影响的。同时为了简单起见,所有的数据都将只插入到一个表格的同一个Column中。

初次分析

在测试之初,需要对集群进行调优,关闭可能大量耗费内存、带宽以及CPU的服务(例如Apache的HTTP服务),保持集群的宁静度。此外,为了保证测试不受干扰,HBase的集群系统需要被独立,以保证不与HDFS所在的Hadoop集群有所交叉。

实验

那么做好一切准备之后,就开始进行数据灌入,客户端从Zookeeper上查询到Regionserver的地址后,开始源源不断地向HBase的Regionserver上喂入Row。

这里,我写了一个通过JFreeChart来实时生成图片的程序,每3分钟,喂数据的客户端会将获取到的耗时统计打印在一张十字坐标图中,这些图又被保存在制定的Web站点中,并通过HTTP服务展示出来。在通过长时间不间断的测试后,我得到了图1。

图1 插入Row的性能测试
图1 插入Row的性能测试

图1好似一条直线上,每隔一段时间就会泛起一个波浪,且两个高峰之间必有一个较矮的波浪。高峰的间隔呈现出越来越大的趋势,而较矮的波浪恰好处于两高峰的中间位置。

解读

为了解释,我对HDFS上HBase所在的主目录下文件,以及被插入表格的region情况进行了实时监控,以期发现这些波浪上发生了什么事情。

回溯到客户端喂入数据的开始阶段,创建表格,在HDFS上便被创建了一个与表格同名的目录,该目录下将出现第一个region,region中会以family名创建一个目录,这个目录下才存在记录具体数据的文件。同时在该表表名目录下,还会生成一个“compaction.dir”目录,该目录将在family名目录下region文件超过指定数目时用于合并region。当第一个region目录出现时,内存中最初被写入的数据将被保存到该文件中,这个间隔是由选项“hbase.hregion.memstore.flush.size”决定的,默认是64MB,该region所在的Regionserver的内存中一旦有超过64MB的数据时,就将被写入到region文件中。这个文件将不断增殖,直到超过由“hbase.hregion.max.filesize”决定的文件大小(默认是256MB,此时加上内存刷入的数据,实际最大可能到256MB+64MB)时,该region将被执行split,立即被一切为二,其过程是在该目录下创建一个名为“.splits”的目录作为标记,然后由Regionserver将文件信息读取进来,分别写入到两个新的region目录中,最后再将老的region删除。这里的标记目录“.splits”可以避免在split过程中发生其他操作,起到类似于多线程安全的锁功能。在新的region中,从老的region中切分出的数据独立为一个文件并不再接收新的数据(该文件大小超过了64MB,最大可达到(256+64)/2=160MB)),内存中新的数据将被保存到一个重新创建的文件中,该文件大小将为64MB。内存每刷新一次,region所在的目录下就将增加一个64MB的文件,直到总文件数超过由“hbase.hstore.compactionThreshold”指定的数量时(默认为3),compaction过程就将被触发了。在上述值为3时,此时该region目录下,实际文件数只有两个,还有额外的一个正处于内存中将要被刷入到磁盘的过程中。Compaction过程是HBase的一个大动作。HBase不仅要将这些文件转移到“compaction.dir”目录进行压缩,而且在压缩后的文件超过256MB时,还必须立即进行split动作。这一系列行为在HDFS上可谓是翻山倒海,影响颇大。待Compaction结束之后,后续的split依然会持续一小段时间,直到所有的region都被切割分配完毕,HBase才会恢复平静并等待下一次数据从内存写入到HDFS。

图2 再次的数据插入测试

图2 再次的数据插入测试

理解了上述过程,就必然会对HBase的数据插入性能为何是图1所示的曲线的原因一目了然。与X轴几乎平行的直线,表明数据正在被写入HBase的Regionserver所在机器的内存中。而较低的波峰意味着Regionserver正在将内存写入到HDFS上,较高的波峰意味着Regionserver不仅正在将内存刷入到HDFS,而且还在执行Compaction和Split两种操作。如果调整“hbase.hstore.compactionThreshold”的值为一个较大的数量,例如改成5,可以预见,在每两个高峰之间必然会等间隔地出现三次较低的波峰,并可预见到,高峰的高度将远超过上述值为3时的高峰高度(因为Compaction的工作更为艰巨)。由于region数量由少到多,而我们插入的Row的Rowkey是随机的,因此每一个region中的数据都会均匀的增加,同一段时间插入的数据将被分布到越来越多的region上,因此波峰之间的间隔时间也将会越来越长。

再次理解上述论述,我们可以推断出HBase的数据插入性能实际上应该被分为三种情况:直线状态、低峰状态和高峰状态。在这三种情况下得到的性能数据才是最终HBase数据插入性能的真实描述。那么提供给用户的数据该是采取哪一个呢?我认为直线状态由于所占时间会较长,尤其在用户写入数据的速度也许并不是那么快的情况下,所以这个状态下得到的性能数据结果更应该提供给用户。

图3 图1的数据分布图

图3 图1的数据分布图

再度分析

前面的HBase性能深度分析,提出了一个猜想,是关于调整“hbase.hstore.compaction-Threshold”值的假设。猜想的内容为:如果改变该值,例如调整为5,那么耗时图形会在每两个高峰之间出现等间隔的三次较低的波峰,并且高峰将会更加突出,超过上述值较低时的波峰高度。

为了证明这个猜想,我将“hbase.hstore.compactionThreshold”值调整为5,并重新做了数据插入测试,一段时间后,得到如图2所示的性能图形(Y轴表示耗时,X轴为插入次数,Sandy建议这里的Y轴应该改为插入速度,但是由于前次已经使用了耗时为Y轴,因此改变Y轴显示的工作只能放到下次测试中了)。

通过相比发现,图1和图2的Y轴比例尺是不同的,图1中Y轴最大为30秒,图2中最大为50秒。可见假设中声称低峰会在两个高峰之间等间隔的出现3次的现象的确是成立的。当高峰出现第5次以后,可以从图2看到代表耗时的点的高峰段已经达到了25秒以上,而对于前次来说,高峰段基本上处于20秒左右,由此可以认为Compaction的压力的确是增加了。现在换一个角度来分析这一情况。我为图1制作了一张数据分布图(图3),与图2进行比较(图4)。

虽说第二次测试经历的时间不如第一次,但是基于统计学的观点,分布图的外形是不会受样本容量大小影响的,因此图3和图4可以进行外观上的比较。这两张分布图都是典型的正态分布图,但又不是标准正态分布,原因在于,波峰段的数据影响了正态分布的标准性,表现之一在于右侧的长尾,表现之二在于众数所在位置右移,以至于左侧凸显了一个小波峰。

图4 图2的数据分布图

图4 图2的数据分布图

计算本次分布图与前次分布图中位于右侧长尾部分的数据的标准差(计算公式:),我们可以得到前次右侧标准差为4.10390692438,而本次右侧标准差为7.12068861446,说明高峰段影响的数据右偏更为严重了。

从外观上表现在右侧长尾在整图比例尺中的宽度和高度要大于前次分布图中的右侧长尾。这说明Compaction的压力增大了。

推导到这里,我发现右侧标准差与Compaction的压力之间是存在显著关系的,今后对Compaction压力增减的估算,貌似可以转换为对右侧标准差的计算。压力增加的比率是否等于标准差的比值呢?这里先做一个标记,等后面有时间再仔细思考一下这个问题。现在假设中的说法“高峰将会更加突出,超过上述值较低时的波峰高度”,应该算是被证明了。

以上论证结束之后,按照惯例还是要提出一些假设和推断。

HBase在已经发布的0.90.x版对Compaction和Split机制作了调整,将Split过程提到了Compaction之前,也就是说,当region目录下,HFiles数目超过“hbase.hstore.compactionThreshold”指定值之后,Regionserver会首先计算一下Compaction之后的文件大小是否会超过“hbase.hregion.max.filesize”确定的Split上限大小,如果超过了,那么HFiles首先被切分,然后才会将切分好的文件转移到新的region中Compaction。这样将大大减小Compaction的压力,由此可以推断,HBase的性能调优必然与“hbase.hstore.compactionThreshold”和“hbase.hregion.max.filesize”这两个值的大小息息相关。理论上,可以将某次设定了确定值的实验中获得的数据代入到一个特定公式中,上述两值作为该公式的自变量,其应变量,即性能数据,将可以轻松地计算出来。

是否真的如此,且待进一步的详细测试。

hbase.mapreduce Description from JavaDoc

From org.apache.hadoop.hbase.mapreduce HBase 0.90.4 API.

Package org.apache.hadoop.hbase.mapreduce Description

Provides HBase MapReduce Input/OutputFormats, a table indexing MapReduce job, and utility

Table of Contents

HBase, MapReduce and the CLASSPATH

MapReduce jobs deployed to a MapReduce cluster do not by default have access to the HBase configuration under $HBASE_CONF_DIR nor to HBase classes. You could add hbase-site.xml to$HADOOP_HOME/conf and add HBase jars to the $HADOOP_HOME/lib and copy these changes across your cluster (or edit conf/hadoop-env.sh and add them to the HADOOP_CLASSPATH variable) but this will pollute your hadoop install with HBase references; its also obnoxious requiring restart of the hadoop cluster before it’ll notice your HBase additions.

As of 0.90.x, HBase will just add its dependency jars to the job configuration; the dependencies just need to be available on the local CLASSPATH. For example, to run the bundled HBaseRowCounter mapreduce job against a table named usertable, type:

$ HADOOP_CLASSPATH=`${HBASE_HOME}/bin/hbase classpath` ${HADOOP_HOME}/bin/hadoop jar ${HBASE_HOME}/hbase-0.90.0.jar rowcounter usertable

Expand $HBASE_HOME and $HADOOP_HOME in the above appropriately to suit your local environment. The content of HADOOP_CLASSPATH is set to the HBase CLASSPATH via backticking the command${HBASE_HOME}/bin/hbase classpath.

When the above runs, internally, the HBase jar finds its zookeeper and guava, etc., dependencies on the passed HADOOP_CLASSPATH and adds the found jars to the mapreduce job configuration. See the source at TableMapReduceUtil#addDependencyJars(org.apache.hadoop.mapreduce.Job) for how this is done.

The above may not work if you are running your HBase from its build directory; i.e. you’ve done $ mvn test install at ${HBASE_HOME} and you are now trying to use this build in your mapreduce job. If you get

java.lang.RuntimeException: java.lang.ClassNotFoundException: org.apache.hadoop.hbase.mapreduce.RowCounter$RowCounterMapper
...

exception thrown, try doing the following:

$ HADOOP_CLASSPATH=${HBASE_HOME}/target/hbase-0.90.0-SNAPSHOT.jar:`${HBASE_HOME}/bin/hbase classpath` ${HADOOP_HOME}/bin/hadoop jar ${HBASE_HOME}/target/hbase-0.90.0-SNAPSHOT.jar rowcounter usertable

Notice how we preface the backtick invocation setting HADOOP_CLASSPATH with reference to the built HBase jar over in the target directory.

 

Bundled HBase MapReduce Jobs

The HBase jar also serves as a Driver for some bundled mapreduce jobs. To learn about the bundled mapreduce jobs run:

$ ${HADOOP_HOME}/bin/hadoop jar ${HBASE_HOME}/hbase-0.90.0-SNAPSHOT.jar
An example program must be given as the first argument.
Valid program names are:
  copytable: Export a table from local cluster to peer cluster
  completebulkload: Complete a bulk data load.
  export: Write table data to HDFS.
  import: Import data written by Export.
  importtsv: Import data in TSV format.
  rowcounter: Count rows in HBase table

HBase as MapReduce job data source and sink

HBase can be used as a data source, TableInputFormat, and data sink, TableOutputFormat or MultiTableOutputFormat, for MapReduce jobs. Writing MapReduce jobs that read or write HBase, you’ll probably want to subclass TableMapper and/or TableReducer. See the do-nothing pass-through classes IdentityTableMapper and IdentityTableReducer for basic usage. For a more involved example, seeRowCounter or review the org.apache.hadoop.hbase.mapreduce.TestTableMapReduce unit test.

Running mapreduce jobs that have HBase as source or sink, you’ll need to specify source/sink table and column names in your configuration.

Reading from HBase, the TableInputFormat asks HBase for the list of regions and makes a map-per-region or mapred.map.tasks maps, whichever is smaller (If your job only has two maps, up mapred.map.tasks to a number > number of regions). Maps will run on the adjacent TaskTracker if you are running a TaskTracer and RegionServer per node. Writing, it may make sense to avoid the reduce step and write yourself back into HBase from inside your map. You’d do this when your job does not need the sort and collation that mapreduce does on the map emitted data; on insert, HBase ‘sorts’ so there is no point double-sorting (and shuffling data around your mapreduce cluster) unless you need to. If you do not need the reduce, you might just have your map emit counts of records processed just so the framework’s report at the end of your job has meaning or set the number of reduces to zero and use TableOutputFormat. See example code below. If running the reduce step makes sense in your case, its usually better to have lots of reducers so load is spread across the HBase cluster.

There is also a new HBase partitioner that will run as many reducers as currently existing regions. The HRegionPartitioner is suitable when your table is large and your upload is not such that it will greatly alter the number of existing regions when done; otherwise use the default partitioner.

Bulk import writing HFiles directly

If importing into a new table, its possible to by-pass the HBase API and write your content directly to the filesystem properly formatted as HBase data files (HFiles). Your import will run faster, perhaps an order of magnitude faster if not more. For more on how this mechanism works, see Bulk Loads documentation.

Example Code

Sample Row Counter

See RowCounter. This job uses TableInputFormat and does a count of all rows in specified table. You should be able to run it by doing: % ./bin/hadoop jar hbase-X.X.X.jar. This will invoke the hbase MapReduce Driver class. Select ‘rowcounter’ from the choice of jobs offered. This will emit rowcouner ‘usage’. Specify tablename, column to count and output directory. You may need to add the hbase conf directory to $HADOOP_HOME/conf/hadoop-env.sh#HADOOP_CLASSPATH so the rowcounter gets pointed at the right hbase cluster (or, build a new jar with an appropriate hbase-site.xml built into your job jar).