7

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

 收集于6天前 阅读数 7
以下为 快照 页面,建议前往来源网站查看,会有更好的阅读体验。
原文链接: http://mp.weixin.qq.com/s?__biz=MzU1MDk0OTcyMg%3D%3D&%3Bmid=2247483963&%3Bidx=1&%3Bsn=04d532cb1fa8ee335add11af8ffd5fb8

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


猜你喜欢

关于极客头条


聚合每日国内外有价值,有趣的链接。