欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  数据库

HBase Bulkload bug修复及patch提交

程序员文章站 2022-04-13 20:24:57
...

第一部分:问题排查。 在店铺搜索相关需求的开发自测过程中,碰到了一个问题:bulkload数据的过程时间过长,运行了很久都没有结束,于是查看日志,发现bulkload的程序在不停的重试,信息如下(当天信息未保存,这是刚重现时截的)。 这些信息看起来没啥问题

第一部分:问题排查。

在店铺搜索相关需求的开发自测过程中,碰到了一个问题:bulkload数据的过程时间过长,运行了很久都没有结束,于是查看日志,发现bulkload的程序在不停的重试,信息如下(当天信息未保存,这是刚重现时截的)。

HBase Bulkload bug修复及patch提交

这些信息看起来没啥问题,bulkload在往表test_shopinfo里load各个hfile,失败了,但是错误是可恢复的,将会重试,接着又看到如下的信息:

HBase Bulkload bug修复及patch提交

好了,问题就是这样,bulkload在不停的失败,不停的重试,没有个尽头。开始怀疑是hbase集群出了情况,经过对hbase的一番排查,最后在regionserver的日志里发现了对应的一些信息:

HBase Bulkload bug修复及patch提交

从日志里看到,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 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 ioes = new ArrayList();
      List> failures = new ArrayList>();
      for (Pair 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
   * @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> familyPaths, boolean assignSeqId,
      BulkLoadListener bulkLoadListener) throws IOException

成功返回true,失败且recoverable,返回false,失败且unrecoverable,抛出IOException。

把这整段代码贴上来,方便看:

      List ioes = new ArrayList();
      List> failures = new ArrayList>();
      for (Pair 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 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;
      }

上面一段代码,在处理一批hfile时,将对应的失败和IOException保存在List里,然后在下面一段代码里进行处理,好吧,问题就在这:上面的代码抓到的IOException,都意味着该次bulkload是肯定要失败的,然而在后续的处理中,代码竟然先处理了failures里的信息,然后输出warm的log告诉用户recoverable,并且返回了false,直接把下面处理IOException的代码跳过了。理一下逻辑,这个地方的处理,必然应该是先处理IOException,如果没有IOException,才轮到处理failures。

至此,问题已经清楚,解决方法也基本明确,可这hbase的代码,不是咱说改就能改的,咋整?

就在这时,道凡大牛伸出了援手,https://issues.apache.org/jira/browse/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上的,http://svn.apache.org/repos/asf/hbase/trunk),安装mvn、jdk等(暂时不清楚具体的jdk版本依赖,我自己搭建的时候用1.6编译出错了,换1.7编译通过的)。http://hbase.apache.org/book.html#developer 这里有一些官方的手册,可能会给你带来一些帮助。

3. 修改代码,重新编译,运行test case,上面的手册对这些过程也有帮助,碰到问题可以参考。修改代码的时候有一些注意事项:http://wiki.apache.org/hadoop/Hbase/HowToContribute 可以先看一下。运行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上。

8. 基本上就没有什么问题了,如果有问题committer应该会再联系你。

最后的最后,如果对此感兴趣或是有问题想交流的,欢迎骚扰。