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对象会有一个MemStoreScanner和N个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数据本地性的介绍)