hbase性能调试 转

本文主要介绍软件层面的性能调优。故,在此之前,请检查硬件状况。硬盘推荐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配置项:

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的线程数:

// 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上。

相关推荐