本文主要介绍软件层面的性能调优。故,在此之前,请检查硬件状况。硬盘推荐SSD,一般SATA即可。网络千兆以上。可以安装Ganglia等工具,检查各节点的各硬件的运作状态:CPU,Memo,网络等等。
一、调整参数
入门级的调优可以从调整参数开始。投入小,回报快。
1. Write Buffer Size
快速配置
- HTable htable = new HTable(config, tablename);
- htable.setWriteBufferSize(6 * 1024 * 1024);
- htable.setAutoFlush(false);
设置buffer的容量,例子中设置了6MB的buffer容量。
* 必须禁止auto flush。
* 6MB是经验值,可以上下微调以适应不同的写场景。
原理
HBase Client会在数据累积到设置的阈值后才提交Region Server。这样做的好处在于可以减少RPC连接次数。同时,我们得计算一下服务端因此而消耗的内存:hbase.client.write.buffer * hbase.regionserver.handler.count。在减少PRC次数和增加服务器端内存之间找到平衡点。
2. RPC Handler
快速配置
修改hbase-site.xml的hbase.regionserver.handler.count配置项:
- <property>
- <name>hbase.regionserver.handler.count</name>
- <value>100</value>
- </property>
原理
该配置定义了每个Region Server上的RPC Handler的数量。Region Server通过RPC Handler接收外部请求并加以处理。所以提升RPC Handler的数量可以一定程度上提高HBase接收请求的能力。当然,handler数量也不是越大越好,这要取决于节点的硬件情况。
3. Compression 压缩
快速配置
- HColumnDescriptor hcd = new HColumnDescriptor(familyName);
- hcd.setCompressionType(Algorithm.SNAPPY);
原理
数据量大,边压边写也会提升性能的,毕竟IO是大数据的最严重的瓶颈,哪怕使用了SSD也是一样。众多的压缩方式中,推荐使用SNAPPY。从压缩率和压缩速度来看,性价比最高。
4. WAL
快速配置
- Put put = new Put(rowKey);
- put.setWriteToWAL(false);
原理
其实不推荐关闭WAL,不过关了的确可以提升性能...因为HBase在写数据前会先写WAL,以保证在异常情况下,HBase可以按照WAL的记录来恢复还未持久化的数据。
5. Replication
虽然推荐replica=3,不过当数据量很夸张的时候,一般会把replica降低到2。当然也不推荐随便降低replica。
6. Compaction
在插数据时,打开HMaster的web界面,查看每个region server的request数量。确保大部分时间,写请求在region server层面大致平均分布。
在此前提下,我们再考虑compaction的问题。继续观察request数量,你会发现在某个时间段,若干region server接收的请求数为0(当然这也可能是client根本没有向这个region server写数据,所以之前说,要确保请求在各region server大致平均分布)。这很有可能是region server在做compaction导致。compaction的过程会block写。
优化的思路有两种,一是提高compaction的效率,二是减少compaction发生的频率。
提高以下两个属性的值,以增加执行compaction的线程数:
- hbase.regionserver.thread.compaction.large
- hbase.regionserver.thread.compaction.small
推荐设置为2。
7. 减少Region Split次数
region split是提升写性能的一大障碍。减少region split次数可以从两方面入手,一是预分配region(该内容会在下章节表设计优化里详述)。其二是适当提升hbase.hregion.max.filesize
提升region的file容量也可以减少split的次数。具体的值需要按照你的数据量,region数量,row key分布等情况具体考量。一般来说,3~4G是不错的选择。
8. HFile format version
0.92.0后的version都应该是2。v2比v1支持更大的region大小。一般经验是Region越大越少,性能更好(当然也不能过分大,否则major compaction的时候时间长的吃不消)。所以推荐把hfile.format.version改成2,并提高hfile大小。对于使用v1 format的用户,不用担心,数据迁移到v2上是有工具的。具体参见HBASE-1621。
9. hbase.ipc.client.tcpnodelay
设置成True。关闭Nagle,可能提高latency。当然HDFS也关掉TPC Nagle。
二、表设计优化
1. 预分配Region
之前有说防止region split的两大手段其中之一就是预分配region。
在此不重复region split的原理,请参见http://blog.sina.com.cn/s/blog_9cee0fd901018vu2.html。按数据量,row key的规则预先设计并分配好region,可以大幅降低region split的次数, 甚至不split。这点非常重要。
2. Column Family的数量
实测发现column family的数量对性能会有直接影响。建议减少column family的数量。单个cf是最好
3. Column Family MAX_VERSIONS/MAX_LENGTH
前者确定保存一个cell的最大历史份数,后者确定多少byte可以存进一个cell 历史记录。所以我们可以减低这些值。
4. Row Key的设计
Region的数据边界是start key和end key。如果记录的row key落在某个region的start key和end key的范围之内,该数据就会存储到这个region上。在写数据的时候,尤其是导入客户原有数据的时候,如果row key设计不当,很可能导致性能问题。之前我们也介绍了row key和region的关系。如果在某个时段内,很多数据的row key都处在某个特定的row key范围内。那这个特定范围row key对应的region会非常繁忙,而其他的region很可能非常的空闲,导致资源浪费。
那么,如何设计row key呢?举个比较实际的例子,如果有张HBase表来记录每天某城市的通话记录, 常规思路下的row key是由电话号码 + yyyyMMddHHmmSS(通话开始时间) + ... 组成。按电话号码的规律来划分region。但是这样很容易导致某时段row key极其不均匀(因为电话通话呈随机性)。但是,如果把电话号码倒序,数据在region层面的分布情况就大有改观。
设计row key的方法千变万化,宗旨只有一条,尽量保证单位时间内写入数据的row key对于region呈均匀分布。
三、优化Client设计
实践发现,写性能差大部分情况是源于Client端的糟糕设计。接下来分享一些Client设计的思路。
1. 均匀分布每个Region Server的写压力
之前也提到了RPC Handler的概念。好的Data Loader需要保证每个RPC Handlder都有活干,每个handler忙,但不至超载。注意region的压力不能过大,否则会导致反复重试,并伴有超时异常(可以提高超时的时间设置)。
如何保证每个Region Server的压力均衡呢?这和region 数量,row key的设计 和client数据的插入顺序有关。设计者需要根据用户数据的情况,集群情况来综合考虑。
2. 并行的数据插入框架
多线程是最简单的解决方案。要点是让每个线程负责一部分的row key范围,而row key范围又和region相关,所以可以在数据插入时,程序控制每个region的压力,不至于有些region闲着没事干。由于相对简单,不再赘述。
即使使用多线程,也受限于单节点的硬件资源,写入速度不可能很快。典型的思路是将客户端部署在多个节点上运行,提高写的并发度。MapReduce是个很好的选择。使用MapReduce把写入程序分布到集群的各个节点上,并在每个mapper中运行多线程的插入程序。这样可以很好的提高写并发度。
注意,不要使用reducer。mapper到reducer需要走网络,受限于集群带宽。其次,实际的应用场景一般是用户从关系型数据库中导出了文本类型的数据,然后希望能把导出的数据写到HBase里。在这种情况下,需要小心谨慎地设计和实现FileInputFormat的file split逻辑。
3. BulkLoad
请拿出HBase的API读读,HFileOutputFomart里有个叫configureIncrementalLoad的方法。API是这么介绍的:
Inspects the table to configure a total order partitioner
Uploads the partitions file to the cluster and adds it to the DistributedCache
Sets the number of reduce tasks to match the current number of regions
Sets the output key/value class to match HFileOutputFormat's requirements
Sets the reducer up to perform the appropriate sorting (either KeyValueSortReducer or PutSortReducer)
The user should be sure to set the map output value class to either KeyValue or Put before running this function.
这是HBase提供的一种基于MapReduce的数据导入方案,完美地绕过了HBase Client(上一节的分布式插入方法也是用mapreduce实现的,不过本质上还是用hbase client来写数据)
网上有不少文章叙述了使用命令行方式运行BulkLoad,google一下你就知道...
但是,不得不说,实际生产环境上很难使用这种方式。毕竟源数据不可能直接用来写HBase。在数据迁移的过程中会涉及到数据清洗、整理归并等许多额外的工作。这显然不是命令行可以做到的事情。按照API的描述, 可行的方案是自定义一个Mapper在mapper中清洗数据,Mapper的输出value为HBase的Put类型,Reducer选用PutSortReducer。然后使用HFileOutputFormat#configureIncrementalLoad(Job, HTable);解决剩余工作。
不过,这种实现也存在局限性。毕竟Mapper到Reducer比较吃网络。
四、写在后面的话
至此,本文介绍了三种HBase数据写入的方法(1种多线程,2种mapreduce),并介绍了各类性能调优的方法。希望能对大家有所帮助。如果有大家有更好的方法,不妨留言讨论,共同进步。
-----------------------------------------
第一部分:问题排查。
在店铺搜索相关需求的开发自测过程中,碰到了一个问题:bulkload数据的过程时间过长,运行了很久都没有结束,于是查看日志,发现bulkload的程序在不停的重试,信息如下(当天信息未保存,这是刚重现时截的)。
这些信息看起来没啥问题,bulkload在往表test_shopinfo里load各个hfile,失败了,但是错误是可恢复的,将会重试,接着又看到如下的信息:
好了,问题就是这样,bulkload在不停的失败,不停的重试,没有个尽头。开始怀疑是hbase集群出了情况,经过对hbase的一番排查,最后在regionserver的日志里发现了对应的一些信息:
从日志里看到,regionserver检查ladder这个family的hfile bounds,发现与regionserver的bounds匹配上了,应该是成功往里load了,但是ecrm这个family的hfile load失败了,日志里的错误信息是由于发生了split才失败的,但是是可以恢复的。
但是我们对于hbase表的策略是通过设定hfile的最大size来避免发生split的,所以基本上不会发生split(我们将最大max设得很大),于是觉得regionserver在处理ecrm的hfile时一定出现了问题,接着找到了HRegion.java的代码,相关代码如下:
- // validation failed, bail out before doing anything permanent. if (failures.size()
- != 0) { StringBuilder list = new StringBuilder(); for (Pair<byte[],
- String> p : failures) { list.append('n').append(Bytes.toString(p.getFirst())).append('
- : ') .append(p.getSecond()); } // problem when validating LOG.warn('There
- was a recoverable bulk load failure likely due to a' + ' split. These (family,
- HFile) pairs were not loaded: ' + list); return false; }
接着看failures的来源,代码如下,就在上面这段代码的上方:
- List<IOException> ioes = new ArrayList<IOException>();
- List<Pair<byte[], String>> failures = new ArrayList<Pair<byte[], String>>();
- for (Pair<byte[], String> p : familyPaths) {
- byte[] familyName = p.getFirst();
- String path = p.getSecond();
- Store store = getStore(familyName);
- if (store == null) {
- IOException ioe = new org.apache.hadoop.hbase.exceptions.DoNotRetryIOException(
- 'No such column family ' + Bytes.toStringBinary(familyName));
- ioes.add(ioe);
- failures.add(p);
- } else {
- try {
- store.assertBulkLoadHFileOk(new Path(path));
- } catch (WrongRegionException wre) {
- // recoverable (file doesn't fit in region)
- failures.add(p);
- } catch (IOException ioe) {
- // unrecoverable (hdfs problem)
- ioes.add(ioe);
- }
- }
- }
一共两处代码往failures里add了东西,下面一处,是先调用了HStore.assertBulkLoadHFileOk(),查看该方法代码后发现,regionserver日志中检查hfile和region bounds的内容就是该方法输出的,而对于ecrm这个family的hfile,根本没有输出相关的bounds信息,因此确定是由上面这段代码第一处failures.add(p)添加进去的,这个时候才反应过来:ecrm这个family是这一次新添加的数据,但是对应hbase表没有重建以添加该family。于是在环境里把hbase表重建,再跑bulkload,很轻松的成功跑完。OK,自测的问题到此已经解决,但是遗留了一个问题:往这hbase表里bulkload不存在的family的hfile,日志竟然告诉我recoverable,然后无限的重试,这不是坑爹吗?于是有了下面的故事。
第二部分:hbase社区上的一番折腾
本着排查问题刨根问底的精神,我又回到了那段坑爹的代码上,仔细的看了两遍,然后发现了问题:
先看这段代码所在方法的说明:
- /**
- * Attempts to atomically load a group of hfiles. This is critical for loading
- * rows with multiple column families atomically.
- *
- * @param familyPaths List of Pair<byte[] column family, String hfilePath>
- * @param bulkLoadListener Internal hooks enabling massaging/preparation of a
- * file about to be bulk loaded
- * @param assignSeqId
- * @return true if successful, false if failed recoverably
- * @throws IOException if failed unrecoverably.
- */
- public boolean bulkLoadHFiles(List<Pair<byte[], String>> familyPaths, boolean assignSeqId,
- BulkLoadListener bulkLoadListener) throws IOException</pre>
成功返回true,失败且recoverable,返回false,失败且unrecoverable,抛出IOException。
把这整段代码贴上来,方便看:
- List<IOException> ioes = new ArrayList<IOException>();
- List<Pair<byte[], String>> failures = new ArrayList<Pair<byte[], String>>();
- for (Pair<byte[], String> p : familyPaths) {
- byte[] familyName = p.getFirst();
- String path = p.getSecond();
- Store store = getStore(familyName);
- if (store == null) {
- IOException ioe = new org.apache.hadoop.hbase.exceptions.DoNotRetryIOException(
- 'No such column family ' + Bytes.toStringBinary(familyName));
- ioes.add(ioe);
- failures.add(p);
- } else {
- try {
- store.assertBulkLoadHFileOk(new Path(path));
- } catch (WrongRegionException wre) {
- // recoverable (file doesn't fit in region)
- failures.add(p);
- } catch (IOException ioe) {
- // unrecoverable (hdfs problem)
- ioes.add(ioe);
- }
- }
- }
- // validation failed, bail out before doing anything permanent.
- if (failures.size() != 0) {
- StringBuilder list = new StringBuilder();
- for (Pair<byte[], String> p : failures) {
- list.append('n').append(Bytes.toString(p.getFirst())).append(' : ')
- .append(p.getSecond());
- }
- // problem when validating
- LOG.warn('There was a recoverable bulk load failure likely due to a' +
- ' split. These (family, HFile) pairs were not loaded: ' + list);
- return false;
- }
- // validation failed because of some sort of IO problem.
- if (ioes.size() != 0) {
- IOException e = MultipleIOException.createIOException(ioes);
- LOG.error('There were one or more IO errors when checking if the bulk load is ok.', e);
- throw e;
- }</pre>
上面一段代码,在处理一批hfile时,将对应的失败和IOException保存在List里,然后在下面一段代码里进行处理,好吧,问题就在这:上面的代码抓到的IOException,都意味着该次bulkload是肯定要失败的,然而在后续的处理中,代码竟然先处理了failures里的信息,然后输出warm的log告诉用户recoverable,并且返回了false,直接把下面处理IOException的代码跳过了。理一下逻辑,这个地方的处理,必然应该是先处理IOException,如果没有IOException,才轮到处理failures。
至此,问题已经清楚,解决方法也基本明确,可这hbase的代码,不是咱说改就能改的,咋整?
就在这时,道凡大牛伸出了援手。道凡说,就在这,提交issue,可以解决问题!
我寻思着能为hbase做些贡献好像还不错的样子,于是怀着试一试的心态点开了链接,注册,create issue,然后用不太熟练的英文把上面的问题描述了一遍,OK,issue创建完了,心想着应该会有大牛过来看看这个bug,然后很随意的帮忙fix一下,就搞定了,也没我啥事了。
第二天到公司,道凡突然发来一条消息,说issue有人回复了,点进去一看,一位大牛Ted Yu进来表示了赞同,还来了一句“Any chance of a patch ?” 我一想,这是大牛在鼓励咱这newbie大胆尝试嘛,果然很有大牛的风范,冲着对大牛的敬仰,以及此时咱后台组群里大哥哥大姐姐们的鼓励,咱抱着“不能怂”的心态,决定大胆尝试一把。
接下来的事情喜闻乐见,完全不知道怎么整的我根本不知道该干啥,好在有Ted Yu的指点和同事们的鼓励、帮助,一步一步的完成了check out代码,修改代码,搭建编译环境,提交patch,补充test case,在自己的环境运行test case,提交带test case的patch,等等等等等等一系列复杂的过程(此处省略好几万字),终于在今天上午,一位committer将我的patch提交到了多个版本的trunk上,事情到此已经基本了结,svn的log里也出现了我的名字,也让我感觉这些天的努力没有白费(由于时差,跟其它人讨论问题以及寻求帮助都需要耐心的等待)。
在此也希望广大同胞们能勇于提交issue,帮助自己也帮助更多使用这些开源软件的同学们,为造福人类贡献绵薄之力。
附上这次的issue的链接: https://issues.apache.org/jira/browse/HBASE-8192
最后附上一个issue从提交到解决的大概过程,希望对后续提交issue的同学能有所帮助:
1. 创建issue,尽可能的把问题描述清楚,如果解决方案比较明确,一并附上,如果不是很明确,可以在comment里跟其他人讨论、交流。
2. 有了解决方案以后,准备自己提交patch的话,就得搭建开发环境(如果没搭过),包括check out代码(patch一般都是打在trunk上的),安装mvn、jdk等(暂时不清楚具体的jdk版本依赖,我自己搭建的时候用1.6编译出错了,换1.7编译通过的)。这里有一些官方的手册,可能会给你带来一些帮助。
3. 修改代码,重新编译,运行test case,上面的手册对这些过程也有帮助,碰到问题可以参考。修改代码的时候有一些注意事项:可以先看一下这里。运行test case的时候关注一下磁盘的剩余空间,因为没空间时报的错误信息可能不是直接相关的,会是其它的一些Exception,所以要多想着这事(我被这个坑了不少次),test data会占据不小的空间(几个G),还有就是记得mvn clean。
4. attach files将你的patch上传,然后submit patch。这里提交的是一份你代码与trunk代码的diff,要从hbase trunk的svn根目录svn diff。
5. 每次attach files之后,过一会就会有Hadoop QA(不是很清楚是否为自动的)来测试你的patch。test result里列出来的问题是需要解决的(除了那些不是你代码改动带来的test case fail)。
6. 提交了patch之后,issue的状态会变为patch available,这时候(可能需要等一段时间)会有人(不确定是否一定是committer)来帮你review,如果觉得没问题的话他们会在comment里留下+1,或是lgtm(looks good to me)之类的东西。
7. 如果patch基本没问题之后,需要等committer来把你的patch拖到一些branch上进行测试,然后他们会在测试通过之后将你的patch commit到对应的svn上。