SparkSQL基于DataSourceV2自定义数据源
版本说明:Spark 2.3
前言:之前在SparkSQL数据源操作文章中整理了一些SparkSQL内置数据源的使用,总的来说SparkSQL支持的数据源还是挺丰富的,但业务上可能不拘束于这几种数据源,比如将HBase作为SparkSQL的数据源,REST数据源等。这里主要讲一下在Spark2.3版本之后推出的DataSourceV2,基于DataSourceV2实现自定义数据源
1 DataSourceV1 VS DataSourceV2
自Spark1.3版本之后,引入了数据源API,我们可以实现自定义数据源。2.3版本之后又引入的新版API,关于V1与V2的区别以及使用可以参考与https://developer.ibm.com/code/2018/04/16/introducing-apache-spark-data-sources-api-v2/这两篇文章。这里简单的总结一下V1的缺点,以及V2的新特性。
1.1 DataSourceV1缺点
- 依赖上层API
- 难以添加新的优化算子
- 难以传递分区信息
- 缺少事务性的写操作
- 缺少列存储和流式计算支持
1.2 DataSourceV2优点
- DataSourceV2 API使用Java编写
- 不依赖于上层API(DataFrame/RDD)
- 易于扩展,可以添加新的优化,同时保持向后兼容
- 提供物理信息,如大小、分区等
- 支持Streamin Source/Sink
- 灵活、强大和事务性的写入API
1.3 Spark2.3中V2的功能
- 支持列扫描和行扫描
- 列裁剪和过滤条件下推
- 可以提供基本统计和数据分区
- 事务写入API
- 支持微批和连续的Streaming Source/Sink
2 基于DataSourceV2实现输入源
SparkSQL的DataSourceV2的实现与StructuredStreaming自定义数据源如出一辙,思想是一样的,但是具体实现有所不同,主要步骤如下:
第一步:继承DataSourceV2和ReadSupport创建XXXDataSource类,重写ReadSupport的creatReader方法,用来返回自定义的DataSourceReader类,如返回自定义XXXDataSourceReader实例
第二步:继承DataSourceReader创建XXXDataSourceReader类,重写DataSourceReader的readSchema方法用来返回数据源的schema,重写DataSourceReader的createDataReaderFactories用来返回多个自定义DataReaderFactory实例
第三步:继承DataReaderFactory创建DataReader工厂类,如XXXDataReaderFactory,重写DataReaderFactory的createDataReader方法,返回自定义DataRader实例
第四步:继承DataReader类创建自定义的DataReader,如XXXDataReader,重写DataReader的next()方法,用来告诉Spark是否有下条数据,用来触发get()方法,重写DataReader的get()方法获取数据,重写DataReader的close()方法用来关闭资源
2.1 继承DataSourceV2和ReadSupport创建XXXDataSource类
这里以创建CustomDataSourceV2类为例
2.1.1 创建CustomDataSourceV2类
/**
* 创建DataSource提供类
* 1.继承DataSourceV2向Spark注册数据源
* 2.继承ReadSupport支持读数据
*/
class CustomDataSourceV2 extends DataSourceV2
with ReadSupport {
// todo
}
2.1.2 重写ReadSupport的createReader方法
该方法用来返回一个用户自定义的DataSourceReader实例
/**
* 创建Reader
*
* @param options 用户定义的options
* @return 自定义的DataSourceReader
*/
override def createReader(options: DataSourceOptions): DataSourceReader = new CustomDataSourceV2Reader(options)
2.2 继承DataSourceReader创建XXXDataSourceReader类
该类用来自定义DataSourceReader,需要继承DataSourceReader,并重写readSchema和createDataReaderFactories方法。
2.2.1 创建CustomDataSourceV2Reader类
/**
* 自定义的DataSourceReader
* 继承DataSourceReader
* 重写readSchema方法用来生成schema
* 重写createDataReaderFactories,用来根据条件,创建多个工厂实例
*
* @param options options
*/
class CustomDataSourceV2Reader(options: DataSourceOptions) extends DataSourceReader {
// Override some functions
}
2.2.2 重写DataSourceReader的readSchema方法
该方法用来返回数据源的schema
/**
* 生成schema
*
* @return schema
*/
override def readSchema(): StructType = ???
2.2.3 重写DataSourceReader的createDataReaderFactories方法
实现该方法,可以根据不同的条件,创建多个createDataReader工厂实例,用来并发获取数据?(暂且这么理解的,或者是按照分区获取数据?)
/**
* 创建DataReader工厂实例
*
* @return 多个工厂类实例
*/
override def createDataReaderFactories(): util.List[DataReaderFactory[Row]] = {
import collection.JavaConverters._
Seq(
new CustomDataSourceV2ReaderFactory().asInstanceOf[DataReaderFactory[Row]]
).asJava
}
2.3 继承DataReaderFactory创建DataReader工厂类
该类是DataReader的工厂来,用来返回DataReader实例
2.3.1 创建CustomDataSourceV2Factory类
/**
* 自定义DataReaderFactory类
*/
class CustomDataSourceV2ReaderFactory extends DataReaderFactory[Row] {
// Override some functions
}
2.3.2 重写DataReaderFactory的createDataReader方法
该方法用来实例化自定义的DataReader
/**
* 重写createDataReader方法,用来实例化自定义的DataReader
*
* @return 自定义的DataReader
*/
override def createDataReader(): DataReader[Row] = new CustomDataReader
2.4 继承DataReader类创建自定义的DataReader
该类为重点实现部分,用来自定义获取数据的方式
2.4.1 创建CustomDataReader类
/**
* 自定义DataReader类
*/
class CustomDataReader extends DataReader[Row] {
// Override some functions
}
2.4.2 重写CustomDataReader的next()方法
该方法返回一个布尔值,来告诉Spark是否含有下条数据,以便触发get()方法获取数据
/**
* 是否有下一条数据
*
* @return boolean
*/
override def next(): Boolean = ???
2.4.3 重写CustomDataReader的get()方法
该方法用来获取数据,返回类型是在继承DataReader时指定的泛型
/**
* 获取数据
* 当next为true时会调用get方法获取数据
*
* @return Row
*/
override def get(): Row = ???
2.4.4 重写CustomDataReader的close()方法
该方法用来关闭相应的资源
/**
* 关闭资源
*/
override def close(): Unit = ???
2.5 以REST为例,实现自定义的数据源
这里主要是从REST接口里获取JSON格式的数据,然后生成DataFrame数据源
2.5.1 创建RestDataSource类
class RestDataSource extends DataSourceV2 with ReadSupport with WriteSupport {
override def createReader(options: DataSourceOptions): DataSourceReader =
new RestDataSourceReader(
options.get("url").get(),
options.get("params").get(),
options.get("xPath").get(),
options.get("schema").get()
)
}
2.5.2 创建RestDataSourceReader类
/**
* 创建RestDataSourceReader
*
* @param url REST服务的的api
* @param params 请求需要的参数
* @param xPath JSON数据的xPath
* @param schemaString 用户传入的schema字符串
*/
class RestDataSourceReader(url: String, params: String, xPath: String, schemaString: String)
extends DataSourceReader {
// 使用StructType.fromDDL方法将schema字符串转成StructType类型
var requiredSchema: StructType = StructType.fromDDL(schemaString)
/**
* 生成schema
*
* @return schema
*/
override def readSchema(): StructType = requiredSchema
/**
* 创建工厂类
*
* @return List[实例]
*/
override def createDataReaderFactories(): util.List[DataReaderFactory[Row]] = {
import collection.JavaConverters._
Seq(
new RestDataReaderFactory(url, params, xPath).asInstanceOf[DataReaderFactory[Row]]
).asJava
}
}
2.5.3 创建RestDataReaderFactory
/**
* RestDataReaderFactory工厂类
*
* @param url REST服务的的api
* @param params 请求需要的参数
* @param xPath JSON数据的xPath
*/
class RestDataReaderFactory(url: String, params: String, xPath: String) extends DataReaderFactory[Row] {
override def createDataReader(): DataReader[Row] = new RestDataReader(url, params, xPath)
}
2.5.4 创建RestDataReader
/**
* RestDataReader类
*
* @param url REST服务的的api
* @param params 请求需要的参数
* @param xPath JSON数据的xPath
*/
class RestDataReader(url: String, params: String, xPath: String) extends DataReader[Row] {
// 使用Iterator模拟数据
val data: Iterator[Seq[AnyRef]] = getIterator
override def next(): Boolean = {
data.hasNext
}
override def get(): Row = {
val seq = data.next().map {
// 浮点类型会自动转为BigDecimal,导致Spark无法转换
case decimal: BigDecimal =>
decimal.doubleValue()
case x => x
}
Row(seq: _*)
}
override def close(): Unit = {
println("close source")
}
def getIterator: Iterator[Seq[AnyRef]] = {
import scala.collection.JavaConverters._
val res: List[AnyRef] = RestDataSource.requestData(url, params, xPath)
res.map(r => {
r.asInstanceOf[JSONObject].asScala.values.toList
}).toIterator
}
}
2.5.5 测试RestDataSource
object RestDataSourceTest {
def main(args: Array[String]): Unit = {
val spark = SparkSession
.builder()
.master("local[2]")
.appName(this.getClass.getSimpleName)
.getOrCreate()
val df = spark.read
.format("com.hollysys.spark.sql.datasource.rest.RestDataSource")
.option("url", "http://model-opcua-hollysysdigital-test.hiacloud.net.cn/aggquery/query/queryPointHistoryData")
.option("params", "{\n \"startTime\": \"1543887720000\",\n \"endTime\": \"1543891320000\",\n \"maxSizePerNode\": 1000,\n \"nodes\": [\n {\n \"uri\": \"/SymLink-10000012030100000-device/5c174da007a54e0001035ddd\"\n }\n ]\n}")
.option("xPath", "$.result.historyData")
//`response` ARRAY<STRUCT<`historyData`:ARRAY<STRUCT<`s`:INT,`t`:LONG,`v`:FLOAT>>>>
.option("schema", "`s` INT,`t` LONG,`v` DOUBLE")
.load()
df.printSchema()
df.show(false)
}
}
3 基于DataSourceV2实现输出源
基于DataSourceV2实现自定义的输出源,需要以下几个步骤:
第一步:继承DataSourceV2和WriteSupport创建XXXDataSource,重写createWriter方法用来返回自定义的DataSourceWriter
第二步:继承DataSourceWriter创建XXXDataSourceWriter类,重写createWriterFactory返回自定义的DataWriterFactory,重写commit方法,用来提交整个事务。重写abort方法,用来做事务回滚
第三步:继承DataWriterFactory创建XXXDataWriterFactory类,重写createWriter方法返回自定义的DataWriter
第四步:继承DataWriter创建XXXDataWriter类,重写write方法,用来将数据写出,重写commit方法用来提交事务,重写abort方法用来做事务回滚
3.1 继承DataSourceV和WriterSupport创建XXXDataSource类
3.1.1 创建CustomDataSourceV2类
/**
* 创建DataSource提供类
* 1.继承DataSourceV2向Spark注册数据源
* 2.继承WriteSupport支持读数据
*/
class CustomDataSourceV2 extends DataSourceV2
with WriteSupport {
// todo
}
3.1.2 重写createWriter方法
/**
* 创建Writer
*
* @param jobId jobId
* @param schema schema
* @param mode 保存模式
* @param options 用于定义的option
* @return Optional[自定义的DataSourceWriter]
*/
override def createWriter(jobId: String,
schema: StructType,
mode: SaveMode,
options: DataSourceOptions): Optional[DataSourceWriter] = Optional.of(new CustomDataSourceV2Writer)
3.2 继承DataSourceWriter创建XXXDataSourceWriter类
3.2.1 创建CustomDataSourceV2Writer
需要继承DataSourceWriter
/**
* 自定义DataSourceWriter
* 继承DataSourceWriter
*/
class CustomDataSourceV2Writer extends DataSourceWriter {
// Override some functions
}
3.3 继承DataWriterFactory创建XXXDataWriterFactory类
3.3.1 创建CustomDataWriterFactory
class CustomDataWriterFactory extends DataWriterFactory[Row] {
// Override some functions
}
3.3.2 重写createDataWriter方法
该方法返回一个自定义的DataWriter
/**
* 创建DataWriter
*
* @param partitionId 分区ID
* @param attemptNumber 重试次数
* @return DataWriter
* 每个分区创建一个RestDataWriter实例
*/
override def createDataWriter(partitionId: Int, attemptNumber: Int): DataWriter[Row] = ???
3.4 继承DataWriter创建XXXDataWriter类
3.4.1 创建CustomDataWriter类
class CustomDataWriter extends DataWriter[Row] {
// Overrride some functions
}
3.4.2 重写write方法
该方法用来写出单条数据,每条数据都会触发该方法
/**
* write
*
* @param record 单条记录
* 每条记录都会触发该方法
*/
override def write(record: Row): Unit = ???
3.4.3 重写commit方法
该方法一般用于事务提交,每个分区触发一次
/**
* commit
*
* @return commit message
* 每个分区触发一次
*/
override def commit(): WriterCommitMessage = ???
3.4.4 重写abort方法
该方法用于事务回滚,当write方法发生异常之后触发该方法
/**
* 回滚:当write发生异常时触发该方法
*/
override def abort(): Unit = ???
4 完整代码
4.1 自定义DataSource示例代码:
package com.hollysys.spark.sql.datasource
import java.util
import java.util.Optional
import org.apache.spark.sql.{Row, SaveMode}
import org.apache.spark.sql.sources.v2.reader.{DataReader, DataReaderFactory, DataSourceReader}
import org.apache.spark.sql.sources.v2.writer.{DataSourceWriter, DataWriter, DataWriterFactory, WriterCommitMessage}
import org.apache.spark.sql.sources.v2.{DataSourceOptions, DataSourceV2, ReadSupport, WriteSupport}
import org.apache.spark.sql.types.StructType
/**
* @author : shirukai
* @date : 2019-01-30 10:37
* Spark SQL 基于DataSourceV2接口实现自定义数据源
*/
/**
* 创建DataSource提供类
* 1.继承DataSourceV2向Spark注册数据源
* 2.继承ReadSupport支持读数据
* 3.继承WriteSupport支持读数据
*/
class CustomDataSourceV2 extends DataSourceV2
with ReadSupport
with WriteSupport {
/**
* 创建Reader
*
* @param options 用户定义的options
* @return 自定义的DataSourceReader
*/
override def createReader(options: DataSourceOptions): DataSourceReader = new CustomDataSourceV2Reader(options)
/**
* 创建Writer
*
* @param jobId jobId
* @param schema schema
* @param mode 保存模式
* @param options 用于定义的option
* @return Optional[自定义的DataSourceWriter]
*/
override def createWriter(jobId: String,
schema: StructType,
mode: SaveMode,
options: DataSourceOptions): Optional[DataSourceWriter] = Optional.of(new CustomDataSourceV2Writer)
}
/**
* 自定义的DataSourceReader
* 继承DataSourceReader
* 重写readSchema方法用来生成schema
* 重写createDataReaderFactories,用来根据条件,创建多个工厂实例
*
* @param options options
*/
class CustomDataSourceV2Reader(options: DataSourceOptions) extends DataSourceReader {
/**
* 生成schema
*
* @return schema
*/
override def readSchema(): StructType = ???
/**
* 创建DataReader工厂实例
*
* @return 多个工厂类实例
*/
override def createDataReaderFactories(): util.List[DataReaderFactory[Row]] = {
import collection.JavaConverters._
Seq(
new CustomDataSourceV2ReaderFactory().asInstanceOf[DataReaderFactory[Row]]
).asJava
}
}
/**
* 自定义DataReaderFactory类
*/
class CustomDataSourceV2ReaderFactory extends DataReaderFactory[Row] {
/**
* 重写createDataReader方法,用来实例化自定义的DataReader
*
* @return 自定义的DataReader
*/
override def createDataReader(): DataReader[Row] = new CustomDataReader
}
/**
* 自定义DataReader类
*/
class CustomDataReader extends DataReader[Row] {
/**
* 是否有下一条数据
*
* @return boolean
*/
override def next(): Boolean = ???
/**
* 获取数据
* 当next为true时会调用get方法获取数据
*
* @return Row
*/
override def get(): Row = ???
/**
* 关闭资源
*/
override def close(): Unit = ???
}
/**
* 自定义DataSourceWriter
* 继承DataSourceWriter
*/
class CustomDataSourceV2Writer extends DataSourceWriter {
/**
* 创建WriterFactory
*
* @return 自定义的DataWriterFactory
*/
override def createWriterFactory(): DataWriterFactory[Row] = ???
/**
* commit
*
* @param messages 所有分区提交的commit信息
* 触发一次
*/
override def commit(messages: Array[WriterCommitMessage]): Unit = ???
/** *
* abort
*
* @param messages 当write异常时调用
*/
override def abort(messages: Array[WriterCommitMessage]): Unit = ???
}
/**
* DataWriterFactory工厂类
*/
class CustomDataWriterFactory extends DataWriterFactory[Row] {
/**
* 创建DataWriter
*
* @param partitionId 分区ID
* @param attemptNumber 重试次数
* @return DataWriter
* 每个分区创建一个RestDataWriter实例
*/
override def createDataWriter(partitionId: Int, attemptNumber: Int): DataWriter[Row] = ???
}
/**
* DataWriter
*/
class CustomDataWriter extends DataWriter[Row] {
/**
* write
*
* @param record 单条记录
* 每条记录都会触发该方法
*/
override def write(record: Row): Unit = ???
/**
* commit
*
* @return commit message
* 每个分区触发一次
*/
override def commit(): WriterCommitMessage = ???
/**
* 回滚:当write发生异常时触发该方法
*/
override def abort(): Unit = ???
}
4.2 自定义RestDataSource代码
package com.hollysys.spark.sql.datasource.rest
import java.math.BigDecimal
import java.util
import java.util.Optional
import com.alibaba.fastjson.{JSONArray, JSONObject, JSONPath}
import org.apache.http.client.fluent.Request
import org.apache.http.entity.ContentType
import org.apache.spark.sql.{Row, SaveMode, SparkSession}
import org.apache.spark.sql.sources.v2.reader.{DataReader, DataReaderFactory, DataSourceReader, SupportsPushDownRequiredColumns}
import org.apache.spark.sql.sources.v2.writer.{DataSourceWriter, DataWriter, DataWriterFactory, WriterCommitMessage}
import org.apache.spark.sql.sources.v2.{DataSourceOptions, DataSourceV2, ReadSupport, WriteSupport}
import org.apache.spark.sql.types.StructType
/**
* @author : shirukai
* @date : 2019-01-09 16:53
* 基于Rest的Spark SQL DataSource
*/
class RestDataSource extends DataSourceV2 with ReadSupport with WriteSupport {
override def createReader(options: DataSourceOptions): DataSourceReader =
new RestDataSourceReader(
options.get("url").get(),
options.get("params").get(),
options.get("xPath").get(),
options.get("schema").get()
)
override def createWriter(jobId: String,
schema: StructType,
mode: SaveMode,
options: DataSourceOptions): Optional[DataSourceWriter] = Optional.of(new RestDataSourceWriter)
}
/**
* 创建RestDataSourceReader
*
* @param url REST服务的的api
* @param params 请求需要的参数
* @param xPath JSON数据的xPath
* @param schemaString 用户传入的schema字符串
*/
class RestDataSourceReader(url: String, params: String, xPath: String, schemaString: String)
extends DataSourceReader {
// 使用StructType.fromDDL方法将schema字符串转成StructType类型
var requiredSchema: StructType = StructType.fromDDL(schemaString)
/**
* 生成schema
*
* @return schema
*/
override def readSchema(): StructType = requiredSchema
/**
* 创建工厂类
*
* @return List[实例]
*/
override def createDataReaderFactories(): util.List[DataReaderFactory[Row]] = {
import collection.JavaConverters._
Seq(
new RestDataReaderFactory(url, params, xPath).asInstanceOf[DataReaderFactory[Row]]
).asJava
}
}
/**
* RestDataReaderFactory工厂类
*
* @param url REST服务的的api
* @param params 请求需要的参数
* @param xPath JSON数据的xPath
*/
class RestDataReaderFactory(url: String, params: String, xPath: String) extends DataReaderFactory[Row] {
override def createDataReader(): DataReader[Row] = new RestDataReader(url, params, xPath)
}
/**
* RestDataReader类
*
* @param url REST服务的的api
* @param params 请求需要的参数
* @param xPath JSON数据的xPath
*/
class RestDataReader(url: String, params: String, xPath: String) extends DataReader[Row] {
// 使用Iterator模拟数据
val data: Iterator[Seq[AnyRef]] = getIterator
override def next(): Boolean = {
data.hasNext
}
override def get(): Row = {
val seq = data.next().map {
// 浮点类型会自动转为BigDecimal,导致Spark无法转换
case decimal: BigDecimal =>
decimal.doubleValue()
case x => x
}
Row(seq: _*)
}
override def close(): Unit = {
println("close source")
}
def getIterator: Iterator[Seq[AnyRef]] = {
import scala.collection.JavaConverters._
val res: List[AnyRef] = RestDataSource.requestData(url, params, xPath)
res.map(r => {
r.asInstanceOf[JSONObject].asScala.values.toList
}).toIterator
}
}
/** *
* RestDataSourceWriter
*/
class RestDataSourceWriter extends DataSourceWriter {
/**
* 创建RestDataWriter工厂类
*
* @return RestDataWriterFactory
*/
override def createWriterFactory(): DataWriterFactory[Row] = new RestDataWriterFactory
/**
* commit
*
* @param messages 所有分区提交的commit信息
* 触发一次
*/
override def commit(messages: Array[WriterCommitMessage]): Unit = ???
/** *
* abort
*
* @param messages 当write异常时调用
*/
override def abort(messages: Array[WriterCommitMessage]): Unit = ???
}
/**
* DataWriterFactory工厂类
*/
class RestDataWriterFactory extends DataWriterFactory[Row] {
/**
* 创建DataWriter
*
* @param partitionId 分区ID
* @param attemptNumber 重试次数
* @return DataWriter
* 每个分区创建一个RestDataWriter实例
*/
override def createDataWriter(partitionId: Int, attemptNumber: Int): DataWriter[Row] = new RestDataWriter(partitionId, attemptNumber)
}
/**
* RestDataWriter
*
* @param partitionId 分区ID
* @param attemptNumber 重试次数
*/
class RestDataWriter(partitionId: Int, attemptNumber: Int) extends DataWriter[Row] {
/**
* write
*
* @param record 单条记录
* 每条记录都会触发该方法
*/
override def write(record: Row): Unit = {
println(record)
}
/**
* commit
*
* @return commit message
* 每个分区触发一次
*/
override def commit(): WriterCommitMessage = {
RestWriterCommitMessage(partitionId, attemptNumber)
}
/**
* 回滚:当write发生异常时触发该方法
*/
override def abort(): Unit = {
println("abort 方法被出发了")
}
}
case class RestWriterCommitMessage(partitionId: Int, attemptNumber: Int) extends WriterCommitMessage
object RestDataSource {
def requestData(url: String, params: String, xPath: String): List[AnyRef] = {
import scala.collection.JavaConverters._
val response = Request.Post(url).bodyString(params, ContentType.APPLICATION_JSON).execute()
JSONPath.read(response.returnContent().asString(), xPath)
.asInstanceOf[JSONArray].asScala.toList
}
}
object RestDataSourceTest {
def main(args: Array[String]): Unit = {
val spark = SparkSession
.builder()
.master("local[2]")
.appName(this.getClass.getSimpleName)
.getOrCreate()
val df = spark.read
.format("com.hollysys.spark.sql.datasource.rest.RestDataSource")
.option("url", "http://model-opcua-hollysysdigital-test.hiacloud.net.cn/aggquery/query/queryPointHistoryData")
.option("params", "{\n \"startTime\": \"1543887720000\",\n \"endTime\": \"1543891320000\",\n \"maxSizePerNode\": 1000,\n \"nodes\": [\n {\n \"uri\": \"/SymLink-10000012030100000-device/5c174da007a54e0001035ddd\"\n }\n ]\n}")
.option("xPath", "$.result.historyData")
//`response` ARRAY<STRUCT<`historyData`:ARRAY<STRUCT<`s`:INT,`t`:LONG,`v`:FLOAT>>>>
.option("schema", "`s` INT,`t` LONG,`v` DOUBLE")
.load()
df.printSchema()
df.show(false)
// df.repartition(5).write.format("com.hollysys.spark.sql.datasource.rest.RestDataSource")
// .save()
}
}