背景
我想以简单的形式在Spark中读取Hbase数据,但是Spark并不支持读取Hbase数据后简单使用。思考能否自己实现这个读取的过程?Hbase的读写API,结果数据往往需要处理后使用。我们是否可以将Hbase结果数据通过转化,直接转化为DataFrame的形式,方便我们使用。如果可行的话,总体思路可以分为几个步骤。1验证下Spark如何读取数据源,2Hbase的数据结构,3如何转化为Spark的数据结构(DataFrame)
读数据简述
1.现有字段信息描述 student_info (id String,name String,age String)
2.在hbase中的存储是没有数据类型的,储存形式为:
ROW | COL+ROW |
---|---|
1000 | column=cf:age, timestamp=1581349656278, value=18 |
1000 | column=cf:id, timestamp=1581349680439, value=1 |
1000 | column=cf:name, timestamp=1581349639526, value=gq |
1001 | column=cf:age, timestamp=1581349697669, value=18 |
1001 | column=cf:id, timestamp=1581349691325, value=2 |
1001 | column=cf:name, timestamp=1581349706198, value=zs |
3.Spark中查询形式为,我们最终的最终效果
package com.asiainfo.hbase
import com.asiainfo.utils.GlobalConfigUtils
import org.apache.spark.sql.{DataFrame, SparkSession}
object TestApp {
def main(args: Array[String]): Unit = {
val session: SparkSession = SparkSession.builder().appName("Spark Example").master("local")
.getOrCreate()
val frame: DataFrame = session.read.
//定义参数 sparkField 就是(id String,name String,age String)
options(Map("sparkField" -> GlobalConfigUtils.getProp("sparkField"))).
//加载我们的自定义实现的类,相当于把自己的逻辑传输给spark
format("com.asiainfo.custom.hbase.DefaultSource").load()
frame.show()
}
}
源码分析
看下Spark是怎么读取Text文件的,最终就能返回DF了
val frame: DataFrame = session.read.text("123.txt")
点击去发现,归根结底调用的是DataFrameReader的load()方法
def load(paths: String*): DataFrame = {
sparkSession.baseRelationToDataFrame(
DataSource.apply(
sparkSession,
paths = paths,
userSpecifiedSchema = userSpecifiedSchema,
className = source,
options = extraOptions.toMap).resolveRelation())
}
里层调用了resolveRelation()方法返回一个BaseRelation对象,外层调用了baseRelationToDataFrame()返回一个DataFrame 那么我们先看看这个resolveRelation干啥了
def resolveRelation(checkFilesExist: Boolean = true): BaseRelation = {
//providingClass.newInstance() 就是我们的提供的数据源类,如果是hbase 需要实现SchemaRelationProvider 中的createRelation方法
val relation = (providingClass.newInstance(), userSpecifiedSchema) match {
case (dataSource: SchemaRelationProvider, Some(schema)) =>
dataSource.createRelation(sparkSession.sqlContext, caseInsensitiveOptions, schema)
case (dataSource: RelationProvider, None) =>
dataSource.createRelation(sparkSession.sqlContext, caseInsensitiveOptions)
case (_: SchemaRelationProvider, None) =>
throw new AnalysisException(s"A schema needs to be specified when using $className.")
case (dataSource: RelationProvider, Some(schema)) =>
val baseRelation =
dataSource.createRelation(sparkSession.sqlContext, caseInsensitiveOptions)
if (baseRelation.schema != schema) {
throw new AnalysisException(s"$className does not allow user-specified schemas.")
}
baseRelation
......
看到其实就是返回了一个baseRelation对象,而且是由providingClass.newInstance()来提供的这个对象的创建方法,providingClass这个对象我们通过调用可以知道,其实它可以是我们自己定义的类,需要实现RelationProvider方法,提供baseRelation对象。所以我这里有思路了,我们需要自己定义一个类A,实现RelationProvider方法,那我看返回的baseRelation被哪里使用到了。顺着代码可以跟踪到,这个对象被传入到了LogicalRelation构造方法中。
override val output: Seq[AttributeReference] = {
val attrs = relation.schema.toAttributes
expectedOutputAttributes.map { expectedAttrs =>
assert(expectedAttrs.length == attrs.length)
attrs.zip(expectedAttrs).map {
// We should respect the attribute names provided by base relation and only use the
// exprId in `expectedOutputAttributes`.
// The reason is that, some relations(like parquet) will reconcile attribute names to
// workaround case insensitivity issue.
case (attr, expected) => attr.withExprId(expected.exprId)
}
}.getOrElse(attrs)
}
并且以val attrs = relation.schema.toAttributes的形式使用到了这个方法。那我们看看BaseRelation方法schema属性是怎么被赋值的。看到类型是一个StructType。构造方法如下
case class StructType(fields: Array[StructField]) {}....
而StructField构造方法。从这里我们知道了。这个schame应该就是有一个个StructField对象封装出来的。而从StructField的构造方法来看,这个对象代表的是SparkSql中的一行行的数据。我们从hbase中取出的数据,只需要封装这个方法即可。
case class StructField(
name: String,
dataType: DataType,
nullable: Boolean = true,
metadata: Metadata = Metadata.empty) {
那我们需要自定义一个类,实现RelationProvider方法,提供一个baseRelation 这个relation 需要我们自己定义,实现baseRelation 类。
具体实现如下
SparkSchema.scala
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.HBaseConfiguration
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.spark.rdd.RDD
import org.apache.spark.sql.{Row, SQLContext}
import org.apache.spark.sql.sources.{BaseRelation, RelationProvider, TableScan}
import org.apache.spark.sql.types.{IntegerType, StringType, StructField, StructType}
import scala.collection.mutable.ArrayBuffer
case class SparkSchema(fieldType: String, fieldName: String) extends Serializable
class SelfHbaseRelation(@transient val sqlContext: SQLContext,
@transient val options: Map[String, String]) extends BaseRelation with TableScan with Serializable {
//1.带有字段类型的 "(id String,name String,age Int)"
private val sparkFieldConf: String = options.getOrElse("sparkField", sys.error("找不到sparkField"))
private val sparkField: Array[SparkSchema] = extractSparkField(sparkFieldConf)
//提前通过已有的结构,定义好schema。其实就是封装好的字段信息 给到spark
override def schema: StructType = {
val rows: Array[StructField] = sparkField.map(field => {
val structField: StructField = field.fieldType.toLowerCase match {
case "string" => StructField(field.fieldName, StringType)
case "int" => StructField(field.fieldName, IntegerType)
}
structField
})
new StructType(rows)
}
//这个方法,会被spark调用,去取的数据,然后与schema结合起来,共同映射为一张表
override def buildScan(): RDD[Row] = {
val hbaseConf: Configuration = HBaseConfiguration.create()
hbaseConf.set("hbase.zookeeper.quorum","jfdacp03:2181,jfdacp04:2181,jfdacp05:2181")//指定zookeeper的地址
hbaseConf.set(TableInputFormat.INPUT_TABLE, "student_info")//指定要查询表名
hbaseConf.set(TableInputFormat.SCAN_CACHEDROWS , "10000")//指定查询时候,缓存多少数据
hbaseConf.set(TableInputFormat.SHUFFLE_MAPS , "1000")
hbaseConf.set("zookeeper.znode.parent","/hbase-unsecure")//hdp 跟cm的zk路径不一致
//通过newAPIHadoopRDD查询
val hbaseRdd: RDD[(ImmutableBytesWritable, Result)] = sqlContext.sparkContext.newAPIHadoopRDD(
hbaseConf,
classOf[TableInputFormat],
classOf[ImmutableBytesWritable],
classOf[Result]
)
val resultRdd: RDD[Result] = hbaseRdd.map(_._2)
//我现在有字段信息了。就应该遍历这个字段信息,然后在hbase中取出相应的值来,然后组装成RDD[row]
//因为要返回的是一个RDD[row] 所以只能使用map
val value: RDD[Row] = resultRdd.map(result => {
val arrayBuffer = new ArrayBuffer[Any]()
//遍历没一个字段,在一个row的result中取出所有字段的值来。最终拼接成一行数据.
//这样一遍 就是一个ROW。ROW其实就是每行的数据 {1,2,3,4,5}
//每一行的数据 使用一个arrayBuffer来接收
sparkField.foreach(field=>{
field.fieldType.toLowerCase() match {
case "string" => arrayBuffer += Bytes.toString(result.getValue(Bytes.toBytes("cf"), Bytes.toBytes(field.fieldName)))
case "int" => arrayBuffer += Bytes.toInt(result.getValue(Bytes.toBytes("cf"), Bytes.toBytes(field.fieldName)))
}
})
Row.fromSeq(arrayBuffer)
})
value
}
private def extractSparkField(sparkFieldConf: String): Array[SparkSchema] = {
val fieldArray: Array[String] = sparkFieldConf.trim.drop(1).dropRight(1).split(",")
val schemas: Array[SparkSchema] = fieldArray.map(field => {
val fieldStr: Array[String] = field.split(" ")
SparkSchema(fieldStr(1), fieldStr(0))
})
schemas
}
}
DefaultSource.scala
import org.apache.spark.sql.SQLContext
import org.apache.spark.sql.sources.{BaseRelation, RelationProvider}
class DefaultSource extends RelationProvider{
override def createRelation(sqlContext: SQLContext, parameters: Map[String, String]): BaseRelation = {
new SelfHbaseRelation(sqlContext,parameters)
}
}