StructuredStreaming 內置數據源及實現自定義數據源
版本說明:
Spark:2.3/2.4
代碼倉庫:https://github.com/shirukai/spark-structured-datasource.git
1 Structured內置的輸入源 Source
官網文檔:http://spark.apache.org/docs/latest/structured-streaming-programming-guide.html#input-sources
Source | Options | Fault-tolerant | Notes |
---|---|---|---|
File Source | maxFilesPerTrigger:每個觸發器中要考慮的最大新文件數(默認值:無最大值)latestFirst:是否先處理最新的新文件,當存在大量積壓的文件時有用(默認值:false) fileNameOnly:是否基於以下方法檢查新文件只有文件名而不是完整路徑(默認值:false)。 |
支持容錯 | 支持glob路徑,但不支持以口號分割的多個路徑 |
Socket Source | host:要連接的主機,必須指定 port:要連接的端口,必須指定 |
不支持容錯 | |
Rate Source | rowsPerSecond(例如100,默認值:1):每秒應生成多少行。 rampUpTime(例如5s,默認值:0s):在生成速度變爲之前加速多長時間rowsPerSecond。使用比秒更精細的粒度將被截斷爲整數秒。numPartitions(例如10,默認值:Spark的默認並行性):生成的行的分區號 |
支持容錯 | |
Kafka Source | http://spark.apache.org/docs/latest/structured-streaming-kafka-integration.html | 支持容錯 |
1.1 File Source
將目錄中寫入的文件作爲數據流讀取。支持的文件格式爲:text、csv、json、orc、parquet
用例
代碼位置:org.apache.spark.sql.structured.datasource.example
val source = spark
.readStream
// Schema must be specified when creating a streaming source DataFrame.
.schema(schema)
// 每個trigger最大文件數量
.option("maxFilesPerTrigger",100)
// 是否首先計算最新的文件,默認爲false
.option("latestFirst",value = true)
// 是否值檢查名字,如果名字相同,則不視爲更新,默認爲false
.option("fileNameOnly",value = true)
.csv("*.csv")
1.2 Socket Source
從Socket中讀取UTF8文本數據。一般用於測試,使用nc -lc 端口號 向Socket監聽的端口發送數據。
用例
代碼位置:org.apache.spark.sql.structured.datasource.example
val lines = spark.readStream
.format("socket")
.option("host", "localhost")
.option("port", 9090)
.load()
1.3 Rate Source
以每秒指定的行數生成數據,每個輸出行包含一個timestamp
和value
。其中timestamp
是一個Timestamp
含有信息分配的時間類型,並且value
是Long
包含消息的計數從0開始作爲第一行類型。此源用於測試和基準測試。
用例
代碼位置:org.apache.spark.sql.structured.datasource.example
val rate = spark.readStream
.format("rate")
// 每秒生成的行數,默認值爲1
.option("rowsPerSecond", 10)
.option("numPartitions", 10)
.option("rampUpTime",0)
.option("rampUpTime",5)
.load()
1.4 Kafka Source
官網文檔:http://spark.apache.org/docs/latest/structured-streaming-kafka-integration.html
用例
代碼位置:org.apache.spark.sql.structured.datasource.example
val df = spark
.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "host1:port1,host2:port2")
.option("subscribePattern", "topic.*")
.load()
2 Structured 內置的輸出源 Sink
Sink | Supported Output Modes | Options | Fault-tolerant | Notes |
---|---|---|---|---|
File Sink | Append | path:輸出路徑(必須指定) | 支持容錯(exactly-once) | 支持分區寫入 |
Kafka Sink | Append,Update,Complete | See the Kafka Integration Guide | 支持容錯(at-least-once) | Kafka Integration Guide |
Foreach Sink | Append,Update,Complete | None | Foreach Guide | |
ForeachBatch Sink | Append,Update,Complete | None | Foreach Guide | |
Console Sink | Append,Update,Complete | numRows:每次觸發器打印的行數(默認值:20) truncate:是否過長時截斷輸出(默認值:true |
||
Memory Sink | Append,Complete | None | 表名是查詢的名字 |
2.1 File Sink
將結果輸出到文件,支持格式parquet、csv、orc、json等
用例
代碼位置:org.apache.spark.sql.structured.datasource.example
val fileSink = source.writeStream
.format("parquet")
//.format("csv")
//.format("orc")
// .format("json")
.option("path", "data/sink")
.option("checkpointLocation", "/tmp/temporary-" + UUID.randomUUID.toString)
.start()
2.2 Console Sink
將結果輸出到控制檯
用例
代碼位置:org.apache.spark.sql.structured.datasource.example
val consoleSink = source.writeStream
.format("console")
// 是否壓縮顯示
.option("truncate", value = false)
// 顯示條數
.option("numRows", 30)
.option("checkpointLocation", "/tmp/temporary-" + UUID.randomUUID.toString)
.start()
2.3 Memory Sink
將結果輸出到內存,需要指定內存中的表名。可以使用sql進行查詢
用例
代碼位置:org.apache.spark.sql.structured.datasource.example
val memorySink = source.writeStream
.format("memory")
.queryName("memorySinkTable")
.option("checkpointLocation", "/tmp/temporary-" + UUID.randomUUID.toString)
.start()
new Thread(new Runnable {
override def run(): Unit = {
while (true) {
spark.sql("select * from memorySinkTable").show(false)
Thread.sleep(1000)
}
}
}).start()
memorySink.awaitTermination()
2.4 Kafka Sink
將結果輸出到Kafka,需要將DataFrame轉成key,value兩列,或者topic、key、value三列
用例
代碼位置:org.apache.spark.sql.structured.datasource.example
import org.apache.spark.sql.functions._
import spark.implicits._
val kafkaSink = source.select(array(to_json(struct("*"))).as("value").cast(StringType),
$"timestamp".as("key").cast(StringType)).writeStream
.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092")
.option("checkpointLocation", "/tmp/temporary-" + UUID.randomUUID.toString)
.option("topic", "hiacloud-ts-dev")
.start()
2.5 ForeachBatch Sink(2.4)
適用於對於一個批次來說應用相同的寫入方式的場景。方法傳入這個batch的DataFrame以及batchId。這個方法在2.3之後的版本纔有而且僅支持微批模式。
用例
代碼位置:org.apache.spark.sql.structured.datasource.example
val foreachBatchSink = source.writeStream.foreachBatch((batchData: DataFrame, batchId) => {
batchData.show(false)
}).start()
2.6 Foreach Sink
Foreach 每一條記錄,通過繼承ForeachWriter[Row],實現open(),process(),close()方法。在open方法了我們可以獲取一個資源連接,如MySQL的連接。在process裏我們可以獲取一條記錄,並處理這條數據發送到剛纔獲取資源連接的MySQL中,在close裏我們可以關閉資源連接。注意,foreach是對Partition來說的,同一個分區只會調用一次open、close方法,但對於每條記錄來說,都會調用process方法。
用例
代碼位置:org.apache.spark.sql.structured.datasource.example
val foreachSink = source.writeStream
.foreach(new ForeachWriter[Row] {
override def open(partitionId: Long, version: Long): Boolean = {
println(s"partitionId=$partitionId,version=$version")
true
}
override def process(value: Row): Unit = {
println(value)
}
override def close(errorOrNull: Throwable): Unit = {
println("close")
}
})
.start()
3 自定義輸入源
某些應用場景下我們可能需要自定義數據源,如業務中,需要在獲取KafkaSource的同時,動態從緩存中或者http請求中加載業務數據,或者是其它的數據源等都可以參考規範自定義。自定義輸入源需要以下步驟:
第一步:繼承DataSourceRegister和StreamSourceProvider創建自定義Provider類
第二步:重寫DataSourceRegister類中的shotName和StreamSourceProvider中的createSource以及sourceSchema方法
第三步:繼承Source創建自定義Source類
第四步:重寫Source中的schema方法指定輸入源的schema
第五步:重寫Source中的getOffest方法監聽流數據
第六步:重寫Source中的getBatch方法獲取數據
第七步:重寫Source中的stop方法用來關閉資源
3.1 創建CustomDataSourceProvider類
3.1.1 繼承DataSourceRegister和StreamSourceProvider
要創建自定義的DataSourceProvider必須要繼承位於org.apache.spark.sql.sources包下的DataSourceRegister以及該包下的StreamSourceProvider。如下所示:
class CustomDataSourceProvider extends DataSourceRegister
with StreamSourceProvider
with Logging {
//Override some functions ……
}
3.1.2 重寫DataSourceRegister的shotName方法
該方法用來指定一個數據源的名字,用來想spark註冊該數據源。如Spark內置的數據源的shotName:kafka
、socket、rate等,該方法返回一個字符串,如下所示:
/**
* 數據源的描述名字,如:kafka、socket
*
* @return 字符串shotName
*/
override def shortName(): String = "custom"
3.1.3 重寫StreamSourceProvider中的sourceSchema方法
該方法是用來定義數據源的schema,可以使用用戶傳入的schema,也可以根據傳入的參數進行動態創建。返回值是個二元組(shotName,scheam),代碼如下所示:
/**
* 定義數據源的Schema
*
* @param sqlContext Spark SQL 上下文
* @param schema 通過.schema()方法傳入的schema
* @param providerName Provider的名稱,包名+類名
* @param parameters 通過.option()方法傳入的參數
* @return 元組,(shotName,schema)
*/
override def sourceSchema(sqlContext: SQLContext,
schema: Option[StructType],
providerName: String,
parameters: Map[String, String]): (String, StructType) = (shortName(),schema.get)
3.1.4 重寫StreamSourceProvider中的createSource方法
通過傳入的參數,來實例化我們自定義的DataSource,是我們自定義Source的重要入口的地方
/**
* 創建輸入源
*
* @param sqlContext Spark SQL 上下文
* @param metadataPath 元數據Path
* @param schema 通過.schema()方法傳入的schema
* @param providerName Provider的名稱,包名+類名
* @param parameters 通過.option()方法傳入的參數
* @return 自定義source,需要繼承Source接口實現
**/
override def createSource(sqlContext: SQLContext,
metadataPath: String,
schema: Option[StructType],
providerName: String,
parameters: Map[String, String]): Source = new CustomDataSource(sqlContext,parameters,schema)
3.1.5 CustomDataSourceProvider.scala完整代碼
package org.apache.spark.sql.structured.datasource.custom
import org.apache.spark.internal.Logging
import org.apache.spark.sql.SQLContext
import org.apache.spark.sql.execution.streaming.{Sink, Source}
import org.apache.spark.sql.sources.{DataSourceRegister, StreamSinkProvider, StreamSourceProvider}
import org.apache.spark.sql.streaming.OutputMode
import org.apache.spark.sql.types.StructType
/**
* @author : shirukai
* @date : 2019-01-25 17:49
* 自定義Structured Streaming數據源
*
* (1)繼承DataSourceRegister類
* 需要重寫shortName方法,用來向Spark註冊該組件
*
* (2)繼承StreamSourceProvider類
* 需要重寫createSource以及sourceSchema方法,用來創建數據輸入源
*
* (3)繼承StreamSinkProvider類
* 需要重寫createSink方法,用來創建數據輸出源
*
*
*/
class CustomDataSourceProvider extends DataSourceRegister
with StreamSourceProvider
with StreamSinkProvider
with Logging {
/**
* 數據源的描述名字,如:kafka、socket
*
* @return 字符串shotName
*/
override def shortName(): String = "custom"
/**
* 定義數據源的Schema
*
* @param sqlContext Spark SQL 上下文
* @param schema 通過.schema()方法傳入的schema
* @param providerName Provider的名稱,包名+類名
* @param parameters 通過.option()方法傳入的參數
* @return 元組,(shotName,schema)
*/
override def sourceSchema(sqlContext: SQLContext,
schema: Option[StructType],
providerName: String,
parameters: Map[String, String]): (String, StructType) = (shortName(),schema.get)
/**
* 創建輸入源
*
* @param sqlContext Spark SQL 上下文
* @param metadataPath 元數據Path
* @param schema 通過.schema()方法傳入的schema
* @param providerName Provider的名稱,包名+類名
* @param parameters 通過.option()方法傳入的參數
* @return 自定義source,需要繼承Source接口實現
**/
override def createSource(sqlContext: SQLContext,
metadataPath: String,
schema: Option[StructType],
providerName: String,
parameters: Map[String, String]): Source = new CustomDataSource(sqlContext,parameters,schema)
/**
* 創建輸出源
*
* @param sqlContext Spark SQL 上下文
* @param parameters 通過.option()方法傳入的參數
* @param partitionColumns 分區列名?
* @param outputMode 輸出模式
* @return
*/
override def createSink(sqlContext: SQLContext,
parameters: Map[String, String],
partitionColumns: Seq[String],
outputMode: OutputMode): Sink = new CustomDataSink(sqlContext,parameters,outputMode)
}
3.2 創建CustomDataSource類
3.2.1 繼承Source創建CustomDataSource類
要創建自定義的DataSource必須要繼承位於org.apache.spark.sql.sources包下的Source。如下所示:
class CustomDataSource(sqlContext: SQLContext,
parameters: Map[String, String],
schemaOption: Option[StructType]) extends Source
with Logging {
//Override some functions ……
}
3.2.2 重寫Source的schema方法
指定數據源的schema,需要與Provider中的sourceSchema指定的schema保持一致,否則會報異常
/**
* 指定數據源的schema,需要與Provider中sourceSchema中指定的schema保持一直,否則會報異常
* 觸發機制:當創建數據源的時候被觸發執行
*
* @return schema
*/
override def schema: StructType = schemaOption.get
3.2.3 重寫Source的getOffset方法
此方法是Spark不斷的輪詢執行的,目的是用來監控流數據的變化情況,一旦數據發生變化,就會觸發getBatch方法用來獲取數據。
/**
* 獲取offset,用來監控數據的變化情況
* 觸發機制:不斷輪詢調用
* 實現要點:
* (1)Offset的實現:
* 由函數返回值可以看出,我們需要提供一個標準的返回值Option[Offset]
* 我們可以通過繼承 org.apache.spark.sql.sources.v2.reader.streaming.Offset實現,這裏面其實就是保存了個json字符串
*
* (2) JSON轉化
* 因爲Offset裏實現的是一個json字符串,所以我們需要將我們存放offset的集合或者case class轉化重json字符串
* spark裏是通過org.json4s.jackson這個包來實現case class 集合類(Map、List、Seq、Set等)與json字符串的相互轉化
*
* @return Offset
*/
override def getOffset: Option[Offset] = ???
3.2.4 重寫Source的getBatch方法
此方法是Spark用來獲取數據的,getOffset方法檢測的數據發生變化的時候,會觸發該方法, 傳入上一次觸發時的end Offset作爲當前batch的start Offset,將新的offset作爲end Offset。
/**
* 獲取數據
*
* @param start 上一個批次的end offset
* @param end 通過getOffset獲取的新的offset
* 觸發機制:當不斷輪詢的getOffset方法,獲取的offset發生改變時,會觸發該方法
*
* 實現要點:
* (1)DataFrame的創建:
* 可以通過生成RDD,然後使用RDD創建DataFrame
* RDD創建:sqlContext.sparkContext.parallelize(rows.toSeq)
* DataFrame創建:sqlContext.internalCreateDataFrame(rdd, schema, isStreaming = true)
* @return DataFrame
*/
override def getBatch(start: Option[Offset], end: Offset): DataFrame = ???
3.2.5 重寫Source的stop方法
用來關閉一些需要關閉或停止的資源及進程
/**
* 關閉資源
* 將一些需要關閉的資源放到這裏來關閉,如MySQL的數據庫連接等
*/
override def stop(): Unit = ???
3.2.6 CustomDataSource.scala完整代碼
package org.apache.spark.sql.structured.datasource.custom
import org.apache.spark.internal.Logging
import org.apache.spark.sql.execution.streaming.{Offset, Source}
import org.apache.spark.sql.types.StructType
import org.apache.spark.sql.{DataFrame, SQLContext}
/**
* @author : shirukai
* @date : 2019-01-25 18:03
* 自定義數據輸入源:需要繼承Source接口
* 實現思路:
* (1)通過重寫schema方法來指定數據輸入源的schema,這個schema需要與Provider中指定的schema保持一致
* (2)通過重寫getOffset方法來獲取數據的偏移量,這個方法會一直被輪詢調用,不斷的獲取偏移量
* (3) 通過重寫getBatch方法,來獲取數據,這個方法是在偏移量發生改變後被觸發
* (4)通過stop方法,來進行一下關閉資源的操作
*
*/
class CustomDataSource(sqlContext: SQLContext,
parameters: Map[String, String],
schemaOption: Option[StructType]) extends Source
with Logging {
/**
* 指定數據源的schema,需要與Provider中sourceSchema中指定的schema保持一直,否則會報異常
* 觸發機制:當創建數據源的時候被觸發執行
*
* @return schema
*/
override def schema: StructType = schemaOption.get
/**
* 獲取offset,用來監控數據的變化情況
* 觸發機制:不斷輪詢調用
* 實現要點:
* (1)Offset的實現:
* 由函數返回值可以看出,我們需要提供一個標準的返回值Option[Offset]
* 我們可以通過繼承 org.apache.spark.sql.sources.v2.reader.streaming.Offset實現,這裏面其實就是保存了個json字符串
*
* (2) JSON轉化
* 因爲Offset裏實現的是一個json字符串,所以我們需要將我們存放offset的集合或者case class轉化重json字符串
* spark裏是通過org.json4s.jackson這個包來實現case class 集合類(Map、List、Seq、Set等)與json字符串的相互轉化
*
* @return Offset
*/
override def getOffset: Option[Offset] = ???
/**
* 獲取數據
*
* @param start 上一個批次的end offset
* @param end 通過getOffset獲取的新的offset
* 觸發機制:當不斷輪詢的getOffset方法,獲取的offset發生改變時,會觸發該方法
*
* 實現要點:
* (1)DataFrame的創建:
* 可以通過生成RDD,然後使用RDD創建DataFrame
* RDD創建:sqlContext.sparkContext.parallelize(rows.toSeq)
* DataFrame創建:sqlContext.internalCreateDataFrame(rdd, schema, isStreaming = true)
* @return DataFrame
*/
override def getBatch(start: Option[Offset], end: Offset): DataFrame = ???
/**
* 關閉資源
* 將一些需要關閉的資源放到這裏來關閉,如MySQL的數據庫連接等
*/
override def stop(): Unit = ???
}
3.3 自定義DataSource的使用
自定義DataSource的使用與內置DataSource一樣,只需要在format裏指定一下我們的Provider類路徑即可。如
val source = spark
.readStream
.format("org.apache.spark.sql.kafka010.CustomSourceProvider")
.options(options)
.schema(schema)
.load()
3.4 實現MySQL自定義數據源
此例子僅僅是爲了演示如何自定義數據源,與實際業務場景無關。
3.4.1 創建MySQLSourceProvider.scala
package org.apache.spark.sql.structured.datasource
import org.apache.spark.internal.Logging
import org.apache.spark.sql.SQLContext
import org.apache.spark.sql.execution.streaming.{Sink, Source}
import org.apache.spark.sql.sources.{DataSourceRegister, StreamSinkProvider, StreamSourceProvider}
import org.apache.spark.sql.streaming.OutputMode
import org.apache.spark.sql.types.StructType
/**
* @author : shirukai
* @date : 2019-01-25 09:10
* 自定義MySQL數據源
*/
class MySQLSourceProvider extends DataSourceRegister
with StreamSourceProvider
with StreamSinkProvider
with Logging {
/**
* 數據源的描述名字,如:kafka、socket
*
* @return 字符串shotName
*/
override def shortName(): String = "mysql"
/**
* 定義數據源的Schema
*
* @param sqlContext Spark SQL 上下文
* @param schema 通過.schema()方法傳入的schema
* @param providerName Provider的名稱,包名+類名
* @param parameters 通過.option()方法傳入的參數
* @return 元組,(shotName,schema)
*/
override def sourceSchema(
sqlContext: SQLContext,
schema: Option[StructType],
providerName: String,
parameters: Map[String, String]): (String, StructType) = {
(providerName, schema.get)
}
/**
* 創建輸入源
*
* @param sqlContext Spark SQL 上下文
* @param metadataPath 元數據Path
* @param schema 通過.schema()方法傳入的schema
* @param providerName Provider的名稱,包名+類名
* @param parameters 通過.option()方法傳入的參數
* @return 自定義source,需要繼承Source接口實現
*/
override def createSource(
sqlContext: SQLContext,
metadataPath: String, schema: Option[StructType],
providerName: String, parameters: Map[String, String]): Source = new MySQLSource(sqlContext, parameters, schema)
/**
* 創建輸出源
*
* @param sqlContext Spark SQL 上下文
* @param parameters 通過.option()方法傳入的參數
* @param partitionColumns 分區列名?
* @param outputMode 輸出模式
* @return
*/
override def createSink(
sqlContext: SQLContext,
parameters: Map[String, String],
partitionColumns: Seq[String], outputMode: OutputMode): Sink = new MySQLSink(sqlContext: SQLContext,parameters, outputMode)
}
3.4.2 創建MySQLSource.scala
package org.apache.spark.sql.structured.datasource
import java.sql.Connection
import org.apache.spark.executor.InputMetrics
import org.apache.spark.internal.Logging
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.execution.datasources.jdbc.JdbcUtils
import org.apache.spark.sql.execution.streaming.{Offset, Source}
import org.apache.spark.sql.types.StructType
import org.apache.spark.sql.{DataFrame, SQLContext}
import org.json4s.jackson.Serialization
import org.json4s.{Formats, NoTypeHints}
/**
* @author : shirukai
* @date : 2019-01-25 09:41
*/
class MySQLSource(sqlContext: SQLContext,
options: Map[String, String],
schemaOption: Option[StructType]) extends Source with Logging {
lazy val conn: Connection = C3p0Utils.getDataSource(options).getConnection
val tableName: String = options("tableName")
var currentOffset: Map[String, Long] = Map[String, Long](tableName -> 0)
val maxOffsetPerBatch: Option[Long] = Option(100)
val inputMetrics = new InputMetrics()
override def schema: StructType = schemaOption.get
/**
* 獲取Offset
* 這裏監控MySQL數據庫表中條數變化情況
* @return Option[Offset]
*/
override def getOffset: Option[Offset] = {
val latest = getLatestOffset
val offsets = maxOffsetPerBatch match {
case None => MySQLSourceOffset(latest)
case Some(limit) =>
MySQLSourceOffset(rateLimit(limit, currentOffset, latest))
}
Option(offsets)
}
/**
* 獲取數據
* @param start 上一次的offset
* @param end 最新的offset
* @return df
*/
override def getBatch(start: Option[Offset], end: Offset): DataFrame = {
var offset: Long = 0
if (start.isDefined) {
offset = offset2Map(start.get)(tableName)
}
val limit = offset2Map(end)(tableName) - offset
val sql = s"SELECT * FROM $tableName limit $limit offset $offset"
val st = conn.prepareStatement(sql)
val rs = st.executeQuery()
val rows: Iterator[InternalRow] = JdbcUtils.resultSetToSparkInternalRows(rs, schemaOption.get, inputMetrics) //todo 好用
val rdd = sqlContext.sparkContext.parallelize(rows.toSeq)
currentOffset = offset2Map(end)
sqlContext.internalCreateDataFrame(rdd, schema, isStreaming = true)
}
override def stop(): Unit = {
conn.close()
}
def rateLimit(limit: Long, currentOffset: Map[String, Long], latestOffset: Map[String, Long]): Map[String, Long] = {
val co = currentOffset(tableName)
val lo = latestOffset(tableName)
if (co + limit > lo) {
Map[String, Long](tableName -> lo)
} else {
Map[String, Long](tableName -> (co + limit))
}
}
// 獲取最新條數
def getLatestOffset: Map[String, Long] = {
var offset: Long = 0
val sql = s"SELECT COUNT(1) FROM $tableName"
val st = conn.prepareStatement(sql)
val rs = st.executeQuery()
while (rs.next()) {
offset = rs.getLong(1)
}
Map[String, Long](tableName -> offset)
}
def offset2Map(offset: Offset): Map[String, Long] = {
implicit val formats: AnyRef with Formats = Serialization.formats(NoTypeHints)
Serialization.read[Map[String, Long]](offset.json())
}
}
case class MySQLSourceOffset(offset: Map[String, Long]) extends Offset {
implicit val formats: AnyRef with Formats = Serialization.formats(NoTypeHints)
override def json(): String = Serialization.write(offset)
}
3.4.3 測試MySQLSource
package org.apache.spark.sql.structured.datasource
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types.{StringType, StructField, StructType, TimestampType}
/**
* @author : shirukai
* @date : 2019-01-25 15:12
*/
object MySQLSourceTest {
def main(args: Array[String]): Unit = {
val spark = SparkSession
.builder()
.appName(this.getClass.getSimpleName)
.master("local[2]")
.getOrCreate()
val schema = StructType(List(
StructField("name", StringType),
StructField("creatTime", TimestampType),
StructField("modifyTime", TimestampType)
)
)
val options = Map[String, String](
"driverClass" -> "com.mysql.cj.jdbc.Driver",
"jdbcUrl" -> "jdbc:mysql://localhost:3306/spark-source?useSSL=false&characterEncoding=utf-8",
"user" -> "root",
"password" -> "hollysys",
"tableName" -> "model")
val source = spark
.readStream
.format("org.apache.spark.sql.structured.datasource.MySQLSourceProvider")
.options(options)
.schema(schema)
.load()
import org.apache.spark.sql.functions._
val query = source.writeStream.format("console")
// 是否壓縮顯示
.option("truncate", value = false)
// 顯示條數
.option("numRows", 30)
.option("checkpointLocation", "/tmp/temporary-" + UUID.randomUUID.toString)
.start()
query.awaitTermination()
}
}
4 自定義輸出源
相比較輸入源的自定義性,輸出源自定義的應用場景貌似更爲常用。比如:數據寫入關係型數據庫、數據寫入HBase、數據寫入Redis等等。其實Structured提供的foreach以及2.4版本的foreachBatch方法已經可以實現絕大數的應用場景的,幾乎是數據想寫到什麼地方都能實現。但是想要更優雅的實現,我們可以參考Spark SQL Sink規範,通過自定義的Sink的方式來實現。實現自定義Sink需要以下四個個步驟:
第一步:繼承DataSourceRegister和StreamSinkProvider創建自定義SinkProvider類
第二步:重寫DataSourceRegister類中的shotName和StreamSinkProvider中的createSink方法
第三步:繼承Sink創建自定義Sink類
第四步:重寫Sink中的addBatch方法
4.1 改寫CustomDataSourceProvider類
4.1.1 新增繼承StreamSinkProvider
在上面創建自定義輸入源的基礎上,新增繼承StreamSourceProvider。如下所示:
class CustomDataSourceProvider extends DataSourceRegister
with StreamSourceProvider
with StreamSinkProvider
with Logging {
//Override some functions ……
}
4.1.2 重寫StreamSinkProvider中的createSink方法
通過傳入的參數,來實例化我們自定義的DataSink,是我們自定義Sink的重要入口的地方
/**
* 創建輸出源
*
* @param sqlContext Spark SQL 上下文
* @param parameters 通過.option()方法傳入的參數
* @param partitionColumns 分區列名?
* @param outputMode 輸出模式
* @return
*/
override def createSink(sqlContext: SQLContext,
parameters: Map[String, String],
partitionColumns: Seq[String],
outputMode: OutputMode): Sink = new CustomDataSink(sqlContext,parameters,outputMode)
4.2 創建CustomDataSink類
4.2.1 繼承Sink創建CustomDataSink類
要創建自定義的DataSink必須要繼承位於org.apache.spark.sql.sources包下的Sink。如下所示:
class CustomDataSink(sqlContext: SQLContext,
parameters: Map[String, String],
outputMode: OutputMode) extends Sink with Logging {
// Override some functions
}
4.2.2 重寫Sink中的addBatch方法
該方法是當發生計算時會被觸發,傳入的是一個batchId和dataFrame,拿到DataFrame之後,我們有三種寫出方式,第一種是使用Spark SQL內置的Sink寫出,如 JSON數據源、CSV數據源、Text數據源、Parquet數據源、JDBC數據源等。第二種是通過DataFrame的foreachPartition寫出。第三種就是自定義SparkSQL的輸出源然後寫出。
/**
* 添加Batch,即數據寫出
*
* @param batchId batchId
* @param data DataFrame
* 觸發機制:當發生計算時,會觸發該方法,並且得到要輸出的DataFrame
* 實現摘要:
* 1. 數據寫入方式:
* (1)通過SparkSQL內置的數據源寫出
* 我們拿到DataFrame之後可以通過SparkSQL內置的數據源將數據寫出,如:
* JSON數據源、CSV數據源、Text數據源、Parquet數據源、JDBC數據源等。
* (2)通過自定義SparkSQL的數據源進行寫出
* (3)通過foreachPartition 將數據寫出
*/
override def addBatch(batchId: Long, data: DataFrame): Unit = ???
注意:
當我們使用第一種方式的時候要注意,此時拿到的DataFrame是一個流式的DataFrame,即isStreaming=ture,通過查看KafkaSink,如下代碼所示,先是通過DataFrame.queryExecution執行查詢,然後在wite裏轉成rdd,通過rdd的foreachPartition實現。同樣的思路,我們可以利用這個rdd和schema,利用sqlContext.internalCreateDataFrame(rdd, data.schema)重新生成DataFrame,這個在MySQLSink中使用過。
override def addBatch(batchId: Long, data: DataFrame): Unit = {
if (batchId <= latestBatchId) {
logInfo(s"Skipping already committed batch $batchId")
} else {
KafkaWriter.write(sqlContext.sparkSession,
data.queryExecution, executorKafkaParams, topic)
latestBatchId = batchId
}
}
def write(
sparkSession: SparkSession,
queryExecution: QueryExecution,
kafkaParameters: ju.Map[String, Object],
topic: Option[String] = None): Unit = {
val schema = queryExecution.analyzed.output
validateQuery(schema, kafkaParameters, topic)
queryExecution.toRdd.foreachPartition { iter =>
val writeTask = new KafkaWriteTask(kafkaParameters, schema, topic)
Utils.tryWithSafeFinally(block = writeTask.execute(iter))(
finallyBlock = writeTask.close())
}
}
4.3 自定義DataSink的使用
自定義DataSink的使用與自定義DataSource的使用相同,在format裏指定一些類Provider的類路徑即可。
val query = source.groupBy("creatTime").agg(collect_list("name")).writeStream
.outputMode("update")
.format("org.apache.spark.sql.kafka010.CustomDataSourceProvider")
.option(options)
.start()
query.awaitTermination()
4.4 實現MySQL自定義輸出源
4.4.1 修改MySQLSourceProvider.scala
上面我們實現MySQL自定義輸入源的時候,已經創建了MySQLSourceProvider類,我們需要在這個基礎上新增繼承StreamSinkProvider,並重寫createSink方法,如下所示:
package org.apache.spark.sql.structured.datasource
import org.apache.spark.internal.Logging
import org.apache.spark.sql.SQLContext
import org.apache.spark.sql.execution.streaming.{Sink, Source}
import org.apache.spark.sql.kafka010.{MySQLSink, MySQLSource}
import org.apache.spark.sql.sources.{DataSourceRegister, StreamSinkProvider, StreamSourceProvider}
import org.apache.spark.sql.streaming.OutputMode
import org.apache.spark.sql.types.StructType
/**
* @author : shirukai
* @date : 2019-01-25 09:10
* 自定義MySQL數據源
*/
class MySQLSourceProvider extends DataSourceRegister
with StreamSourceProvider
with StreamSinkProvider
with Logging {
//……省略自定義輸入源的方法
/**
* 創建輸出源
*
* @param sqlContext Spark SQL 上下文
* @param parameters 通過.option()方法傳入的參數
* @param partitionColumns 分區列名?
* @param outputMode 輸出模式
* @return
*/
override def createSink(
sqlContext: SQLContext,
parameters: Map[String, String],
partitionColumns: Seq[String], outputMode: OutputMode): Sink = new MySQLSink(sqlContext: SQLContext,parameters, outputMode)
}
4.4.1 創建MySQLSink.scala
package org.apache.spark.sql.structured.datasource
import org.apache.spark.internal.Logging
import org.apache.spark.sql.{DataFrame, SQLContext, SaveMode}
import org.apache.spark.sql.execution.streaming.Sink
import org.apache.spark.sql.streaming.OutputMode
/**
* @author : shirukai
* @date : 2019-01-25 17:35
*/
class MySQLSink(sqlContext: SQLContext,parameters: Map[String, String], outputMode: OutputMode) extends Sink with Logging {
override def addBatch(batchId: Long, data: DataFrame): Unit = {
val query = data.queryExecution
val rdd = query.toRdd
val df = sqlContext.internalCreateDataFrame(rdd, data.schema)
df.show(false)
df.write.format("jdbc").options(parameters).mode(SaveMode.Append).save()
}
}
4.2.3 測試MySQLSink
package org.apache.spark.sql.structured.datasource
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types.{StringType, StructField, StructType, TimestampType}
/**
* @author : shirukai
* @date : 2019-01-29 09:57
* 測試自定義MySQLSource
*/
object MySQLSourceTest {
def main(args: Array[String]): Unit = {
val spark = SparkSession
.builder()
.appName(this.getClass.getSimpleName)
.master("local[2]")
.getOrCreate()
val schema = StructType(List(
StructField("name", StringType),
StructField("creatTime", TimestampType),
StructField("modifyTime", TimestampType)
)
)
val options = Map[String, String](
"driverClass" -> "com.mysql.cj.jdbc.Driver",
"jdbcUrl" -> "jdbc:mysql://localhost:3306/spark-source?useSSL=false&characterEncoding=utf-8",
"user" -> "root",
"password" -> "hollysys",
"tableName" -> "model")
val source = spark
.readStream
.format("org.apache.spark.sql.structured.datasource.MySQLSourceProvider")
.options(options)
.schema(schema)
.load()
import org.apache.spark.sql.functions._
val query = source.groupBy("creatTime").agg(collect_list("name").cast(StringType).as("names")).writeStream
.outputMode("update")
.format("org.apache.spark.sql.structured.datasource.MySQLSourceProvider")
.option("checkpointLocation", "/tmp/MySQLSourceProvider11")
.option("user","root")
.option("password","hollysys")
.option("dbtable","test")
.option("url","jdbc:mysql://localhost:3306/spark-source?useSSL=false&characterEncoding=utf-8")
.start()
query.awaitTermination()
}
}
3 總結
通過上面的筆記,參看官網文檔,可以學習到Structured支持的幾種輸入源:File Source、Socket Source、Rate Source、Kafka Source,平時我們會用到KafkaSource以及FileSource,SocketSource、RateSource多用於測試場景。關於輸入源沒有什麼優雅的操作,只能通過重寫Source來實現。對於輸出源來說,Spark Structured提供的foreach以及foreachBatch已經能適用於大多數場景,沒有重寫Sink的必要。關於Spark SQL 自定義輸入源、Streaming自定義數據源後期會慢慢整理出來。