HBase排查|记一次BulkLoad导致的RIT异常

目录结构

  • 1. 前言
  • 2. BulkLoad 导致数据无限膨胀
  • 3. 删表超时导致 RIT
  • 4. 重新规划 Import 的流程
  • 5. 总结

1. 前言

Hadoop 集群的 DataNode 节点严重不均衡,某些节点的磁盘空间占用逼近百分之百,眼看着马上就要爆掉了。
虽然这些 DataNode 节点同时也是 HBase 集群的 RegionServer,但一开始还真没把磁盘的这种异常现象与 HBase 的活动联系起来。随后通过查看 HDFS 的日志,我们才进一步确定了 HBase 的某一个 RegionServer 此时正发生着大量的写入操作。DataNode 日志信息大致如下:

DN_ERROR

从日志中可以看到,HDFS_WRITE 操作的一个 client id 正指向发生异常的那个 DataNode 节点(也即 RegionServer)。其实,在这段时间内,Hadoop 集群上的大 HBase 集群一直在进行着数据同步操作,线上 HBase 集群的表通过 Export/Import 的方式同步进大 HBase 集群,之后,这个大 HBase 集群将作为灾备集群服务于线上的业务。
为了提高 Import 阶段 MR 任务的运行效率,我们改变 Import 时以 HBaseClient 的方式写入集群,而是先用 Import 工具在临时目录中生成每个表的 HFile,最后再使用 BulkLoad 的方式实现快速的数据导入。然而,一系列异常的状况正是发生在 HFIle BulkLoad 的阶段。

2. BulkLoad 导致数据无限膨胀

在确定是 HBase 的问题之后,我们重点排查了正在进行的数据同步流程。Import 的 MR 任务完成之后,HFIle 在临时目录中生成。目录中 HFIle 的文件结构如下图所示:

接着使用 BulkLoad 的命令快速导入数据,对于数据量比较小的表,命令很快就执行成功了,数据也很快加载完毕,而且数据是完整的,只是让我们感到奇怪的一点是,BulkLoad 的命令运行结束后,临时目录中生成的 HFIle 一点也没有减少。

在之前 hive to hbase 的数据同步流程中,我们同样是用 Spark 程序先把 hive 表转换成 HFile,然后用 BulkLoad 的方式导入 HFile,整个流程结束后,临时目录为空,看起来好像是 BulkLoad 的命令做了 Move 操作,把生成的 HFile 直接移动进了 HBase 的数据目录 /hbase/data/table
中,整个过程对 HBase 的影响微乎其微。
这两种方式生成的 HFile 究竟有何异同,目前我们还没有深入研究,而是放在了之后的计划之中。随后,在继续同步几张数据量比较大的表的时候,BulkLoad 命令执行过程中打印了如下警告信息:

bulkload_error
[2020-07-09 05:23:23,452] {bash_operator.py:120} INFO - 20/07/09 05:23:23 INFO client.RpcRetryingCaller: Call exception, tries=16, retries=35, started=1190177 ms ago, cancelled=false, msg=row 'TMofTtwC4hkHMY6Lq0QKew==' on table 'PERSON_INFO:CELL_PERSON_INFO' at region=PERSON_INFO:CELL_PERSON_INFO,TMofTtwC4hkHMY6Lq0QKew==,1594205285703.24c827308174f7ec321180ff59038072., hostname=rs-host,60020,1594199016949, seqNum=2
[2020-07-09 05:23:23,452] {bash_operator.py:120} INFO - 20/07/09 05:23:23 WARN ipc.CoprocessorRpcChannel: Call failed on IOException
[2020-07-09 05:23:23,453] {bash_operator.py:120} INFO - java.net.SocketTimeoutException: callTimeout=1200000, callDuration=1210221: row 'TMofTtwC4hkHMY6Lq0QKew==' on table 'PERSON_INFO:CELL_PERSON_INFO' at region=PERSON_INFO:CELL_PERSON_INFO,TMofTtwC4hkHMY6Lq0QKew==,1594205285703.24c827308174f7ec321180ff59038072., hostname=rs-host,60020,1594199016949, seqNum=2
[2020-07-09 05:23:23,453] {bash_operator.py:120} INFO - at org.apache.hadoop.hbase.client.RpcRetryingCaller.callWithRetries(RpcRetryingCaller.java:169)

看起来像是 bulkload 超时,而程序依旧在执行,不会被中断,而且这张表的数据目录持续膨胀,本来 100 多 G 的数据,bulkload 命令运行半小时之后,竟然膨胀到好几个 T,有些表运行了一晚上膨胀到了几十个 T。查看 HBase 的底层数据目录,发现这些表底层 HFile 的存储格式貌似也有些不太正常。

hfile_big

数据表的 hfile 多了好多_Seq 之类的后缀,且这些文件越来越多,不知道何时才会停止。到了这里,上文描述的某一个 DataNode 磁盘空间占用异常就是这样的文件引起的。对一个 RS 节点产生了这么大影响,当时还以为是预分区过少或没有给表预分区的缘故,继而导致出现了大量的写入热点。后来又根据表的实际数据量,对表做了重新的预分区操作,可 Import 的过程中依旧会出现这样的情况。
目前为止,还没有找到一个合理的解释来完美说明为什么会出现这样的情况。

3 删表超时导致 RIT

针对那些数据量异常的表,在没找到最合适的解决办法之前,只能优先删除,避免撑爆磁盘。Import 成 HFIle 再 Bulkload 数据的方式,貌似也不太靠谱,只能重新以默认方式进行 Import 操作。
就在删除数据同步过程中数据量异常的表时,disable 的命令卡在了客户端迟迟无法返回响应结果,最后抛出类似操作超时的异常。在集群监控页面上可以看到 disable 的 Procedures 一直在运行。

disable 的命令卡住:

监控界面上的 disable Procedures 持续在运行:

由于数据量异常的大,disable 的耗时过长,触发了客户端的操作超时,命令自动中断。此时,黔驴技穷了。看起来只能重启集群了,我们也正是这样做的。而 RIT 正是在此过程中发生,重启集群的过程中 CDH 界面上 Master 进程飙红。CDH 的 Master 进程由于 RIT 的存在而无法正常启动。

在 HBase 自带的监控界面上可以看到发生 RIT 的表的信息。
发生 RIT 的表也是上一个删除操作中超时的数据量异常的表,至于 RIT 发生的根本原因,直至现在也不明其理,只知道上述的种种不正常的操作和表现出来的现象,导致了这个异常。

我们尝试用 hbase hbck
命令来试图修复损坏的 Region。首先运行 hbase hbck 待操作表名
的命令来查看当前表的整体状态。
并摘取 ERROR 的日志信息。

ERROR: Region { meta => ENTCARD:表名,,1591256713254.77eb283b4525e8ce2f0bcf5d27803927., hdfs => hdfs://hdfs-service/hbase/data/ENTCARD/表名/77eb283b4525e8ce2f0bcf5d27803927, deployed => , replicaId => 0 } not deployed on any region server.
20/07/07 21:57:49 INFO util.HBaseFsck: Handling overlap merges in parallel. set hbasefsck.overlap.merge.parallel to false to run serially.
ERROR: There is a hole in the region chain between and . You need to create a new .regioninfo and region dir in hdfs to plug the hole.
ERROR: Found inconsistency in table ENTCARD:表名

从中进一步得到具体的 ERROR: There is a hole in the region chain between and . You need to create a new .regioninfo and region dir in hdfs to plug the hole 的提示信息,该表出现了 Region 空洞,也就是出现了 start key 和 end key 不连续的 region。

关于 region 空洞的修复, hbase hbck
的使用文档中描述的很详细,运行下 hbase hbck -repairHoles 表名

大致说明下该命令的具体作用,也即修复范围。hbase hbck -repairHoles 相当于-fixAssignments -fixMeta -fixHdfsHoles -fixHdfsOrphans 这四个参数同时生效。

  • -fixAssignments 用于修复 Region 分配错误
  • -fixMeta 用于修复.META.表的问题(前提是 HDFS 上的 region 信息是正确的)
  • -fixHdfsHoles 修复 RegionHoles 的问题
  • -fixHdfsOrphans 修复 Orphan Region(HDFS 上没有.regioninfo 的 Region

等待命令运行结束后,再一次使用 hbase hbck 待修复的表名
命令来查看集群的状态。在输出信息的末尾,如果你看到一个大大的 OK,那就证明此次修复是成功的,观察 HBase 的集群监控界面,原有 RIT 的错误提示消失不见,集群恢复健康,此时运行重启命令,集群顺利重启,之前超时的 disable 操作的 Procedures 也消失了,然后就可以成功删除那个异常的表了。

异常的表其实有好几张,之后的删除操作中同样遇到了超时卡住的问题和 RIT,针对后续的 RIT,使用 hbase hbck 表名
得到的异常信息并不都是 RegionHoles。不一样的状态异常如下:

ERROR: Region { meta => person_info:person_info_encrypt,,1591256723291.80e182837f515bfd18c68bd845320875.,
hdfs => hdfs://hdfs-service/hbase/data/person_info/person_info_encrypt/80e182837f515bfd18c68bd845320875,
deployed => centos-bigdata-datanode-ip-.internal,60020,1594127519656;person_info:person_info_encrypt,,1591256723291.80e182837f515bfd18c68bd845320875., replicaId => 0 }
should not be deployed according to META, but is deployed on bigdata-datanode-ip,60020,1594127519656

should not be deployed according to META, but is deployed on
再运行 hbase hbck 的修复命令,提示信息如下:

意思是一个 region close 的关闭任务已经在运行,不能重复提交。
那就只能先重启了,disable 的 Procedures 终止,把这张 RIT 的表直接删掉。

4. 重新规划 Import 的流程

删除掉那些数据量异常的表后,DN 节点的磁盘空间被释放,磁盘的压力也随之消失。原有 Import 成 HFile 再 Bulkload 的方案不能继续用了,只能使用 Import 的默认方式,走 hbase-client API 来写入数据。这种方式写入数据的速度虽然慢,但总不至于引起这样的匪夷所思的异常。
使用 Import 默认的数据导入方式时,还需要注意一个点。对于数据量大的表,创建表时要进行合理的预分区。如果不进行表的预分区操作,数据导入时极大可能会引起写入热点,严重影响数据的读写效率,在极端情况下甚至导致集群的服务挂掉。上述所有操作准备妥当之后,重新调度 Export、Import 的迁移脚本,数据开始稳定导出并写入灾备集群,而且效率也不是很慢。

5. 总结

以上内容林林总总记录了我们此次跨大版本数据迁移过程中遇到的种种问题,文字的力量或许还不足以让我们百分之百重现当时的细枝末节。这里也只是记录了一个大概的过程,中间也遗留了很多亟待解决的问题。
在以往 HBase 的使用经历中,各个场景下的 HBase 的数据迁移,我们几乎都有遇见过。升级 2.1 时候老 HBase 集群同步数据到新集群,主备新集群之间的数据同步,新集群到大 Hadoop 灾备 HBase 集群间同步数据,HIve 大批量离线数据同步到 HBase 集群等等,所依赖的大部分命令其实都源自 HBase 内置的工具命令。但在具体的实施过程中,又会遇到各种各样的问题,版本兼容问题,同步效率、稳定性、流程标准化、自动化调度等等。
其实了解一个工具的使用,最简单有效的方式就是深入源码,在入口函数里你可以找到命令使用的传参细节,在流程代码里你可以剖析工具实现的底层原理,然后是全面而深入的测试,不要忽略细枝末节,甚至在必要时把源码贴出来进行简单的扩展和包装,最后形成一个标准化的解决流程。