Structured Streaming
簡介
Structured Streaming
構建在SparkSQL
之上的流處理引擎。可以使用戶繼續使用DataSet/dataFrame
操
作流數據。並且提供了多種計算模型可供選擇,默認情況下,使用的依然是Spark的marco batch這種計
算模型能夠到100ms左右的end-to-end的精準一次的容錯計算。除此之外也提供了基於EventTime
語義
的窗口計算(DStream 基於Processor Time不同)。同時在spark-2.3版本又提出新的計算模型
Continuous Processing
可以達到1ms左右的精準一次的容錯計算。
快速入門案例
- pom
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>2.4.3</version>
</dependency>
- WordCount
def main(args: Array[String]): Unit = {
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.apache.jetty.server").setLevel(Level.OFF)
//1.構建SparkSession
val spark = SparkSession.builder()
.appName("wordcount")
.master("local[*]")
.getOrCreate()
import spark.implicits._
//2.創建輸入流-readStream
var lines = spark.readStream
.format("socket")
.option("host", "localhost")
.option("port", 9999)
.load()
//3.對dataframe實現轉換
var wordCounts = lines.as[String]
.flatMap(_.split("\\s+"))
.groupBy("value")
.count()
//4.構建query 輸出
val query = wordCounts.writeStream
.format("console")
.outputMode(OutputMode.Update()) //有狀態持續計算 Complete| Update| Append
.start()
//5.等待流結束
query.awaitTermination()
}
- 有狀態持續計算 Complete| Update| Append 之間的區別
- Complete: 每一個trigger到來時,就輸出整個完整的dataframe
- Update: 只輸出那些被修改的Row。
每一次window sliding,就去跟原來的結果比較,有變化就輸出- Append: 只輸出新添加的(原來沒有的)Row()(如果是groupby,要有watermark纔可以)
每當一個watermark時間結束了,這個臨時的結果再回轉換成正式的結果並導出。
- nc -l 999 輸入
aa bb cc aa
cc aa aa aa
- 輸出結果(由於使用了Update 第二次輸入沒有
bb
,所有Batch: 2沒有bb輸出)
-------------------------------------------
Batch: 1
-------------------------------------------
+-----+-----+
|value|count|
+-----+-----+
| cc| 1|
| bb| 1|
| aa| 2|
+-----+-----+
-------------------------------------------
Batch: 2
-------------------------------------------
+-----+-----+
|value|count|
+-----+-----+
| cc| 2|
| aa| 5|
+-----+-----+
程序流程結構
1.構建
SparkSession
對象
2.藉助於SparkSession#readStream
加載動態的Dataframe
3.使用DataFrame API
或者是SQL語句 實現對動態數據計算
4.通過DataFrame#writeStream
方法構建StreamQuery
對象
5.調用StreamQuery#awaitTermination
等待關閉指令
基本概念
Structure Stream的核心思想是通過將實時數據流看成是一個持續插入table.因此用戶就可以使用SQL查 詢DynamicTable|UnboundedTable。底層Spark通過StreamQuery實現對數據持續計算。
當對Input執行轉換的時候系統產生一張結果表 ResultTable
,當有新的數據產生的時候,系統會往
Input Table
插入一行數據,這會最終導致系統更新 ResultTable
,每一次的更新系統將更新的數
據寫到外圍系統-Sink.
Output
定義如何將Result寫出到外圍系統,目前Spark支持三種輸出模式:(上面已經簡單介紹過了)
Complete Mode
- 整個ResultTable的數據會被寫到外圍系統。Update Mode
- 只會將ResultTable中被更新的行,寫到外圍系統( spark-2.1.1 +支持)Append Mode
- 只有新數據插入ResultTable的時候,纔會將結果輸出。注意:這種模式只適用
於被插入結果表的數據都是隻讀的情況下,纔可以將輸出模式定義爲Append(查詢當中不應該出
現聚合算子,當然也有特例,例如流中聲明watermarker)
由於Structure Streaming計算的特點,Spark會在內存當中存儲程序計算中間狀態用於生產結果表的數
據,Spark並不會存儲 Input Table 的數據,一旦處理完成之後,讀取的數據會被丟棄。整個聚合的
過程無需用戶干預(對比Storm,Storm狀態管理需要將數據寫到外圍系統)。
故障容錯
Structured Streaming通過checkpoint
和write ahead log
去記錄每一次批處理的數據源的偏移量(區
間),可以保證在失敗的時候可以重複的讀取數據源。其次Structure Streaming也提供了Sink的冪等寫
的特性(在編程中一個冪等 操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同),
因此Structure Streaming實現end-to-end
exactly-once
語義的故障恢復。
Structured Streaming API
自Spark-2.0版本以後Dataframe/Dataset
纔可以處理有界數據和無界數據。Structured Streaming
也是用
SparkSession
方式去創建Dataset/DataFrame
,同時所有Dataset/DataFrame
的操作保持和Spark SQL
中Dataset/DataFrame
一致。
Input Sources
File Source
目前支持支持text
, csv
, json
, orc
, parquet
等格式的文件,當這些數據被放入到採樣目錄,系統會以流的
形式讀取採樣目錄下的文件.
//1.創建SparkSession
val spark = SparkSession
.builder()
.master("local[*]")
.appName("printline")
.getOrCreate()
import spark.implicits._
var df = spark.readStream
.format("text")
//json/csv/parquet/orc 等
.load("file:///Users/mashikang/IdeaProjects/structured_stream/src/main/resources")
var userDF = df.as[String]
.map(line => line.split("\\s+"))
.map(tokens => (tokens(0).toInt, tokens(1), tokens(2).toBoolean, tokens(3).toInt))
.toDF("id", "name", "sex", "age")
val query = userDF.writeStream.format("console")
.outputMode(OutputMode.Append())
.start()
query.awaitTermination()
- 文件
1 zhangsan true 20
2 lisi true 28
3 wangwu false 24
4 zhaoliu true 28
Socket source(debug)
//1.構建SparkSession
val spark = SparkSession.builder()
.appName("wordcount")
.master("local[*]")
.getOrCreate()
import spark.implicits._
//2.創建輸入流-readStream
var lines = spark.readStream
.format("socket")
.option("host", "localhost")
.option("port", 9999)
.load()
//3.對dataframe實現轉換
var wordCounts = lines.as[String]
.flatMap(_.split("\\s+"))
.groupBy("value")
.count()
//4.構建query 輸出
val query = wordCounts.writeStream
.format("console")
.outputMode(OutputMode.Update()) //有狀態持續計算 Complete| Update
.start()
//5.等待流結束
query.awaitTermination()
Kafka source
- pom.xml
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql-kafka-0-10_2.11</artifactId>
<version>2.4.3</version>
</dependency>
//1.創建SparkSession
val spark=SparkSession .builder()
.master("local[*]")
.appName("printline")
.getOrCreate()
import spark.implicits._
var df=spark.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092")
.option("subscribe", "topic01")
.load()
.selectExpr("CAST(key AS STRING)","CAST(value AS STRING)")
val wordCounts=df.select("value").as[String]
.flatMap(_.split("\\s+"))
.coalesce(1)
.map((_,1))
.toDF("word","count")
.groupBy("word")
.sum("count")
val query = wordCounts.writeStream.
format("console")
.outputMode(OutputMode.Update())
.start()
query.awaitTermination()
Output Sink
File sink(Append Mode Only)
//1.創建SparkSession
val spark = SparkSession.builder()
.master("local[*]")
.appName("printline")
.getOrCreate()
import spark.implicits._
var df = spark.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092")
.option("subscribe", "topic01")
.load()
.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
val wordCounts = df.select("value").as[String]
.flatMap(_.split("\\s+"))
.coalesce(1)
.map((_, 1))
.toDF("word", "count")
val query = wordCounts.writeStream
.format("json")
.option("checkpointLocation", "file:///Users/mashikang/IdeaProjects/structured_stream/src/main/resources/checkpoints")
.outputMode(OutputMode.Append())
.start("file:////Users/mashikang/IdeaProjects/structured_stream/src/main/resource/json")
query.awaitTermination()
KafkaSink((Append|Update|Complete))
//1.創建SparkSession
val spark = SparkSession.builder()
.master("local[*]")
.appName("printline")
.getOrCreate()
import spark.implicits._
import org.apache.spark.sql.functions._
var df = spark.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092")
.option("subscribe", "topic01")
.load()
.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
val wordCounts = df.select("value").as[String]
.flatMap(_.split("\\s+"))
.coalesce(1)
.map((_, 1))
.toDF("word", "count")
.groupBy("word")
.agg(sum("count") as "count")
.selectExpr("word", "CAST(count AS STRING)")
.withColumnRenamed("word", "key")
.withColumnRenamed("count", "value")
val query = wordCounts.writeStream
.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092")
.option("topic", "topic02")
.option("checkpointLocation", "file:///Users/mashikang/IdeaProjects/structured_stream/src/main/resources/checkpoints")
.outputMode(OutputMode.Update())
.start()
query.awaitTermination()
Foreach sink(Append|Update|Complate)
UserRowWriter
這裏的 open方法在,每一次微批的時候觸發,其中 epochId
表示計算的批次。一般如果要保證
exactly-once
語義的處理時候,需要在外圍系統存儲 epochId
,如果存在重複計算 epochId
不
變。
class UserRowWriter extends ForeachWriter[Row] {
// 存儲 上一次epochid
var lastEpochId: Long = -1L
/**
* 計算 當前是否處理當前批次,如果epochId=lastEpochId說明是重複記錄,丟棄更新 false
* epochId!=lastEpochId 返回true 調用 open
*
* @param partitionId
* @param epochId
* @return
*/
override def open(partitionId: Long, epochId: Long): Boolean = {
var flag: Boolean = false
if (epochId != -1L) {
if (lastEpochId == epochId) {
// 是重複記錄
flag = false
} else {
flag = true
lastEpochId = epochId
}
} else {
// 第一次進來
lastEpochId = epochId
flag = true
}
flag
}
override def process(value: Row): Unit = {
println(" ,epochId:" + lastEpochId)
}
override def close(errorOrNull: Throwable): Unit = {
if (errorOrNull != null)
errorOrNull.printStackTrace()
}
}
//1.創建SparkSession
val spark = SparkSession.builder()
.master("local[*]")
.appName("printline")
.getOrCreate()
import spark.implicits._
import org.apache.spark.sql.functions._
var df = spark.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092")
.option("subscribe", "topic01")
.load()
.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
val wordCounts = df.select("value").as[String]
.flatMap(_.split("\\s+"))
.coalesce(1)
.map((_, 1))
.toDF("word", "count")
.groupBy("word")
.agg(sum("count") as "count")
.selectExpr("word", "CAST(count AS STRING)")
.withColumnRenamed("word", "key")
.withColumnRenamed("count", "value")
val query = wordCounts.writeStream
.outputMode(OutputMode.Update())
.foreach(new UserRowWriter)
.start()
query.awaitTermination()
Window on Event Time
Structured Streaming使用聚合函數基於EventTime計算window是非常簡單的類似於分組聚合。分組聚
合是按照指定的column字段對錶中的數據進行分組,然後使用聚合函數對用戶指定的column字段進行
聚合。
下面一張圖描繪的是計算10分鐘內的單詞統計,每間隔5分鐘滑動一個時間窗口。
按照窗口原始含義是將落入到同一個窗口的數據進行分組,因此在Structured Streaming可以使用
groupby和window表達窗口計算
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.apache.jetty.server").setLevel(Level.OFF)
//1.創建SparkSession
val spark=SparkSession
.builder()
.master("local[*]")
.appName("printline")
.getOrCreate()
import spark.implicits._
//字符,時間戳
var df=spark.readStream .format("socket")
.option("host", "localhost")
.option("port", "9999")
.load()
import org.apache.spark.sql.functions._
var sdf=new SimpleDateFormat("mm:ss")
val wordCounts=df.select("value")
.as[String]
.map(_.split(","))
// 這裏的Timestamp導如java.sql的依賴
.map(tokens=>(tokens(0),new Timestamp(tokens(1).toLong)))
.toDF("word","timestamp")
.groupBy(
window($"timestamp","10 seconds","5 seconds"),
$"word"
)
.count()
.map(r=>
(sdf.format(r.getStruct(0).getTimestamp(0)),
sdf.format(r.getStruct(0).getTimestamp(1)),
r.getString(1),r.getLong(2)))
.toDF("start","end","word","count")
val query = wordCounts.writeStream
.outputMode(OutputMode.Update())
.format("console")
.start()
query.awaitTermination()
處理延遲 Data 和 Watermarking
默認情況下,Spark會把落入到時間窗口的數據進行聚合操作。但是需要思考的是Event-Time是基於事
件的時間戳進行窗口聚合的。那就有可能事件窗口已經觸發很久了,但是有一些元素因爲某種原因,導
致遲到了,這個時候Spark需要將遲到的的數據加入到已經觸發的窗口進行重複計算。但是需要注意如
果在長時間的流計算過程中,如果不去限定窗口計算的時間,那麼意味着Spark要在內存中一直存儲窗
口的狀態,這樣是不切實際的,因此Spark提供一種稱爲watermarker
的機制用於限定存儲在Spark內存
中中間結果存儲的時間,這樣系統就可以將已經確定觸發過的窗口的中間結果給刪除。如果後續還有數
據在窗口endtime以後抵達該窗口,Spark把這種數據定義爲late數據
。其中watermarker
計算方式 max event time seen by engine - late threshold
如果watermarker
的取值大於了時間窗口的
endtime即可認定該窗口的計算結果就可以被丟棄了。如果此時再有數據落入到已經被丟棄的時間窗
口,則該遲到的數據會被Spark放棄更新,也就是丟棄。
Watermarking=
max event time seen by engine - late threshold
Watermarking保障機制:
- 能夠保證在window的EndTime > 水位線的窗口的狀態Spark會存儲起來,這個時候如果有遲到的
數據再水位線沒有淹沒window之前Spark可以保障遲到的數據能正常的處理。 - 如果水位線已經沒過了窗口的end時間,那麼後續遲到數據不一定能夠被處理,換句話說,遲到越
久的數據 被處理的機率越小。
如果是使用
水位線計算
,輸出模式必須是Update或者Append,否則系統不會刪除。
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.apache.jetty.server").setLevel(Level.OFF)
//1.創建SparkSession
val spark=SparkSession
.builder()
.master("local[*]")
.appName("printline")
.getOrCreate()
import spark.implicits._
//字符,時間戳
var df=spark.readStream .format("socket")
.option("host", "localhost")
.option("port", "9999")
.load()
import org.apache.spark.sql.functions._
var sdf=new SimpleDateFormat("mm:ss")
val wordCounts=df.select("value")
.as[String]
.map(_.split(","))
// 這裏的Timestamp導如java.sql的依賴
.map(tokens=>(tokens(0),new Timestamp(tokens(1).toLong)))
.toDF("word","timestamp")
// 與上面窗口的API相比,多了水位線的設置
.withWatermark("timestamp", "5 seconds")
.groupBy(
window($"timestamp","10 seconds","5 seconds"),
$"word"
)
.count()
.map(r=>
(sdf.format(r.getStruct(0).getTimestamp(0)),
sdf.format(r.getStruct(0).getTimestamp(1)),
r.getString(1),r.getLong(2)))
.toDF("start","end","word","count")
val query = wordCounts.writeStream
.outputMode(OutputMode.Update())
.format("console")
.start()
query.awaitTermination()
Spark清除window聚合狀態條件
- Output mode 必須是 Append 或者 Update.,如果是Update 只要窗口有數據更新即可有輸出。
如果是Append,必須當水位線沒過window的時候纔會將Result寫出。
Update Mode
Append Mode
- 必須在分組出現聚合使用時間column/window列
- withWaterMaker的時間column必須和groupBy後面時間column保持一致,例如: 錯誤實例
df.withWatermark("time", "1 min").groupBy("time2").count()
。 - 一定要在分組聚合之前調用withWaterMaking,例如
df.groupBy("time").count().withWatermark("time", "1 min")
錯誤實例
df.withWatermark("time", "1 min").groupBy("time").count()
正確寫法。
Join 操作
Structured Streaming 不僅僅支持對靜態的 Dataset/DataFrame 做join操作,也支持對streaming
Dataset/DataFrame實現join操作。
- Stream-static Joins
spark-2.0
支持 - Stream-stream Joins
Spark 2.3
支持
Stream-static Joins
//1.創建SparkSession
val spark=SparkSession
.builder()
.master("local[6]")
.appName("printline")
.getOrCreate()
import spark.implicits._
import org.apache.spark.sql.functions._
/**
* +---+------+---+
* | id| name|age|
* +---+------+---+
* | 1| 張三 | 18|
* | 2| lisi| 28|
* | 3|wangwu| 38|
* +---+------+---+
*/
val userDF=spark.read
.format("json")
.load("/Users/mashikang/IdeaProjects/structured_stream/src/main/resources/json")
.selectExpr("CAST(id AS INTEGER)","name","CAST(age AS INTEGER)")
//1 apple 1 4.5
var orderItemDF= spark.readStream
.format("socket")
.option("host","localhost")
.option("port",9999)
.load() .as[String]
.map(line=>line.split("\\s+"))
.map(tokens=>(tokens(0).toInt,tokens(1),tokens(2).toInt,tokens(3).toDouble))
.toDF("uid","item","count","price")
val jointResults = orderItemDF.join(userDF,$"id"===$"uid","left_outer")
val query = jointResults
.writeStream
.format("console")
.outputMode(OutputMode.Append())
.start()
query.awaitTermination()
Stream-stream Joins
- 兩邊流都必須聲明watermarker,告知引擎什麼是可以清楚狀態(默認取最低)。
- 需要在連接條件中添加eventTime column的時間約束,這樣引擎就知道什麼時候可以清除後續
的流的狀態。
- Time range join conditions
- Join on event-time windows
inner join
- 方案1 Time range join conditions
//1.創建SparkSession
val spark = SparkSession
.builder()
.master("local[*]")
.appName("printline")
.getOrCreate()
import spark.implicits._
//001 apple 1 4.5 1566529410000
val orderDF = spark
.readStream
.format("socket")
.option("host", "localhost")
.option("port", 9999)
.load()
.map(row => row.getAs[String]("value").split("\\s+"))
.map(t => (t(0), t(1), (t(2).toInt * t(3).toDouble), new Timestamp(t(4).toLong)))
.toDF("uid", "item", "cost", "order_time")
//001 zhangsan 1566529410000
val userDF = spark
.readStream
.format("socket")
.option("host", "localhost")
.option("port", 8888)
.load()
.map(row => row.getAs[String]("value").split("\\s+"))
.map(t => (t(0), t(1), new Timestamp(t(2).toLong)))
.toDF("id", "name", "login_time")
import org.apache.spark.sql.functions._
//用戶的登陸數據緩存 2 seconds 訂單數據緩存4秒
val userWatermarker = userDF.withWatermark("login_time", "2 seconds")
val orderWaterMarker = orderDF.withWatermark("order_time", "4 seconds")
//連接 用戶登陸以後將2秒以內的購買行爲和用進行join
val joinDF = userWatermarker.join(orderWaterMarker,
expr(
"""
|id=uid and order_time >= login_time and order_time <= login_time + interval 2 seconds
""".stripMargin)
)
val query = joinDF.writeStream
.format("console")
.outputMode(OutputMode.Append()).start()
query.awaitTermination()
- 方案2Join on event-time windows
//1.創建SparkSession
val spark = SparkSession
.builder()
.master("local[*]")
.appName("printline")
.getOrCreate()
import spark.implicits._
//001 apple 1 4.5 1566529410000
val orderDF = spark
.readStream
.format("socket")
.option("host", "localhost")
.option("port", 9999)
.load()
.map(row => row.getAs[String]("value").split("\\s+"))
.map(t => (t(0), t(1), (t(2).toInt * t(3).toDouble), new Timestamp(t(4).toLong)))
.toDF("uid", "item", "cost", "order_time")
//001 zhangsan 1566529410000
val userDF = spark
.readStream
.format("socket")
.option("host", "localhost")
.option("port", 8888)
.load()
.map(row => row.getAs[String]("value").split("\\s+"))
.map(t => (t(0), t(1), new Timestamp(t(2).toLong)))
.toDF("id", "name", "login_time")
import org.apache.spark.sql.functions._
//用戶的登陸數據緩存 2 seconds 訂單數據緩存4秒
val userWatermarker = userDF.withWatermark("login_time", "2 seconds")
.select(
window($"login_time", "5 seconds"),
$"id", $"name", $"login_time")
.withColumnRenamed("window", "leftWindow")
val orderWaterMarker = orderDF.withWatermark("order_time", "4 seconds")
.select(
window($"order_time", "5 seconds"),
$"uid", $"item", $"cost", $"order_time")
.withColumnRenamed("window", "rightWindow")
//連接用戶登陸以後將2秒以內的購買行爲和用進行join
val joinDF = userWatermarker
.join(
orderWaterMarker,
expr(
"""
|id=uid and leftWindow = rightWindow
""".stripMargin)
)
val query = joinDF.writeStream
.format("console")
.outputMode(OutputMode.Append()).start()
query.awaitTermination()
outer join
//1.創建SparkSession
val spark = SparkSession
.builder()
.master("local[*]")
.appName("printline")
.getOrCreate()
import spark.implicits._
//001 apple 1 4.5 1566529410000
val orderDF = spark
.readStream
.format("socket")
.option("host", "localhost")
.option("port", 9999)
.load()
.map(row => row.getAs[String]("value").split("\\s+"))
.map(t => (t(0), t(1), (t(2).toInt * t(3).toDouble), new Timestamp(t(4).toLong)))
.toDF("uid", "item", "cost", "order_time")
//001 zhangsan 1566529410000
val userDF = spark
.readStream
.format("socket")
.option("host", "localhost")
.option("port", 8888)
.load()
.map(row => row.getAs[String]("value").split("\\s+"))
.map(t => (t(0), t(1), new Timestamp(t(2).toLong)))
.toDF("id", "name", "login_time")
import org.apache.spark.sql.functions._
//系統分別會對 user 和 order 緩存 最近 1 seconds 和 2 seconds 數據,
// 一旦時間過去,系統就無 法保證數據狀態繼續保留
val loginWatermarker=userDF.withWatermark("login_time","1 second")
val orderWatermarker=orderDF.withWatermark("order_time","2 seconds")
//計算訂單的時間 & 用戶 登陸之後的0~1 seconds 關聯 數據 並且進行join
val joinDF = loginWatermarker
.join(
orderWatermarker,
expr("uid=id and order_time >= login_time and order_time <= login_time + interval 1 seconds"),
"leftOuter"
)
val query = joinDF.writeStream
.format("console")
.outputMode(OutputMode.Append()).start()
query.awaitTermination()