Structured Streaming 編程指南

歡迎關注我的微信公眾號:FunnyBigData

概述

Structured Streaming 是一個基于 Spark SQL 引擎的、可擴展的且支持容錯的流處理引擎。你可以像表達靜態數據上的批處理計算一樣表達流計算。Spark SQL 引擎將隨著流式數據的持續到達而持續運行,并不斷更新結果。你可以在Scala,Java,Python或R中使用 Dataset/DataFrame API 來表示流聚合,事件時間窗口(event-time windows),流到批處理連接(stream-to-batch joins)等。計算在相同的優化的 Spark SQL 引擎上執行。最后,通過 checkpoint 和 WAL,系統確保端到端的 exactly-once。簡而言之,Structured Streaming 提供了快速、可擴展的、容錯的、端到端 exactly-once 的流處理。

在本指南中,我們將引導你熟悉編程模型和 API。首先,我們從一個簡單的例子開始:streaming word count。

快速示例

假設要監聽從本機 9999 端口發送的文本的 WordCount,讓我們看看如何使用結構化流式表達這一點。 首先,必須 import 必須的類并創建 SparkSession

import org.apache.spark.sql.functions._
import org.apache.spark.sql.SparkSession

val spark = SparkSession
  .builder
  .appName("StructuredNetworkWordCount")
  .getOrCreate()
  
import spark.implicits._

然后,創建一個流式 Streaming DataFrame 來代表不斷從 localhost:9999 接收數據,并在該 DataFrame 上執行 transform 來計算 word counts。

// Create DataFrame representing the stream of input lines from connection to localhost:9999
val lines = spark.readStream
  .format("socket")
  .option("host", "localhost")
  .option("port", 9999)
  .load()

// Split the lines into words
val words = lines.as[String].flatMap(_.split(" "))

// Generate running word count
val wordCounts = words.groupBy("value").count()

DataFrame lines 代表一個包含流數據的無限的表。該表包含一個 string 類型的 value 列,流數據里的每條數據變成了該表中的一行。接下來,我們調用 .as[String] 將 DataFrame 轉化為 Dataset,這樣我們就可以執行 flatMap 來 split 一行為多個 words。返回值 Dataset words 包含所有的 words。最后,執行 words.groupBy("value").count() 得到 wordCounts注意,這是一個流式的 DataFrame,代表這個流持續運行中的 word counts

現在我們設置好了要在流式數據上執行的查詢,接下來要做的就是真正啟動數據接收和計算。要做到這一點,我們設置了每當結果有更新就輸出完整的結果(通過 outputMode("complete")指定)至控制臺。然后調用 start 來啟動流計算。

// Start running the query that prints the running counts to the console
val query = wordCounts.writeStream
  .outputMode("complete")
  .format("console")
  .start()

query.awaitTermination()

當上面的代碼運行起來后,流式計算會在后臺啟動,.awaitTermination() 會一直等待到計算結束。

另外,需要執行 Netcat 來向 localhost:9999 發送數據,比如:

$ nc -lk 9999
apache spark
apache hadoop
...

然后,計算再接收到數據后會不斷打印出結果:

# TERMINAL 2: RUNNING StructuredNetworkWordCount

-------------------------------------------
Batch: 0
-------------------------------------------
+------+-----+
| value|count|
+------+-----+
|apache|    1|
| spark|    1|
+------+-----+

-------------------------------------------
Batch: 1
-------------------------------------------
+------+-----+
| value|count|
+------+-----+
|apache|    2|
| spark|    1|
|hadoop|    1|
+------+-----+
...

編程模型

Structured Streaming 的關鍵思想是將持續不斷的數據當做一個不斷追加的表。這使得流式計算模型與批處理計算引擎十分相似。你將使用類似對于靜態表的批處理方式來表達流計算,然后 Spark 以在無限表上的增量計算來運行。

基本概念

將輸入的流數據當做一張 “輸入表”。把每一條到達的數據作為輸入表的新的一行來追加

在輸入表上執行的查詢將會生成 “結果表”。每個觸發間隔(trigger interval)(例如 1s),新的行追加到輸入表,最終更新結果表。無論何時更新結果表,我們都希望將更改的結果行 output 到外部存儲/接收器(external sink)。

output 有以下三種模式:

  • Complete Mode:整個更新的結果表將被寫入外部存儲。由存儲連接器(storage connector)決定如何處理整個表的寫入
  • Append Mode:只有結果表中自上次觸發后附加的新行將被寫入外部存儲。這僅適用于不期望更改結果表中現有行的查詢
  • Update Mode:只有自上次觸發后結果表中更新的行將被寫入外部存儲(自 Spark 2.1.1 起可用)。 請注意,這與完全模式不同,因為此模式僅輸出自上次觸發以來更改的行。如果查詢不包含聚合操作,它將等同于附加模式

請注意,每種模式適用于某些類型的查詢。這將在后面詳細討論。

為了說明這個模型的使用,讓我們來進一步理解上面的快速示例:

  • 最開始的 DataFrame lines 為輸入表
  • 最后的 DataFrame wordCounts 為結果表

在流上執行的查詢將 DataFrame lines 轉化為 DataFrame wordCounts 與在靜態 DataFrame 上執行的操作完全相同。當啟動計算后,Spark 會不斷從 socket 連接接收數據。如果有新的數據到達,Spark將運行一個 “增量” 查詢,將以前的 counts 與新數據相結合,以計算更新的 counts,如下所示:

這種模式與許多其他流處理引擎有顯著差異。許多流處理引擎要求用戶自己維護運行的狀態,因此必須對容錯和數據一致性(at-least-once, or at-most-once, or exactly-once)進行處理。 在這個模型中,當有新數據時,Spark負責更新結果表,從而減輕用戶的工作。作為例子,我們來看看該模型如何處理 event-time 和延遲的數據。

處理 event-time 和延遲數據

event-time 是嵌入在數據中的時間。對于許多 application,你可能希望在 event-time 上進行操作。例如,如果要每分鐘獲取IoT設備生成的事件數,則會希望使用數據生成的時間(即嵌入在數據中的 event-time),而不是 Spark 接收到數據的時間。在該模型中 event-time 被非常自然的表達,來自設備的每個事件都是表中的一行,event-time 是行中的一列。這允許基于 window 的聚合(例如每分鐘的事件數)僅僅是 event-time 列上的特殊類型的分組(grouping)和聚合(aggregation):每個時間窗口是一個組,并且每一行可以屬于多個窗口/組。因此,可以在靜態數據集和數據流上進行基于事件時間窗口( event-time-window-based)的聚合查詢,從而使用戶操作更加方便。

此外,該模型也可以自然的處理接收到的時間晚于 event-time 的數據。因為 Spark 一直在更新結果表,所以它可以完全控制更新舊的聚合數據,或清除舊的聚合以限制中間狀態數據的大小。自 Spark 2.1 起,開始支持 watermark 來允許用于指定數據的超時時間(即接收時間比 event-time 晚多少),并允許引擎相應的清理舊狀態。這將在下文的 “窗口操作” 小節中進一步說明。

容錯語義

提供端到端的 exactly-once 語義是 Struectured Streaming 背后設計的關鍵目標之一。為了達到這點,設計了 Structured Streaming 的 sources(數據源)、sink(輸出)以及執行引擎可靠的追蹤確切的執行進度以便于通過重啟或重新處理來處理任何類型的故障。對于每個具有偏移量(類似于 Kafka 偏移量或 Kinesis 序列號)的 streaming source。引擎使用 checkpoint 和 WAL 來記錄每個 trigger 處理的 offset 范圍。streaming sinks 被設計為對重新處理是冪等的。結合可以重放的 sources 和支持重復處理冪等的 sinks,不管發生什么故障 Structured Streaming 可以確保端到端的 exactly-once 語義。

使用 Datasets 和 DataFrames API

自 Spark 2.0 起,Spark 可以代表靜態的、有限數據和流式的、無限數據。與靜態的 Datasets/DataFrames 類似, 你可以使用 SparkSession 基于 streaming sources 來創建 DataFrames/Datasets,并且與靜態 DataFrames/Datasets 使用相同的操作。

創建流式 DataFrames 和流式 Datasets

流式 DataFrames 可以通過 DataStreamReader 創建,DataStreamReader 通過調用 SparkSession.readStream() 創建。與靜態的 read() 方法類似,你可以指定 source 的詳細信息:格式、schema、選項等。

輸入源

在 Spark 2.0 中,只有幾個內置的 sources:

  • File source:以文件流的形式讀取目錄中寫入的文件。支持的文件格式為text,csv,json,parquet。請注意,文件必須以原子方式放置在給定的目錄中,這在大多數文件系統中可以通過文件移動操作實現
  • Kafka source:從 Kafka 拉取數據。兼容 Kafka 0.10.0 以及更高版本。
  • Socket source(僅做測試用):從 socket 讀取 UTF-8 文本數據。請注意,這只能用于測試,因為它不提供端到端的容錯

某些 source 不是容錯的,因為它們不能保證在故障后可以重放數據。以下是 Spark 中所有 sources 的詳細信息:

  • File Source:
    • options:
      • path:輸入目錄的路徑,所有格式通用
      • maxFilesPerTrigger:每次 trigger 最大文件數(默認無限大)
      • latestFirst:是否首先處理最新的文件,當有大量積壓的文件時很有用(默認 false)
      • fileNameOnly:是否僅根據文件名而不是完整路徑檢查新文件(默認 false)。將此設置為“true”,以下文件將被視為相同的文件,因為它們的文件名“dataset.txt”是相同的:"file:///dataset.txt"、"s3://a/dataset.txt"、"s3n://a/b/dataset.txt"、"s3a://a/b/c/dataset.txt"
    • 容錯:支持
    • 注意:支持通配符路徑,但不支持逗號分隔的多個路徑/通配符路徑
  • Socket Source:
    • options:
      • host: 要連接的 host, 必須指定
      • port: 要連接的 port, 必須指定
    • 容錯:不支持
    • 注意:無
  • Kafka Source:

以下是一些例子:

val spark: SparkSession = ...

// Read text from socket
val socketDF = spark
  .readStream
  .format("socket")
  .option("host", "localhost")
  .option("port", 9999)
  .load()

socketDF.isStreaming    // Returns True for DataFrames that have streaming sources

socketDF.printSchema

// Read all the csv files written atomically in a directory
val userSchema = new StructType().add("name", "string").add("age", "integer")
val csvDF = spark
  .readStream
  .option("sep", ";")
  .schema(userSchema)      // Specify schema of the csv files
  .csv("/path/to/directory")    // Equivalent to format("csv").load("/path/to/directory")

這些示例生成的流 DataFrames 是無類型的,在編譯時并不會進行類型檢查,只在運行時進行檢查。某些操作,比如 map、flatMap 等,需要在編譯時就知道類型,這時你可以將 DataFrame 轉換為 Dataset(使用與靜態相同的方法)。

流式 DataFrames/Datasets 的 schema 推斷和分區

默認情況下,基于 File Source 需要你自行指定 schema,而不是依靠 Spark 自動推斷。這樣的限制確保了 streaming query 會使用確切的 schema。你也可以通過將spark.sql.streaming.schemaInference 設置為 true 來重新啟用 schema 推斷。

當子目錄名為 /key=value/ 時,會自動發現分區,并且對這些子目錄進行遞歸發現。如果這些列出現在提供的 schema 中,spark 會讀取相應目錄的文件并填充這些列。可以增加組成分區的目錄,比如當 /data/year=2015/ 存在是可以增加 /data/year=2016/;但修改分區目錄是無效的,比如創建目錄 /data/date=2016-04-17/

流式 DataFrames/Datasets 上的操作

你可以在流式 DataFrames/Datasets 上應用各種操作:從無類型,類似 SQL 的操作(比如 select、where、groupBy),到類似有類型的 RDD 操作(比如 map、filter、flatMap)。讓我們通過幾個例子來看看。

基本操作 - Selection, Projection, Aggregation

大部分常見的 DataFrame/Dataset 操作也支持流式的 DataFrame/Dataset。少數不支持的操作將會在后面進行討論。

case class DeviceData(device: String, deviceType: String, signal: Double, time: DateTime)

val df: DataFrame = ... // streaming DataFrame with IOT device data with schema { device: string, deviceType: string, signal: double, time: string }
val ds: Dataset[DeviceData] = df.as[DeviceData]    // streaming Dataset with IOT device data

// Select the devices which have signal more than 10
df.select("device").where("signal > 10")      // using untyped APIs   
ds.filter(_.signal > 10).map(_.device)         // using typed APIs

// Running count of the number of updates for each device type
df.groupBy("deviceType").count()                          // using untyped API

// Running average signal for each device type
import org.apache.spark.sql.expressions.scalalang.typed
ds.groupByKey(_.deviceType).agg(typed.avg(_.signal))    // using typed API

event-time(事件時間)上的 window 操作

使用 Structured Streaming 進行滑動的 event-time 窗口聚合是很簡單的,與分組聚合非常類似。在分組聚合中,為用戶指定的分組列中的每個唯一值維護一個聚合值(例如計數)。在基于 window 的聚合的情況下,為每個 window 維護聚合(aggregate values),流式追加的行根據 event-time 落入相應的聚合。讓我們通過下圖來理解。

想象下,我們的快速示例現在改成了包含數據生成的時間。現在我們想在 10 分鐘的 window 內計算 word count,每 5 分鐘更新一次。比如 12:00 - 12:10, 12:05 - 12:15, 12:10 - 12:20 等。12:00 - 12:10 是指數據在 12:00 之后 12:10 之前到達。現在,考慮一個 word 在 12:07 的時候接收到。該 word 應當增加 12:00 - 12:1012:05 - 12:15 相應的 counts。所以 counts 會被分組的 key 和 window 分組。

結果表將如下所示:

由于這里的 window 與 group 非常類似,在代碼上,你可以使用 groupBywindow 來表達 window 聚合。例子如下:

import spark.implicits._

val words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String }

// Group the data by window and word and compute the count of each group
val windowedCounts = words.groupBy(
  window($"timestamp", "10 minutes", "5 minutes"),
  $"word"
).count()

Watermark 和延遲數據處理

現在考慮一個數據延遲到達會怎么樣。例如,一個在 12:04 生成的 word 在 12:11 被接收到。application 會使用 12:04 而不是 12:11 去更新 12:00 - 12:10的 counts。這在基于 window 的分組中很常見。Structured Streaming 會長時間維持部分聚合的中間狀態,以便于后期數據可以正確更新舊 window 的聚合,如下所示:

然后,當 query 運行了好幾天,系統必須限制其累積的內存中中間狀態的數量。這意味著系統需要知道什么時候可以從內存狀態中刪除舊的聚合,因為 application 不會再為該聚合更晚的數據進行聚合操作。為啟動此功能,在Spark 2.1中,引入了 watermark(水印),使引擎自動跟蹤數據中的當前事件時間,并相應地清理舊狀態。你可以通過指定事件時間列來定義一個 query 的 watermark 和 late threshold(延遲時間閾值)。對于一個開始于 T 的 window,引擎會保持中間狀態并允許后期的數據對該狀態進行更新直到 max event time seen by the engine - late threshold > T。換句話說,在延遲時間閾值范圍內的延遲數據會被聚合,但超過該閾值的數據會被丟棄。讓我們以一個例子來理解這一點。我們可以使用 withWatermark() 定義一個 watermark,如下所示:

import spark.implicits._

val words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String }

// Group the data by window and word and compute the count of each group
val windowedCounts = words
    .withWatermark("timestamp", "10 minutes")
    .groupBy(
        window($"timestamp", "10 minutes", "5 minutes"),
        $"word")
    .count()

在這個例子中,我們定義了基于 timestamp 列定義了 watermark,并且將 10 分鐘定義為允許數據延遲的閾值。如果該數據以 update 輸出模式運行:

  • 引擎將不斷更新結果表中 window 中的 counts 直到該 window 比 watermark 更舊
  • 數據中的 timestamp 值比當前的最大 event-time 落后 10 分鐘以上的數據將被丟棄

以下為示圖:

如圖所示,引擎跟蹤的最大 event-time 是藍色虛線,并且在每個 trigger 開始時設置 watermark 為 (max event time - '10 mins') 的紅線例如,當引擎發現 (12:14, dog) 時將下次 trigger 的 watermark 設置為 12:04。然后,當 watermark 更新為 12:11 時,window (12:00 - 12:10) 的中間狀態被清除,所有后續數據(例如(12:04,donkey))被認為是“太晚”,因此被丟棄。根據 output 模式,每次觸發后,更新的計數(即紫色行)都將作為觸發輸出進行寫入到 sink。

某些 sink(例如文件)可能不支持 update mode 所需的細粒度更新。所以,我們還支持 append 模式,只有最后確定的計數被寫入。這如下圖所示。

注意,在非流式 Dataset 上使用 withWatermark 是無效的空操作。

與之前的 update mode 類似,引擎維護每個 window 的中間計數。只有當 window < watermark 時才會刪除 window 的中間狀態數據,并將該 window 最終的 counts 追加到結果表或 sink 中。例如,window 12:00 - 12:10 的最終結果將在 watermark 更新到 12:11 后再追加到結果表中。

watermark 清除聚合狀態的條件十分重要,為了清理聚合狀態,必須滿足以下條件(自 Spark 2.1.1 起,將來可能會有變化):

  • output mode 必須為 append 或 update:complete mode 需要保留所有的聚合數據,因此 watermark 不能用來清理聚合數據
  • 聚合必須具有 event-time 列或基于 event-time 的 window
  • withWatermark 必須調用在用來聚合的時間列上。比如 df.withWatermark("time", "1 min").groupBy("time2").count() 是無效的
  • withWatermark 必須在調用聚合前調用來說明 watermark 的細節。比如,df.groupBy("time").count().withWatermark("time", "1 min") 是無效的

Join 操作

流式 DataFrames 可以與靜態 DataFrames 進行 join 來創建新的流式 DataFrames。如下:

val staticDf = spark.read. ...
val streamingDf = spark.readStream. ...

streamingDf.join(staticDf, "type")          // inner equi-join with a static DF
streamingDf.join(staticDf, "type", "right_join")  // right outer join with a static DF

流重復數據的刪除(去重)

你可以使用事件中的唯一標識符對數據流中的記錄進行重復數據刪除。這與使用唯一標識符列的靜態重復數據消除完全相同。該查詢會存儲所需的一定量先前的數據,以便可以過濾重復的記錄。類似于聚合,你可以使用或不使用 watermark 來刪除重復數據,如下例子:

  • 使用 watermark:如果重復記錄可能到達的時間有上限,則可以在事件時間列上定義 watermark,并使用 guid 和事件時間列進行重復數據刪除
  • 不使用 watermark:由于重復記錄可能到達的時間沒有上限,會將來自過去所有記錄的數據存儲為狀態
val streamingDf = spark.readStream. ...  // columns: guid, eventTime, ...

// Without watermark using guid column
streamingDf.dropDuplicates("guid")

// With watermark using guid and eventTime columns
streamingDf
  .withWatermark("eventTime", "10 seconds")
  .dropDuplicates("guid", "eventTime")

任意有狀態的操作

許多場景需要使用比聚合更復雜的狀態操作,可能不得不把任意類型的數據保存為狀態,并使用每個 trigger 中的流式事件對狀態執行任意操作。自 Spark2.2 起,這可以通過調用 mapGroupWithStateflatMapGroupWithState 做到。這兩個操作都允許你在分組的數據集上應用用戶定義的代碼來更新用戶定義的狀態,有關更具體的細節,請查看API文檔 GroupStateexample

不支持的操作

DataFrame/Dataset 有一些操作是流式 DataFrame/Dataset 不支持的,其中的一些如下:

  • 不支持多個流聚合
  • 不支持 limit、first、take 這些取 N 條 Row 的操作
  • 不支持 Distinct
  • 只有當 output mode 為 complete 時才支持排序操作
  • 有條件地支持流和靜態數據集之間的外連接:
    • 不支持與流式 Dataset 的全外連接(full outer join)
    • 不支持左側外連接(left outer join)與右側的流式 Dataset
    • 右側外連接與左側的流式 Dataset 不支持

此外,還有一些 Dataset 方法將不適用于流數據集。它們是立即運行查詢并返回結果的操作,這在流數據集上沒有意義。相反,這些功能可以通過顯式啟動流式查詢來完成。

  • count():無法從流式 Dataset 返回單個計數。而是使用 ds.groupBy().count() 返回一個包含運行計數的 streaming Dataset
  • foreach():使用 ds.writeStream.foreach(...) 代替
  • show():使用輸出到 console sink 代替

如果你執行了這些操作,你會看到一個 AnalysisException,像 operation XYZ is not supported with streaming DataFrames/Datasets”。雖然其中一些可能在未來版本的 Spark 中得到支持,還有其他一些從根本上難以有效地實現。例如,不支持對輸入流進行排序,因為它需要跟蹤流中接收到的所有數據,這從根本上是很難做到的。

啟動流式查詢

一旦定義了最終的結果 DataFrame/Dataset,剩下的就要啟動流計算。要做到這一點,必須使用通過調用 Dataset.writeStream() 返回的 DataStreamWriter。必須指定以下的一個或多個:

  • output sink 細節:data format、location 等
  • output mode
  • query name:可選的,指定用于識別的查詢的唯一名稱
  • trigger interval:可選的,如果沒有指定,則系統將在上一次處理完成后立即檢查是否有新的可用數據。如果由于上一次的觸發還未完成導致下一次的觸發時間錯過了,系統會在下一次的觸發時間進行觸發而不是在上一次觸發結束后立馬觸發
  • checkpoint location:對于那些可以保證端到端容錯的 output sinks,系統會往指定的 location 寫入所有的 checkpoint 信息。該 location 必須是一個 HDFS 兼容的文件系統。checkpoint 會在下一節中進行更詳細得介紹

Output Modes

有幾種類型的輸出模式:

  • Append mode(默認的):這是默認模式,其中只有從上次觸發后添加到結果表的新行將被輸出到 sink。適用于那些添加到結果表中的行從不會更改的查詢。只有 select、where、map、flatMap、filter、join 等查詢會支持 Append mode
  • Complete mode:每次 trigger 后,整個結果表將被輸出到 sink。聚合查詢(aggregation queries)支持該模式
  • Update mode:(自 Spark 2.1.1 可用)。只有結果表中自上次 trigger 后更新的行將被輸出到 sink

不同類型的流式 query 支持不同的 output mode。以下是兼容性:

輸出接收器(Output sink)

有幾種類型的內置輸出接收器。

  • File sink:存儲輸出至目錄:
writeStream
    .format("parquet")        // can be "orc", "json", "csv", etc.
    .option("path", "path/to/destination/dir")
    .start()
  • Foreach sink:對輸出中的記錄運行任意計算:
writeStream
    .foreach(...)
    .start()
  • Console sink(用來調試):每次 trigger 將輸出打印到控制臺。支持 Append 和 Complete 模式。僅適用于小數據量的調試之用,因為在每次 trigger 之后,完整的輸出會被存儲在 driver 的內存中,請謹慎使用:
writeStream
    .format("console")
    .start()
  • Memory sink(用來調試):輸出作為內存表存儲在內存中。支持 Append 和 Complete 模式。僅適用于小數據量的調試之用,因為在每次 trigger 之后,完整的輸出會被存儲在 driver 的內存中,請謹慎使用:
writeStream
    .format("memory")
    .queryName("tableName")
    .start()

某些接收器不容錯,因為它們不保證輸出的持久性,僅用于調試目的。請參閱上一節關于容錯語義的部分。以下是 Spark 中所有內置接收器的詳細信息:

請注意,必須調用 start() 來實際啟動查詢的執行。這將返回一個 StreamingQuery 對象,它是持續運行的查詢的句柄。你可以使用該對象來管理查詢,我們將在下一小節中討論。現在,讓我們通過幾個例子來了解:

// ========== DF with no aggregations ==========
val noAggDF = deviceDataDf.select("device").where("signal > 10")   

// Print new data to console
noAggDF
  .writeStream
  .format("console")
  .start()

// Write new data to Parquet files
noAggDF
  .writeStream
  .format("parquet")
  .option("checkpointLocation", "path/to/checkpoint/dir")
  .option("path", "path/to/destination/dir")
  .start()

// ========== DF with aggregation ==========
val aggDF = df.groupBy("device").count()

// Print updated aggregations to console
aggDF
  .writeStream
  .outputMode("complete")
  .format("console")
  .start()

// Have all the aggregates in an in-memory table
aggDF
  .writeStream
  .queryName("aggregates")    // this query name will be the table name
  .outputMode("complete")
  .format("memory")
  .start()

spark.sql("select * from aggregates").show()   // interactively query in-memory table

使用 Foreach

foreach 操作允許在輸出數據上進行任意操作。在 Spark 2.1 中,只有 Scala 和 Java 可用。要使用這個,你必須實現 ForeachWriter 接口,其具有每次 trigger 后每當有一系列行生成時會調用的方法,注意一下幾個要點:

  • writer 必須是可序列化的,因為它將被序列化并發送給 executor 執行
  • open、process 和 close 會在 executors 上被調用
  • 只有當 open 方法被調用時 writer 才執行所有的初始化。請注意,如果在創建對象時立即進行任何初始化,那么該初始化將在 driver 中發生,這可能不是你預期的
  • open 方法可以使用 version 和 partition 來決定是否需要寫入序列的行。可以返回 true(繼續寫入)或 false(無需寫入)。如果返回 false,process 不會在任何行上被調用。例如,在部分失敗之后,失敗的 trigger 的部分輸出分區可能已經被提交到數據庫。基于存儲在數據庫中的元數據,可以識別已經提交的分區,因此返回 false 以避免再次提交它們。
  • 每當 open 被調用,close 也會被調用(除非 JVM 因為意外退出)。即使 open 返回 false 也是如此。如果在處理和寫入數據的時候發生錯誤,close 會被調用。你有責任清理在 open 中創建的狀態(例如連接,事務等),以免資源泄漏

管理流式查詢

當 query 啟動時,StreamingQuery 被創建,可以用來監控和管理該 query:

val query = df.writeStream.format("console").start()   // get the query object

query.id          // get the unique identifier of the running query that persists across restarts from checkpoint data

query.runId       // get the unique id of this run of the query, which will be generated at every start/restart

query.name        // get the name of the auto-generated or user-specified name

query.explain()   // print detailed explanations of the query

query.stop()      // stop the query

query.awaitTermination()   // block until query is terminated, with stop() or with error

query.exception       // the exception if the query has been terminated with error

query.recentProgress  // an array of the most recent progress updates for this query

query.lastProgress    // the most recent progress update of this streaming query

可以在單個 SparkSession 中啟動任意數量的 query。他們都將同時運行共享集群資源。可以調用 sparkSession.streams() 來獲取 StreamingQueryManager,可以用來管理當前 active queries:

val spark: SparkSession = ...

spark.streams.active    // get the list of currently active streaming queries

spark.streams.get(id)   // get a query object by its unique id

spark.streams.awaitAnyTermination()   // block until any one of them terminates

監控流式查詢

有兩種 API 用于監控和調試 active queries:以交互方式和異步方式。

交互式 APIs(Interactive APIs)

你可以調用 streamingQuery.lastProgress()streamingQuery.status() 來直接獲取某個 query 的當前的狀態和指標。lastProgress 返回一個 StreamingQueryProgress 對象。它具有關于流最后一個 trigger 的進度的所有信息,包括處理哪些數據、處理速度、處理延遲等。還有 streamingQuery.recentProgress 返回最后幾個進度的數組。

另外,streamingQuery.status() 返回一個 StreamingQueryStatus。它提供了有關 query 執行的信息,比如是否有 trigger active,是否有數據正在被處理等。

以下是一些例子:

val query: StreamingQuery = ...

println(query.lastProgress)

/* Will print something like the following.

{
  "id" : "ce011fdc-8762-4dcb-84eb-a77333e28109",
  "runId" : "88e2ff94-ede0-45a8-b687-6316fbef529a",
  "name" : "MyQuery",
  "timestamp" : "2016-12-14T18:45:24.873Z",
  "numInputRows" : 10,
  "inputRowsPerSecond" : 120.0,
  "processedRowsPerSecond" : 200.0,
  "durationMs" : {
    "triggerExecution" : 3,
    "getOffset" : 2
  },
  "eventTime" : {
    "watermark" : "2016-12-14T18:45:24.873Z"
  },
  "stateOperators" : [ ],
  "sources" : [ {
    "description" : "KafkaSource[Subscribe[topic-0]]",
    "startOffset" : {
      "topic-0" : {
        "2" : 0,
        "4" : 1,
        "1" : 1,
        "3" : 1,
        "0" : 1
      }
    },
    "endOffset" : {
      "topic-0" : {
        "2" : 0,
        "4" : 115,
        "1" : 134,
        "3" : 21,
        "0" : 534
      }
    },
    "numInputRows" : 10,
    "inputRowsPerSecond" : 120.0,
    "processedRowsPerSecond" : 200.0
  } ],
  "sink" : {
    "description" : "MemorySink"
  }
}
*/


println(query.status)

/*  Will print something like the following.
{
  "message" : "Waiting for data to arrive",
  "isDataAvailable" : false,
  "isTriggerActive" : false
}
*/

異步 API

你還可以通過附加 StreamingQueryListener 異步監控與 SparkSession 關聯的所有查詢。一旦你通過 sparkSession.streams.attachListener() 附加了自定義的 StreamingQueryListener 對象,當 query 啟動、結束、active 查詢有進展時就會被回調。下面是一個例子:

val spark: SparkSession = ...

spark.streams.addListener(new StreamingQueryListener() {
    override def onQueryStarted(queryStarted: QueryStartedEvent): Unit = {
        println("Query started: " + queryStarted.id)
    }
    override def onQueryTerminated(queryTerminated: QueryTerminatedEvent): Unit = {
        println("Query terminated: " + queryTerminated.id)
    }
    override def onQueryProgress(queryProgress: QueryProgressEvent): Unit = {
        println("Query made progress: " + queryProgress.progress)
    }
})

使用 checkpoint 從失敗中恢復

在失敗或主動 shutdown 的情況下,可以恢復之前的查詢進度和狀態并從該處繼續運行。這是依賴 checkpoint 和 WAL(write ahead logs) 來完成的。你可以配置一個 checkpoint 路徑,query 會將進度信息(比如每個 trigger 處理的 offset ranger)和運行中的聚合寫入到 checkpoint 的位置。checkpoint 的路徑必須是一個 HDFS 兼容的文件系統,并且需要在定義 query 的時候設置好,如下:

aggDF
  .writeStream
  .outputMode("complete")
  .option("checkpointLocation", "path/to/HDFS/dir")
  .format("memory")
  .start()
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容