文章目录
读取
利用TableInputFormat读取
Base64.Encoder base64Encoder = Base64.getEncoder();
String sparkMaster = "local";
String zkQuorum = "master";
String zkClientPort = "2181";
String tableName = "KITTI";
byte[] startRowKey = Bytes.toBytes("0r");
byte[] endRowKey = Bytes.toBytes("9r");
SparkSession sparkSession = SparkSession.builder().master(sparkMaster).getOrCreate();
Configuration hconf= HBaseConfiguration.create();
hconf.set("hbase.zookeeper.property.clientPort", zkClientPort);
hconf.set("hbase.zookeeper.quorum",zkQuorum);
//设置读取HBase表的名称
hconf.set(TableInputFormat.INPUT_TABLE, tableName);
//设置读取HBase表scan的数据范围
Scan scan = new Scan();
scan.withStartRow(startRowKey, true);
scan.withStopRow(endRowKey, true);
hconf.set(TableInputFormat.SCAN, base64Encoder.encodeToString(ProtobufUtil.toScan(scan).toByteArray()));
JavaRDD<Tuple2<ImmutableBytesWritable, Result>> hbaseRDD = sparkSession.sparkContext()
.newAPIHadoopRDD(hconf, TableInputFormat.class, ImmutableBytesWritable.class, Result.class).toJavaRDD();
TableInputFormat原理
TableInputFormat通过hbase scan获取数据,SparkRDD采用的分区策略就是根据region的数量,决定partition的数量,hbase一个region对应一个RDD partition。
1. 数据分区数目:
- oneInputSplitPerRegion()方法将每一个Region作为一个数据分区InputSplit。
将Region转换为InputSplit时会判断Region的startEndKey和Scan 的startEndRowKey是否有重合区域,如果没有,则舍弃此Region。并且利用Region的startEndKey和Scan 的startEndRowKey的交集作为TableSplit数据的splitStart, splitStop。
-
如果用户定义了hbase.mapreduce.tableinput.mappers.per.region参数(nSplitsPerRegion),则会调用createNInputSplitsUniform,将上述生成的每个InputSplit平均切分成nSplitsPerRegion个InputSplit。
-
如果用户定义了hbase.mapreduce.tif.input.autobalance参数, 则会调用calculateAutoBalancedSplits将上述生成的InputSplit进行动态调整,使得每个InputSize的大小接近hbase.mapreduce.tif.ave.regionsize参数。注意,这不会改变InputSplit的数目。
- 分区数据读取
使用TableRecordReader读取分区数据,并设置InputSplit的splitStart和splitEnd作为TableRecordReader起终点。TableRecordReader利用TableRecordReaderImpl实现。
TableRecordReaderImpl利用传入的Scan和表名,获取Hbase数据表的ResultScanner,利用ResultScanner获取该分区的数据记录。
值得一提的是HBase在进行数据scan的时候,会调用StoreFileScanner和MemstoreScanner两个扫描器。其中StoreFileScanner可通过StoreFile的索引定位到待查找范围所在的block,从而直接读取StoreFile中对应的block,而不需要遍历整个StoreFile文件。
利用TableSnapshotInputFormat读取
首先必须要为读取的HBase的数据表创建快照:
hbase shell :> snapshot ‘tableName’,‘snapshotName’
Base64.Encoder base64Encoder = Base64.getEncoder();
String sparkMaster = "local";
String zkQuorum = "master";
String zkClientPort = "2181";
//byte[] startRowKey = Bytes.toBytes("0r");
//byte[] endRowKey = Bytes.toBytes("9r");
String hbaseRootDir = "hdfs://master:9000/HBase_DB";
String snapshotRestoreDir = "hdfs://master:9000/snapshot_restore";//spark读取snapshot时,会将snapshot被复制到该文件夹,文件夹必须和hbaseRootDir处于相同的文件系统
String snapName ="KITTI_snapshot";
SparkSession sparkSession = SparkSession.builder().master(sparkMaster).getOrCreate();
Configuration hconf= HBaseConfiguration.create();
hconf.set("hbase.rootdir",hbaseRootDir);
hconf.set("hbase.zookeeper.property.clientPort", zkClientPort);
hconf.set("hbase.zookeeper.quorum",zkQuorum);
//设置读取Hfile的scan的数据范围
Scan scan = new Scan();//必须要创建Scan
/* scan.withStartRow(startRowKey, true);
scan.withStopRow(endRowKey, true);*/
hconf.set(TableInputFormat.SCAN, base64Encoder.encodeToString(ProtobufUtil.toScan(scan).toByteArray()));
//设置要读取的hbase表快照
Job job = Job.getInstance(hconf);
Path path = new Path(snapshotRestoreDir);
TableSnapshotInputFormat.setInput(job, snapName, path);
JavaRDD<Tuple2<ImmutableBytesWritable, Result>> hbaseRDD = sparkSession.sparkContext()
.newAPIHadoopRDD(job.getConfiguration(), TableSnapshotInputFormat.class, ImmutableBytesWritable.class, Result.class).toJavaRDD();
System.out.println("分区数目:" + hbaseRDD.partitions().size());
TableSnapshotInputFormat原理
hbase快照原理:https://www.jianshu.com/p/8d091591d872
-
数据分区数目
基本和TableInputFormat相似,都是一般情况下一个region对应一个数据分区,如果定义了hbase.mapreduce.tableinput.mappers.per.region参数,则会平均切分。同样也会使用Scan范围过滤无交集的Region,以及利用Region的startEndKey和Scan 的startEndRowKey的交集作为InputSplit数据的splitStart, splitStop。
-
分区数据读取
利用ClientSideRegionScanner实现分区数据读取。利用Hbase snapshot中各Region、HFile的位置,创建RegionScanner,直接读取HDFS上该Region的HFile文件获取数据,而不是再是借用HBase的scan API获取数据(scan API利用RegionServer进程读取HFile再传回Spark Task,这里是Spark Task直接读取Hfile)。
利用Spark-Hbase-connector读取
介绍:
https://blog.cloudera.com/spark-hbase-dataframe-based-hbase-connector/
使用方法:
https://hbase.apache.org/book.html#_sparksqldataframes
Spark-Hbase-connector原理
扩展了Spark DataSource API。
hbase-spark集成应用了关键技术,如分区剪枝、列剪枝、谓词下推和数据局部性。
创建org.apache.hadoop.hbase.spark.DefaultSource和HBaseRelation。
DefaultSource继承自 RelationProvider接口,实现createRelation方法,创建返回HBaseRelation对象。
HBaseRelation继承 BaseRelation类,实现PrunedFilteredScan接口,实现buildScan方法创建返回RDD<Row>。
buildScan首选将过滤条件进行谓词下推。pushDownRowKeyFilter用于过滤不在查询范围内的region。创建HBaseTableScanRDD,并将pushDownRowKeyFilter的查询范围添加到HBaseTableScanRDD的ranges变量中。
-
数据分区数目
HBaseTableScanRDD的getPartitions方法首先会获取HBase表的所有region。将每个region的范围(start/endKey)与ranges求交,最后获得HBaseScanPartition。HBase一个region对应RDD一个Partition。
如果region的范围(start/endKey)与ranges中的范围无交集,则返回null,否则返回交集。从而可以过滤掉无 关的region。
-
分区数据读取
同样是利用HBase scan API获取读取分区数据。
总结
以上三种方式Spark读取HBase得到的RDD的分区均与Hbase的Region一一对应,且都可以使用scan或者SQL where的方式,传入rowkey scan范围,过滤无关的Hbase Region和Row,减少RDD的分区数量和Row的数量。
第一种使用Spark core的API 和Hbase scan API读取HBsae数据。
第二种使用Spark core的API 和Hbase snapshot、Hfile,利用Spark Task直接读取Hfile文件,减轻了对Hbase region server的负担,一定程度上提高了读取效率,不需要通过RegionServer间接获取数据。但是读取Hbase之前,需要对相关表做snapshot。
e scan API读取HBsae数据。
第二种使用Spark core的API 和Hbase snapshot、Hfile,利用Spark Task直接读取Hfile文件,减轻了对Hbase region server的负担,一定程度上提高了读取效率,不需要通过RegionServer间接获取数据。但是读取Hbase之前,需要对相关表做snapshot。
第三种使用SparkSQL DataSource API和Hbase scan API,可以使用SparkSQL的各种优化措施,如谓词下推,二进制内存优化等,效率应该要好于第一种方式。