22

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

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzU1MDk0OTcyMg%3D%3D&%3Bmid=2247483963&%3Bidx=1&%3Bsn=04d532cb1fa8ee335add11af8ffd5fb8
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

MjYzU3N.jpg!web

总 第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:

扫码关注

6Zvq2yR.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK