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://blog.csdn.net/zjerryj/article/details/84922369與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()
}
}