HBase拉取数据和Spark拉取数据的区别

HBase架构图如下所示

HMaster:一个集群中有多个HMaster,但是只有一个处于Active状态。HMaster通过连接Zookeeper感知集群上哪些HRegionServer是活着的及它们所在的位置,然后管理RegionServer。

HRegionServer:就是一台服务器,一个RS会管理多个HRegion,每个HRegion有多个Store,每个Store对应一个列簇。

我的疑问是,如果是单个Scan,要获取HBase集群上多个Region中的数据的话,下面两种做法分别是怎么做的?

 

使用Java API话,HBase集群是如何读取数据的

使用Java Client的话,Scan操作并不是发送一个命令就直接拿到所有数据了,它的工作原理实际上如下所示:

客户端这边的for循环中,每获取一条记录都会调用一次next请求。如果客户端缓存中还有未读的数据,那么就返回未读的数据,否则的话需要将请求发送往服务端,服务端扫描到数据然后再返回给客户端。

需要注意的是,Scan不是并发执行,如果数据分布在多个Region中,Scan是逐个去Region中获取数据。这个和批量的Get不同,批量的Get是会按照Region分组然后并发执行的。

 

Scan在服务端执行的流程是这样的:

Scan并非并行执行,它会先去Memstore中去找,再从blockcache中找,还找不到就会去hfile中找,看被指把结果放到blockCache中,blockcache是一个LRU,存储空间达到上限时会淘汰掉最老的一批数据。

客户端根据配置的地址连接上ZK,然后从ZK中加载hbase元数据信息(第一次请求时连接一次即可)。如果集群发生某些变化导致hbase:meta元数据更改,客户端请求就会发生异常,这个时候会再去ZK上加载一份最新的元数据信息到本地

Scan执行的时候并非并发,而是一个Region一个Region扫描的,扫描完第一个Region之后,构建新的nextScanner的时候会重置startkey,以寻找下一个region,知道循环达到endKey的位置。

扫描一个Region的话会构造一个RegionScanner,以后查这个Region的数据都是通过它来完成。RegionScanner底下还有StoreScanner和StoreFileScanner。一张表有多少个列簇就有多少个StoreScanner。因为HDFS块大小的限制,每个列簇会分成多个Store,每个Store又是有内存中的MemStore和磁盘上的StoreFile组成,所以每个StoreScanner对象会有一个MemStoreScannerN个StoreFileScanner来进行实际数据的获取。

具体怎么从HFile中获取数据以后再分析吧,可以提下是通过HFile的索引结构定位具体的Block以及rowKey…

获取的时候会将该Store中的所有StoreFileScanner和MemStoreScanner合并成一个heap(最小堆),让这些Scanner从小到大排序是为了数据能够从小到大获取,

 

使用Spark的话,Spark是如何分布式读取的:

从HBase拉取数据的话,一般用的都是newAPIHadoopRDD方法,以它为例进行分析。可以看出该方法的入参一共有4个:

Configuration: 任务的配置

fClass: 即InputFormat,定义规则,拆分数据

kClass: key类型

vClass: value类型(有点像M/R任务)

这里的getLocal看解释是为了强制加载hdfs-site.xml,避免HDFS在HA的模式下出错,为什么这么做还不是很明白。

总之,是将conf构造成M/R任务的JobConf并加入HDFS的认证,然后构造NewHadoopRDD。

 

构造NewHadoopRDD的核心函数有三个:

getPartitions  用于创建HadoopRDD的分区

getPreferredLocations 寻找每个Partition(或者说split)数据本地性

compute 对每一个Partition进行计算,得出一个可遍历的结果(其实并不是得到一个结果集,后面会分析到)

我们对这些方法一个一个进行分析

 

首先是getPartitions():

核心其实就是一句话:inputFormat.getSplits(jobContext).toArray

本人使用的是TableInputFormat,点进去看下它的getSplits()方法

发现它调用的是父类的getSplits()方法,再点进去看下,其实看方法的注释就可以看明白了,table有多少个region,就会有多少个splits。

感觉这里是一个缺点:每次都会计算这张表里面每个region的大小,不知道为啥要这么做,但感觉这样是没有必要的(这段看法是错误的,后续又补充)。我自身的这张表的Region看了下,确实是179个。

 

 

接着是getPreferredLocations():

从这段代码可以看出来,对于localhost的host,是没有PreferredLocation的,这个会把对应于该partition的task追加到no_prefs的任务队列中,进行相应data locality的任务调度(这一部分的代码暂时还没看到,需要后续确认下)

 

最后就是一个compute()了:

这里的compute其实就是RDD的compute方法。和RDD指定的泛型不同的是,NewHadoopRDD返回的是InterruptibleIterator[(K, V)]迭代器。

也就是说compute并不是真正返回结果集合,compute方法是在遍历每一行数据时嵌套一个函数进行处理。在这里是根据分片的信息生成遍历数据的Itreable接口。(如果是普通RDD的话,有父RDD的情况下,那就是给父RDD的Iterable接口上给遍历每个元素的时候再套上一个方法)

 

从以上方法中可以看出,compute会根据输入的split类型创建对应的RecordReader,用于读取分片中的数据。并定义了hasNext和next方法,用于逐条逐条的获取数据,这样就完成Partition数据的计算。

 

 

补充,为什么要计算Region的大小:

    又一个参数的设置hbase.mapreduce.input.autobalance,这个参数可以适当的调节Region之间大小不均衡的问题,避免某个Region过大加载的时候出问题(内存溢出或者耗时久):

    它会计算 long averageRegionSize = totalRegionSize / splits.size();

    然后调用calculateRebalancedSplits()方法,遍历splits,根据评价Region的大小对大的Region(默认3倍平均Region的大小算作大Region)进行切分(但是只会切分一次,即分成两个Region来加载),对一些小的Region进行合并(几个小的Region一起加载):

    如果getSplitKey()之后Region还是很大,个人想到的是不如直接重写下getSplit()方法,按照HFile文件加载。

    如果还是太大,可以参考HBase在split时候的逻辑,获取到HFile的midKey对HFile进行切割。

    如果切割之后的HFile还是太大,不如把所有的RowKey都搂出来,然后自行分组遍历,如果1000个RowKey作为一组遍历。

 

    

 

 

参考:

https://www.jianshu.com/p/e2bbf23f1ba2(HBase架构图理解)

https://www.jianshu.com/p/c7bc56fe93c3(Caching/Batch/MaxResultSize)

https://www.cnblogs.com/duanxz/p/6339585.html(RDD数据本地性的介绍)

 

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值