HBase作为一种可以进行海量数据存储、并进行高性能读写的NoSQL数据库,在大数据中有着广泛的引用,而Spark作为常用的大数据计算引擎,需要访问存储HBase中的海量数据进行分析处理。那么Spark如何整合HBase来加载HBase中的表,以及将外部数据持久化到HBase,这就是接下来要介绍的两种方式:直接读写HBase和通过Phoenix读写HBase。
加载HBase表并指定过滤条件,转化成RDD,再将RDD转化成Dataset,便可以做一些SQL关联处理。
代码如下:
import org.apache.hadoop.hbase.client., Result
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.spark.sql.{Row, SparkSession}
import org.apache.spark.sql.types.{IntegerType, StringType, StructField, StructType}
def readHBase(spark: SparkSession): Unit = {
val config = HBaseConfiguration.create()
config.set(TableInputFormat.INPUT_TABLE, "test") //表名
config.set(TableInputFormat.SCAN_ROW_START, "start_key") //扫描起始rowKey
config.set(TableInputFormat.SCAN_ROW_STOP, "stop_key") //扫描终止rowKey
//HBase表加载为RDD[(K, V)]
val rdd = spark.sparkContext.newAPIHadoopRDD(config,
classOf[TableInputFormat],
classOf[ImmutableBytesWritable],
classOf[Result]
)
//从Result中获取指定列最新版本的值
//rdd转为RDD[Row]
val rdd1 = rdd.map(m => {
//获取一行查询结果
val result: Result = m._2
val rowKey = Bytes.toString(result.getRow) //获取row key
val userId = Bytes.toString(result.getValue("cf".getBytes,"user_id".getBytes))
val name = Bytes.toString(result.getValue("cf".getBytes,"name".getBytes))
val age = Bytes.toString(result.getValue("cf".getBytes,"age".getBytes))
Row(rowKey, userId, name, age)
})
//创建schema
val schema = StructType(
StructField("user_id", IntegerType, false) ::
StructField("name", StringType, false) ::
StructField("age", IntegerType, true) :: Nil)
//RDD转为DataFrame
val df = spark.createDataFrame(rdd1, schema)
df.select("name", "age")
}
在一些将批量数据导入HBase系统的场景中,如果调用Put API单条导入HBase,很可能会给RegionServer带来较大的写入负载,比如,会出现消耗大量资源(CPU、带宽、IO)、引起RegionServer频繁flush、引起RegionServer频繁GC等问题。而Bulk Load首先是使用MapReduce将待写入数据转换成HFile文件,再直接将这些HFile文件加载到集群中。BulkLoad并没有将写入请求发送给RegionServer,所以并不会出现上述一系列的问题。
代码如下:
import com.alibaba.fastjson.JSON
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.{HFileOutputFormat2, TableInputFormat}
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.hbase.{HBaseConfiguration, KeyValue}
import org.apache.hadoop.mapreduce.Job
import org.apache.spark.sql.{Dataset, SparkSession}
import scala.collection.mutable.ArrayBuffer
def writeHBase(spark: SparkSession, datasetJson: Dataset[String]): Unit = {
//假设datasetJson是一个json字符串类型的Dataset
//结构为"{"name":"", "age":"", "phone":"", "address":"", }"
val ds = spark.read.json(datasetJson)
val rdd = ds.rdd.mapPartitions(iterator => {
iterator.map(m => {
//json字符串解析成JSONObject
val json = JSON.parseObject(m.toString())
//phone作为KeyValue的row key
val phone = json.getString("phone")
//以便遍历其他所有键值对
json.remove("phone")
//键值对字节序列
val writable = new ImmutableBytesWritable(Bytes.toBytes(phone))
//初始化数组, 存储JSONObject中的键值对
val array = ArrayBuffer[(ImmutableBytesWritable, KeyValue)]()
//JSON中的key作为Hbase表中的列名,并按字典序排序
val jsonKeys = json.keySet().toArray
.map(_.toString).sortBy(x => x)
val length = jsonKeys.length
for (i <- 0 until length) {
val key = jsonKeys(i)
val value = json.get(jsonKeys(i)).toString
//KeyValue为HBase中的基本类型Key/Value。
//构造函数中的参数依次为:rowkey、列族、列名、值。
//Json对象中的每个key和其值构成HBase中每条记录的value
val keyValue: KeyValue = new KeyValue(
Bytes.toBytes(phone), //row key
"cf".getBytes(), //列族名
key.getBytes(), //列名
value.getBytes()) //列的值
array += ((writable, keyValue))
}
array
})
//重新分区,减少保存的文件数
//展开数组中的元素
//对rowkey排序
}).repartition(1).flatMap(x => x).sortByKey()
val config = HBaseConfiguration.create()
config.set(TableInputFormat.INPUT_TABLE, "test") //表名
val job = Job.getInstance(config)
job.setMapOutputKeyClass(classOf[ImmutableBytesWritable])
job.setMapOutputValueClass(classOf[KeyValue])
//持久化到HBase表
rdd.saveAsNewAPIHadoopFile("/tmp/test",
classOf[ImmutableBytesWritable],
classOf[KeyValue],
classOf[HFileOutputFormat2],
job.getConfiguration)
}
要使用phoenix-spark插件,需要在pom.xml(Maven工程)文件中添加如下依赖:
<dependency>
<groupId>org.apache.phoenixgroupId>
<artifactId>phoenix-sparkartifactId>
<version>4.14.1-HBase-1.2version>
<scope>providedscope>
dependency>
我这里使用的Spark版本为2.3.1,HBase版本为1.2.0。
import org.apache.hadoop.conf.Configuration
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession
import org.apache.phoenix.spark._
//方法一:使用数据源API加载Phoenix表为一个DataFrame
def readPhoenix(spark: SparkSession): Unit = {
val df = spark.read
.format("org.apache.phoenix.spark")
.options(Map("table" -> "TEST", "zkUrl" -> "host:2181"))
.load()
df.show()
}
//方法二:使用Configuration对象加载Phoenix表为一个DataFrame
def readPhoenix(spark: SparkSession): Unit = {
val conf = new Configuration()
conf.set("hbase.zookeeper.quorum", "hostname:2181")
val df = spark.sqlContext.phoenixTableAsDataFrame(
"test", //表名
Seq("NAME", "AGE"), //指定要加载的列名
predicate = Some("PHONE = 13012340000"), //可设置where条件
conf = conf)
df.show()
}
//方法三:使用Zookeeper URL加载Phoenix表为一个RDD
def readPhoenix(spark: SparkSession): Unit = {
val rdd =spark.sparkContext.phoenixTableAsRDD(
"TEST",
Seq("NAME", "AGE"),
predicate = Some("PHONE = 13012340000"),
zkUrl = Some("hostname:2181") //Zookeeper URL来连接Phoenix
)
rdd.map(_.get(""))
}
创建Phoenix表DDL:
CREATE TABLE TEST(id BIGINT NOT NULL PRIMARY KEY, col1 VARCHAR, col2 INTEGER);
代码如下:
import org.apache.spark.sql.SparkSession
import org.apache.phoenix.spark._
def writePhoenix(spark: SparkSession): Unit = {
val dataSet = List((1L, "1", 1), (2L, "2", 2), (3L, "3", 3))
spark.sparkContext.parallelize(dataSet)
.saveToPhoenix(
"TEST", //表名
Seq("ID","COL1","COL2"), //列命名
zkUrl = Some("host:2181") //Zookeeper URL
)
}
代码如下:
def writePhoenix(spark: SparkSession): Unit = {
val df = spark.read
.format("org.apache.phoenix.spark")
.options(Map("table" -> "TEST", "zkUrl" -> "host:2181"))
.load()
//方法一
df.saveToPhoenix(Map("table" -> "TEST", "zkUrl" -> "host:2181"))
//方法二
df.write
.format("org.apache.phoenix.spark")
.mode("overwrite")
.option("table", "OUTPUT_TABLE")
.option("zkUrl", "host:2181")
.save()
}