在网上看到了一篇文章说到了HBase的BulkLoad机制,从文章中看出BulkLoad是不走HBase的服务端的,直接和Hadoop打交道将数据直接生成HFile,然后将文件直接移到Region下面。如下图所示:
相比较于直接写HBase,BulkLoad主要是绕过了写WAL日志这一步,还有写Memstore和Flush到磁盘,从理论上来分析性能会比Put快!
Spark+BulkLoad往HBase中导入数据:
//HFileOutputFormat要求行键有序: rowKey + 列族 + 列名 整体有序
val hfileRDD = df.rdd.map(row => {
//OneRecord(rowKey: String, plateNo: String, plateColor: Int, passTime: Long, crossingId: Int)
val rowKeyBytes = Bytes.toBytes(row.getString(0))
//这里控制 列族 + 列名 的排序
new ImmutableBytesWritable(rowKeyBytes) ->
new KeyValue(rowKeyBytes, family, column1, Bytes.toBytes(row.getString(1)))
//这里控制行键的排序
}).sortByKey()
val hfileInHDFSPath = "hdfs://*IP*:8020/tmp/Test"
hfileRDD.saveAsNewAPIHadoopFile(hfileInHDFSPath,
classOf[ImmutableBytesWritable],
classOf[KeyValue],
classOf[HFileOutputFormat2],
sparkSession.sparkContext.hadoopConfiguration
)val load = new LoadIncrementalHFiles(sparkSession.sparkContext.hadoopConfiguration)n
val table = connection.getTable(TableName.valueOf("TEST"))//创建BulkLoad作业将数据写入到HBase
val job = Job.getInstance(sparkSession.sparkContext.hadoopConfiguration)
job.setJobName("Test")
job.setMapOutputKeyClass(classOf[ImmutableBytesWritable])
job.setMapOutputValueClass(classOf[KeyValue])
HFileOutputFormat2.configureIncrementalLoadMap(job, table)
load.doBulkLoad(new Path(hfileInHDFSPath), table.asInstanceOf[HTable])
table.close()
connection.close()
一些有意思的点分析:
1.需要对RDD中的数据手动排序
使用Put写到HBase中的时候HBase会自动进行排序,但是自己生成的HFile文件不会自动排序,所以需要手动排序好,先主键再列族再列这样的方式进行排序,不然会报错。
2.KeyValue格式的时间戳会自动赋值
new KeyValue的时候默认会给这个cell一个时间戳,默认是Long.MaxValue:
但是写的时候会更新为当前时间,从HBase中来看确实也是这样的:
从KeyValue对象的构造函数中可以清楚的看到,每个KeyValue对象中都包含rowKey,所以一直说rowKey不应太大,不然每条数据都会很大:
3.BulkLoad是怎么进行的?
其实BulkLoad分成客户端和服务端两步操作,先来看客户端干了啥:
客户端主要是为每个HFile创建了一个LoadQueueItem结构,然后判断HFile是否过大或者跨约了多个Region,如果是则要对HFile文件进行拆分。然后将属于同一个Region的文件记录到一起:
/**
* @return A Multimap<startkey, LoadQueueItem> that groups LQI by likely
* bulk load region targets.
*/
接着发送请求给服务端,请求服务端进行挂载:
接下来就是服务端的操作了
服务端操作
RSRpcService收到BulkLoad请求之后,会调用HRegion中的bulkLoadHFiles方法:
同时进行multi-family bulk load需要获取writeLock
向一个region里面load的可能会有多个列族,不同的列族对应不用HStore,需要分别load。写的时候还要flushcache(),保证HFile SeqId不重复:
正式Load的时候,如果文件与当前hbase所在的文件系统不是同一个文件系统,那么则会把HFile copy到当前的文件系统中:
真正的Load就是一个对文件rename的操作…就不细说了
个人理解是,如果是现成的大批量数据的导入操作,比如做数据级联,尽量使用BulkLoad,比Spark直接写入HBase性能要高。咨询过同事,他给了我如下几个他的建议:
1.导入数据的表要做好预拆分Region,并且HFile文件要控制在Region内,不要让BulkLoad进行拆分
2.HBase和Hadoop尽量在同一套环境中,以免影响挂载性能。
3.生成的HFile要尽量避免合并,以免影响HBase服务(这点厚点要研究下如何做到,生成什么样的HFile的情况下会进行合并)
参考:
https://www.cnblogs.com/smartloli/p/9501887.html(BulkLoad示意图)
https://www.2cto.com/net/201710/692437.html(HFile手工排序)