HBase BulkLoad异常生成千万HDFS文件问题排查

总 第17


2019年 第13篇
一、背景
HBase BulkLoad通常分为两步骤
Step1. MR生成HFile文件
Step2. 将HFile文件导入集群
在Step1的reduce过程中,使用HFileOutputFormat2工具类生成HFile。

我们在Kylin的Merge任务中使用到了这一工具类,
也正是这个类异常生成了上千万个只有54B大小的HDFS文件,造成HDFS的namenode节点不可用,我们排查并修复了该问题提交至社区 HBASE-22887

本文将探究这个问题前世今生~ 揭开它的神秘面纱~


二、问题详解

在HBase2.0.0版本以前,
不同column family(以下简称CF)须同时进行flush,也就是说对于任意rowkeyA如果出现在F1的第一个文件中那也必须出现在F2的第一个文件中。MR任务调用
HFileOutputFormat2直接生成HFile文件时,

要求输入数据类型为HBase的Cell格式的有序数据集合(例如rowKey1-F1,rowKey1-F2,rowKey2-F1,rowKey2-F2),一般情况下每个 调用
HFileOutputFormat2
的reducer会为每个CF生成一个HFile。但由

于region的单个文件maxsize有限制
(默认10G,由参数hbase.hregion.max.filesize配置),当发现数据量超过maxsize时则会close当前writer,结束当前文件写入(这个过程简称为roll),再有新的数据需要写入时则new新的writer。那么当任意CF数据量超过maxsize,进行roll操作,其他CF必须同时roll,无论当时它们的数据量如何。


HFileOutputFormat2
的write函数实现的逻辑大致为:

  1. 首先解析传入的数据,得到他的rowKey和family

  2. 根据family找到对应family的writer


  3. 判定 writer
    中数据量是否达到maxsize,若达到置标志位为true。


  4. 标志位为true
    ,判定当前是否为一个新rowKey,如果是,那么将roll所有writer。


  5. 若以上两条有一个为否,则将数据append进 writer
    ,记录当前rowKey。

  6. 重复以上操作,直至所有数据写入完毕。

也就是说在中途要想roll writer必须拿到俩个令牌:


  • 令牌1: 某一个 writer中
    数据量达到maxsize:


    它可以由任意一个 writer
    下发,并且统一发给所有 writer

  • 令牌2: 为新的rowKey:
    通常情况下只要rowKey-F1存在,那么一定是F1能拿到这个令牌。


一旦拿到两个令牌,所有 writer
必须都进入roll流程。

源码中两个令牌判定逻辑如下:

  // 令牌1:If any of the HFiles for the column families has reached maxsize, we need to roll all the writers

  if (wl != null && wl.written + length >= maxsize) {

    this.rollRequested = true;

  }

  // 令牌2:This can only happen once a row is finished though

  if (rollRequested && Bytes.compareTo(this.previousRow, rowKey) != 0) {

    rollWriters();//roll所有writer&释放令牌1

  }


这个过程中是因为同时roll原则,我们才设置了标志位(令牌1), writer
间两个令牌都是共享的,用于判定令牌2 previousRow也是共享的。


于是常常会因为一个 writer
达到最大值而所有writer都要roll再生成新writer,这会有什么问题呢?

碎文件过多。

如果我们有100个family,而只有F1比较大,其他都很小,F1每次写的时候可能其他 writer
都很小,造成的很大的浪费。

在HBase2.0.0 时这个问题发生了划时代的改变,同region的不同family的HFile不必再拥有相同的rowKey分割。
由此上面一个问题也可以得到解决,理论上哪个CF数据量达到maxsize限制,只需将该CF的writer roll即可,其他writer不受影响。
于是在2.0.0版本将上面的代码做如下更改,当拿到双令牌时只roll当前writer。

 //原判定逻辑判定令牌1

  if (wl != null && wl.written + length >= maxsize) {

    this.rollRequested = true;

  }

 //原判定逻辑判定令牌2 

  if (rollRequested && Bytes.compareTo(this.previousRow, rowKey) != 0) {

    rollWriters(wl);//仅将当前writer roll&释放令牌1

  }

这样取消了writer之前的双通关令牌共享,当一个 writer
拿到两个令牌时,不会影响到其他的 writer

我们的问题就发生在这了。


此时,我们的令牌1-标志位this.rollRequested依旧是一块共享令牌,它可以由任意一个 writer
发放给其他所有 writer
,无论其他人的写入量是否达到了最大值,于此同时我们的令牌2只有F1可以获得。

试想一下以下情况,我们有两个CF F1&F2,经过一段时间的数据写入后:

  1. 写入rowKey1-F2:

    F2的 writer
    发现自己超过了maxsize,需要关闭当前writer,向所有 writer
    发放了令牌1–

    this.rollRequested

    true
  2. 此时F2的rowKey因为和它的上一个rowKey一样,无法获得令牌2,只能继续将数据append进当前writer。

  3. 写入rowKey2-F1:

    它是新的rowKey,因为令牌1共享,我们天然的拥有了令牌1,无论此时F1的 writer
    多大,同时我们判定rowKey与上一个不同,执行了F1的 writer

    rollWriters
    ,同时释放了令牌。


  4. 写入

    rowKey2-F2:


    F2的 writer
    发现自己 超过了maxsize
    ,向所有 writer
    再次发放了令牌1,且因为和它的上一个rowKey一样, 只能继续将数据append进当前writer


  5. 写入
    rowKey3-F1:获得

    双令牌,roll当前writer(只有一个值rowKey2-F1)


  6. 写入
    rowKey3-F2:

    发令牌1



    无限循环


最终结果就是一直不该roll的F1- writer
,一直坚持着每个KeyValue进行一次roll;


而一直该roll的F2- writer
一直因为和上一个rowKey一样没办法roll。产生了大量

小文件导致namenode节点内存不足。



三、问题解决

从HBase进化的流程入手,也就不难理解这个问题的出现了,自然也就不难解决这个问题了,方案有两个:

  • PlanA:

    退回到之前HBase1.x.x的版本,所有 writer
    同进同出。

    我们内部考虑使用这个方案,考虑到公司HBase集群多为1.x.x,方便HBase迁移回退,同时我们的CF的不会很多,CF之间的数据量几乎相同差距不大。

  • PlanB:
    将共享变量独立出来。

    令牌1本意是在HBase1.x.x时期用于不同的 writer
    之间通信,但到了2.0.0时代, writer
    独立后便不具备存在意义。

    我们每次只需要现场判断是否达到maxsize,是否为新的rowKey即可(这里理论上在HBase不允许有相同的CF有相同的rowKey,但作为一个对外提供使用的包我们无法做保证所以需要判断rowKey)。

    同时我们需要将preRowKey独立出来,每个 writer
    保存一个。

    一种实现方案如下:
    (提交给社区的就是这个方案)

private final Map<byte[], byte[]> previousRows = new TreeMap(Bytes.BYTES_COMPARATOR); 

... 

if (wl != null && wl.written + length >= maxsize && Bytes.compareTo(this.previousRows.get(family), rowKey) != 0) {

     rollWriters(wl); 

}

...

previousRows.put(family,rowKey);

至此问题解决。

End. 



四、彩蛋


1.  为什么是我们?

要复现这个问题,必须HBase2.0.0或以上+mr这样的组合,加上单HFile大小要超过10G双重禁制,并且还要恰巧先达到10G的不是第一个CF,在日常的任务中可以说是很难碰见了。
偏偏巧合的是我们的Kylin执行任务就是HBase2.0.0+mr,问题就出现了。

2. 怎么找到问题的?


问题排查几经周折,最终在理解了整个执行过程后, 重新编译源码
增加mr任务运行时日志,找到了问题根源。

:tada::tada::tada:

扫码关注