StructuredStreaming編程指南

1、概述

結(jié)構(gòu)化流是一個基于Spark SQL引擎的可擴展、容錯的流處理引擎。您可以用在靜態(tài)數(shù)據(jù)上表示批處理計算的方式來表示流計算。Spark SQL引擎將負責遞增和連續(xù)地運行它,并在流數(shù)據(jù)繼續(xù)到達時更新最終結(jié)果。可以使用Scala、Java、Python或R中的DataSet/DataFrAPI API來表示流聚合、事件時間窗口、流到批連接等。在相同的優(yōu)化Sql SQL引擎上執(zhí)行計算。最后,系統(tǒng)通過檢查點和提前寫入日志來確保端到端的容錯性。簡而言之,結(jié)構(gòu)化流提供了快速、可擴展、容錯、端到端的一次性流處理,而用戶無需考慮流。
在內(nèi)部,默認情況下,結(jié)構(gòu)化流式查詢使用微批處理引擎進行處理,該引擎將數(shù)據(jù)流作為一系列小批處理作業(yè)進行處理,從而實現(xiàn)端到端延遲,最短可達100毫秒,并且可以保證容錯性。然而,自Spark 2.3以來,我們引入了一種新的低延遲處理模式,稱為連續(xù)處理,它可以在至少一次保證的情況下實現(xiàn)低至1毫秒的端到端延遲。在查詢中不更改數(shù)據(jù)集/數(shù)據(jù)幀操作的情況下,您可以根據(jù)應(yīng)用程序要求選擇模式。
在本文中,將介紹編程模型和API。將主要使用默認的微批量處理模型來解釋這些概念,然后討論連續(xù)處理模型。首先,從一個簡單的結(jié)構(gòu)化流式查詢示例開始——流式WordCount。

2、快速入門

假設(shè)你希望維護從偵聽TCP Socket的數(shù)據(jù)服務(wù)器接收的文本數(shù)據(jù)的word count。讓我們看看如何使用structured streaming來表達這一點。首先,我們必須導(dǎo)入必要的類并創(chuàng)建本地SparkSession,這是與Spark相關(guān)的所有功能的起點。

import org.apache.spark.api.java.function.FlatMapFunction;
import org.apache.spark.sql.*;
import org.apache.spark.sql.streaming.StreamingQuery;

import java.util.Arrays;
import java.util.Iterator;

SparkSession spark = SparkSession
  .builder()
  .appName("JavaStructuredNetworkWordCount")
  .getOrCreate();

接下來,讓我們創(chuàng)建一個流式DataFrame,它表示從偵聽localhost:9999的服務(wù)器接收到的文本數(shù)據(jù),并轉(zhuǎn)換該DataFrame以計算單詞字數(shù)。

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

// Split the lines into words
Dataset<String> words = lines
  .as(Encoders.STRING())
  .flatMap((FlatMapFunction<String, String>) x -> Arrays.asList(x.split(" ")).iterator(), Encoders.STRING());

// Generate running word count
Dataset<Row> wordCounts = words.groupBy("value").count();

此行數(shù)據(jù)幀表示包含流式文本數(shù)據(jù)的無邊界表。此表包含一列名為“value”的字符串,流式文本數(shù)據(jù)中的每一行將成為表中的一行。注意,由于我們只是在設(shè)置轉(zhuǎn)換,所以目前還沒有接收到任何數(shù)據(jù),而且還沒有啟動轉(zhuǎn)換。接下來,我們使用.as(encoders.string())將數(shù)據(jù)幀轉(zhuǎn)換為字符串數(shù)據(jù)集,這樣我們就可以應(yīng)用flatmap操作將每一行拆分為多個單詞。結(jié)果單詞數(shù)據(jù)集包含所有單詞。最后,我們通過對數(shù)據(jù)集中的唯一值進行g(shù)roupby并對其進行計數(shù)來定義WordCounts數(shù)據(jù)幀。注意,這是一個流數(shù)據(jù)幀,它表示流的運行單詞計數(shù)。

我們現(xiàn)在已經(jīng)設(shè)置了對流數(shù)據(jù)的查詢。剩下的就是實際開始接收數(shù)據(jù)并計算計數(shù)。為此,我們將其設(shè)置為每次更新時向控制臺打印完整的計數(shù)集(由outputmode("complete")指定)。然后使用start()啟動流計算。

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

query.awaitTermination();

執(zhí)行此代碼后,流計算將在后臺啟動。查詢對象是該活動流查詢的句柄,我們決定使用waitTermination()等待查詢的終止,以防止查詢活動時進程退出。

要實際執(zhí)行這個示例代碼,首先需要使用

$ nc -lk 9999

然后,在另一個終端中,可以使用

$ ./bin/run-example org.apache.spark.examples.sql.streaming.StructuredNetworkWordCount localhost 9999

然后,在運行netcat服務(wù)器的終端中鍵入的任何行都將被計數(shù)并每秒在屏幕上打印。像下面這樣:

# TERMINAL 1:
# Running Netcat

$ nc -lk 9999
apache spark
apache hadoop


# TERMINAL 2: RUNNING JavaStructuredNetworkWordCount

$ ./bin/run-example org.apache.spark.examples.sql.streaming.JavaStructuredNetworkWordCount localhost 9999

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

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

3、編程模型

結(jié)構(gòu)化流中的關(guān)鍵思想是將實時數(shù)據(jù)流視為一個不斷追加的表。這導(dǎo)致了一個新的流處理模型,與批處理模型非常相似。您將把流計算表示為與靜態(tài)表類似的標準批處理查詢,spark將在無邊界輸入表上以增量查詢的形式運行它。讓我們更詳細地了解這個模型。

3.1、基本概念

想象把流數(shù)據(jù)當成一個'Input Table',每個data item到來后都會追加到這個table里面。


image.png

對輸入的查詢將生成“結(jié)果表”。每一個觸發(fā)間隔(比如說,每1秒),新的行都會附加到輸入表中,這最終會更新結(jié)果表。每當結(jié)果表更新時,我們都希望將更改后的結(jié)果行寫入外部接收器。

image.png

“輸出”定義為寫入外部存儲器的內(nèi)容。可以在不同的模式下定義輸出:

  • 完成模式-整個更新的結(jié)果表將寫入外部存儲器。由存儲連接器決定如何處理整個表的寫入。
  • 追加模式-只有自上一個觸發(fā)器以來追加到結(jié)果表中的新行才會寫入外部存儲器。這僅適用于結(jié)果表中不希望更改現(xiàn)有行的查詢。
  • 更新模式-只有自上次觸發(fā)器以來在結(jié)果表中更新的行才會寫入外部存儲器(從spark 2.1.1開始可用)。請注意,這與完整模式不同,因為此模式只輸出自上一個觸發(fā)器以來已更改的行。如果查詢不包含聚合,則等同于追加模式。
    請注意,每個模式都適用于某些類型的查詢。這將在后面詳細討論。
    為了說明這個模型的使用,讓我們在上面的快速示例的上下文中理解這個模型。第一行數(shù)據(jù)框是輸入表,最后一行字數(shù)數(shù)據(jù)框是結(jié)果表。請注意,在流式處理行DataFreame上生成單詞計數(shù)的查詢與靜態(tài)DataFreame的查詢完全相同。但是,當這個查詢啟動時,spark將不斷檢查來自socket連接的新數(shù)據(jù)。如果有新的數(shù)據(jù),spark將運行一個“增量”查詢,將以前運行的計數(shù)與新的數(shù)據(jù)結(jié)合起來,以計算更新的計數(shù),如下所示。


    image.png

請注意,結(jié)構(gòu)化流并沒有具體化整個表。它從流數(shù)據(jù)源中讀取最新的可用數(shù)據(jù),然后遞增處理以更新結(jié)果,然后丟棄源數(shù)據(jù)。它只保留更新結(jié)果所需的最小中間狀態(tài)數(shù)據(jù)(例如前面示例中的中間計數(shù))。

此模型與許多其他流處理引擎顯著不同。許多流系統(tǒng)要求用戶自己維護正在運行的聚合,因此必須考慮容錯性和數(shù)據(jù)一致性(至少一次、最多一次或完全一次)。在這個模型中,spark負責在有新數(shù)據(jù)時更新結(jié)果表,從而減少用戶對結(jié)果表的推理。作為一個例子,讓我們看看這個模型如何處理基于事件時間的處理和延遲到達的數(shù)據(jù)。

3.2、處理事件時間和延遲數(shù)據(jù)

事件時間是嵌入到數(shù)據(jù)本身中的時間。對于許多應(yīng)用程序,您可能希望在此事件時間上進行操作。例如,如果您希望獲得每分鐘由物聯(lián)網(wǎng)設(shè)備生成的事件數(shù),那么您可能希望使用生成數(shù)據(jù)的時間(即數(shù)據(jù)中的事件時間),而不是Spark接收數(shù)據(jù)的時間。這個事件時間很自然地用這個模型表示——設(shè)備中的每個事件都是表中的一行,而事件時間是行中的一列值。這允許基于窗口的聚合(例如,每分鐘事件數(shù))只是事件時間列上特殊類型的分組和聚合-每個時間窗口都是一個組,并且每一行可以屬于多個窗口/組。因此,這種基于事件時間窗口的聚合查詢既可以在靜態(tài)數(shù)據(jù)集(例如,從收集的設(shè)備事件日志中)上定義,也可以在數(shù)據(jù)流上定義,從而使用戶的生活更加方便。

此外,該模型根據(jù)事件時間自然地處理比預(yù)期晚到達的數(shù)據(jù)。由于Spark正在更新結(jié)果表,因此它可以完全控制在有延遲數(shù)據(jù)時更新舊聚合,以及清除舊聚合以限制中間狀態(tài)數(shù)據(jù)的大小。自Spark2.1以來,我們支持做標記,允許用戶指定最新數(shù)據(jù)的閾值,并允許引擎相應(yīng)地清除舊狀態(tài)。稍后將在窗口操作部分中更詳細地解釋這些內(nèi)容。

3.3、容錯語義

只交付一次端到端語義是結(jié)構(gòu)化流設(shè)計背后的關(guān)鍵目標之一。為了實現(xiàn)這一點,spark設(shè)計了結(jié)構(gòu)化的流媒體源、接收器和執(zhí)行引擎,以便可靠地跟蹤處理的確切進度,以便通過重新啟動和/或重新處理來處理任何類型的故障。假設(shè)每個流源都有偏移量(類似于Kafka偏移量或Kinesis序列號),以跟蹤流中的讀取位置。引擎使用檢查點和提前寫入日志來記錄每個觸發(fā)器中正在處理的數(shù)據(jù)的偏移范圍。流水槽設(shè)計成等量處理后處理。同時,使用可重放源和等量匯點,結(jié)構(gòu)化流可以確保在任何故障下端到端的語義都是一次性的。

4、使用DataSet和DataFrames的API

由于Spark 2.0,DataSet和DataFrame可以表示靜態(tài)的有界數(shù)據(jù),也可以表示流式的無界數(shù)據(jù)。與靜態(tài)DataSet和DataFrame類似,你可以使用公共入口點SparkSession從流源創(chuàng)建流DataSets和DataFrames,并將它們作為靜態(tài)DataSet和DataFrame應(yīng)用于相同的操作。

4.1、創(chuàng)建流式DataSets和DataFrames

Streaming DataFrames可以通過DataStreamReader接口創(chuàng)建。與創(chuàng)建靜態(tài)DataFrame的讀取接口類似,您可以指定源的詳細信息—數(shù)據(jù)格式、模式、選項等。

  • Input Sources
    有一些內(nèi)置資源。
    1.File source -文件源-讀取以數(shù)據(jù)流形式寫入目錄的文件。支持的文件格式有文本、csv、json、orc、parquet。注意,文件必須原子地放置在給定的目錄中,在大多數(shù)文件系統(tǒng)中,可以通過文件移動操作來實現(xiàn)。
    2.Kafka source -卡夫卡來源-從卡夫卡讀取數(shù)據(jù)。它與Kafka經(jīng)紀人0.10.0或更高版本兼容。有關(guān)詳細信息,請參閱《卡夫卡集成指南》。
    3.Socket source -套接字源(用于測試)-從套接字連接讀取utf8文本數(shù)據(jù)。偵聽服務(wù)器套接字位于驅(qū)動程序處。請注意,這只能用于測試,因為這不提供端到端的容錯保證。
    4.Rate source-速率源(用于測試)-以每秒指定的行數(shù)生成數(shù)據(jù),每個輸出行包含時間戳和值。其中timestamp是包含消息調(diào)度時間的timestamp類型,value是包含消息計數(shù)的long類型,從0開始作為第一行。此源用于測試和基準測試。

有些源不具有容錯性,因為它們不能保證在失敗后可以使用檢查點偏移量重播數(shù)據(jù)。請參見前面關(guān)于容錯語義的部分。以下是Spark中所有來源的詳細信息。


image.png

下面是一些例子。

SparkSession spark = ...

// Read text from socket
Dataset<Row> 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
StructType userSchema = new StructType().add("name", "string").add("age", "integer");
Dataset<Row> 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")

這些示例生成未類型化的流式DataFrame,這意味著在編譯時不檢查DataFrame的架構(gòu),只在提交查詢時在運行時檢查。一些操作(如map、flatmap等)需要在編譯時知道類型。為此,可以使用與靜態(tài)數(shù)據(jù)幀相同的方法將這些非類型化流數(shù)據(jù)幀轉(zhuǎn)換為類型化流數(shù)據(jù)集。有關(guān)詳細信息,請參閱《SQL編程指南》。此外,有關(guān)支持的流媒體源的更多詳細信息將在文檔的后面討論。

4.2、流式DataFrames/Datasets的模式推斷和劃分

您可以對流式DataFrames/Datasets應(yīng)用各種操作—從非類型化、類似SQL的操作(例如select、where、groupby)到類似RDD的類型化操作(例如map、filter、flatmap)。有關(guān)詳細信息,請參閱《SQL編程指南》。讓我們來看幾個您可以使用的示例操作。

  • 基本操作--Selection, Projection, Aggregation
    DataFrame/Dataset上的大多數(shù)常見操作都支持流式處理。本節(jié)稍后將討論一些不受支持的操作。
import org.apache.spark.api.java.function.*;
import org.apache.spark.sql.*;
import org.apache.spark.sql.expressions.javalang.typed;
import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder;

public class DeviceData {
  private String device;
  private String deviceType;
  private Double signal;
  private java.sql.Date time;
  ...
  // Getter and setter methods for each field
}

Dataset<Row> df = ...;    // streaming DataFrame with IOT device data with schema { device: string, type: string, signal: double, time: DateType }
Dataset<DeviceData> ds = df.as(ExpressionEncoder.javaBean(DeviceData.class)); // 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((FilterFunction<DeviceData>) value -> value.getSignal() > 10)
  .map((MapFunction<DeviceData, String>) value -> value.getDevice(), Encoders.STRING());

// 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
ds.groupByKey((MapFunction<DeviceData, String>) value -> value.getDeviceType(), Encoders.STRING())
  .agg(typed.avg((MapFunction<DeviceData, Double>) value -> value.getSignal()));

還可以將流式數(shù)據(jù)幀/數(shù)據(jù)集注冊為臨時視圖,然后對其應(yīng)用SQL命令。

df.createOrReplaceTempView("updates");
spark.sql("select count(*) from updates");  // returns another streaming DF

注意,可以使用df.isStreaming來標識數(shù)據(jù)幀/數(shù)據(jù)集是否具有流數(shù)據(jù)。

df.isStreaming()
4.3、Window Operations on Event Time

滑動事件時間窗口上的聚合對于結(jié)構(gòu)化流非常簡單,并且與分組聚合非常相似。在分組聚合中,為用戶指定的分組列中的每個唯一值維護聚合值(例如計數(shù))。對于基于窗口的聚合,將為行的事件時間所在的每個窗口維護聚合值。讓我們用一個例子來理解這一點。

假設(shè)我們的快速示例被修改了,流現(xiàn)在包含了行以及生成行的時間。我們不需要運行單詞計數(shù),而是希望在10分鐘的窗口內(nèi)對單詞進行計數(shù),每5分鐘更新一次。也就是說,單詞在10分鐘窗口12:00-12:10、12:05-12:15、12:10-12:20等時間段內(nèi)接收的單詞中計數(shù)。請注意,12:00-12:10表示12:00之后但12:10之前到達的數(shù)據(jù)。現(xiàn)在,考慮一下12:07收到的一個詞。這個詞應(yīng)該增加對應(yīng)于兩個窗口12:00-12:10和12:05-12:15的計數(shù)。因此,計數(shù)將由分組鍵(即字)和窗口(可以從事件時間計算)這兩個參數(shù)索引。

結(jié)果表如下所示。


image.png

由于此窗口化與分組類似,因此在代碼中,可以使用groupby()和window()操作來表示窗口化聚合。

Dataset<Row> words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String }

// Group the data by window and word and compute the count of each group
Dataset<Row> windowedCounts = words.groupBy(
  functions.window(words.col("timestamp"), "10 minutes", "5 minutes"),
  words.col("word")
).count();
  • 處理后期數(shù)據(jù)和水印
    現(xiàn)在考慮一下如果其中一個事件延遲到達應(yīng)用程序會發(fā)生什么。例如,在12:04(即事件時間)生成的單詞可以在12:11被應(yīng)用程序接收。應(yīng)用程序應(yīng)使用時間12:04而不是12:11更新窗口12:00-12:10的舊計數(shù)。這在基于窗口的分組中自然發(fā)生——結(jié)構(gòu)化流可以長時間保持部分聚合的中間狀態(tài),以便后期數(shù)據(jù)可以正確更新舊窗口的聚合,如下圖所示。


    image.png

    但是,要運行這個查詢幾天,系統(tǒng)必須綁定它在內(nèi)存狀態(tài)中累積的中間量。這意味著系統(tǒng)需要知道何時可以從內(nèi)存狀態(tài)中除去舊聚合,因為應(yīng)用程序?qū)⒉辉俳邮赵摼酆系难舆t數(shù)據(jù)。為了實現(xiàn)這一點,在Spark2.1中,我們引入了水印技術(shù),它允許引擎自動跟蹤數(shù)據(jù)中的當前事件時間,并嘗試相應(yīng)地清除舊狀態(tài)。您可以通過指定事件時間列和閾值來定義查詢的水印,該閾值說明數(shù)據(jù)在事件時間方面的預(yù)計延遲時間。對于從時間t開始的特定窗口,引擎將保持狀態(tài)并允許延遲數(shù)據(jù)更新狀態(tài),直到(引擎看到的最大事件時間-延遲閾值>t)。換句話說,閾值內(nèi)的延遲數(shù)據(jù)將被聚合,但超過閾值的數(shù)據(jù)將開始下降(有關(guān)確切的保證,請參閱本節(jié)后面的部分)。讓我們用一個例子來理解這一點。我們可以很容易地使用withwatermark()在前面的示例中定義水印,如下所示。

Dataset<Row> words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String }

// Group the data by window and word and compute the count of each group
Dataset<Row> windowedCounts = words
    .withWatermark("timestamp", "10 minutes")
    .groupBy(
        functions.window(words.col("timestamp"), "10 minutes", "5 minutes"),
        words.col("word"))
    .count();

在本例中,我們定義了查詢的水印“timestamp”列的值,還定義了“10分鐘”作為允許數(shù)據(jù)延遲的閾值。如果在更新輸出模式下運行此查詢(稍后在輸出模式部分中討論),則引擎將繼續(xù)更新結(jié)果表中窗口的計數(shù),直到窗口比水印舊,而水印比“timestamp”列中的當前事件時間落后10分鐘。這是一個例子。


image.png

如圖所示,引擎跟蹤的最大事件時間是藍色虛線,每個觸發(fā)器開始時設(shè)置為(最大事件時間-“10分鐘”)的水印是紅線。例如,當引擎觀察數(shù)據(jù)(12:14,dog)時,它將下一個觸發(fā)器的水印設(shè)置為12:04。這個水印允許引擎在額外的10分鐘內(nèi)保持中間狀態(tài),以便計算延遲的數(shù)據(jù)。例如,數(shù)據(jù)(12:09,cat)出現(xiàn)故障和延遲,并落在Windows 12:00-12:10和12:05-12:15中。由于它仍在觸發(fā)器中的水印12:04之前,因此引擎仍將中間計數(shù)保持為狀態(tài),并正確更新相關(guān)窗口的計數(shù)。但是,當水印更新到12:11時,窗口的中間狀態(tài)(12:00-12:10)被清除,所有后續(xù)數(shù)據(jù)(例如(12:04,驢))被視為“太晚”,因此被忽略。請注意,在每個觸發(fā)器之后,更新的計數(shù)(即紫色行)都會寫入sink作為觸發(fā)器輸出,這由更新模式?jīng)Q定。

某些接收器(如文件)可能不支持更新模式所需的細粒度更新。為了使用它們,我們還支持附加模式,其中只有最終計數(shù)被寫入sink。如下所示。
請注意,在非流式數(shù)據(jù)集中使用withwatermark是不起作用的。由于水印不應(yīng)以任何方式影響任何批查詢,因此我們將直接忽略它。


image.png

與之前的更新模式類似,引擎為每個窗口保持中間計數(shù)。但是,部分計數(shù)不會更新到結(jié)果表,也不會寫入接收器。引擎等待“10分鐘”計算延遲日期,然后將窗口的中間狀態(tài)<水印,并將最終計數(shù)附加到結(jié)果表/接收器。例如,只有在水印更新為12:11之后,才會將窗口12:00-12:10的最終計數(shù)追加到結(jié)果表中。

  • 水印清除聚合狀態(tài)的條件
    需要注意的是,水印必須滿足以下條件才能清除聚合查詢中的狀態(tài)(從spark 2.1.1開始,以后可能會更改)。
    1.輸出模式必須是追加或更新。完整模式要求保留所有聚合數(shù)據(jù),因此不能使用水印刪除中間狀態(tài)。有關(guān)每個輸出模式語義的詳細說明,請參閱輸出模式部分。
    2.聚合必須具有事件時間列或事件時間列上的窗口。
    3.必須在與聚合中使用的時間戳列相同的列上調(diào)用WithWatermark。例如,df.withWatermark(“time”,“1 min”).groupby(“time2”).count()在追加輸出模式下無效,因為水印是在聚合列的不同列上定義的。
    4.必須在聚合之前調(diào)用WithWatermark才能使用水印詳細信息。例如,df.groupby(“time”).count().withWatermark(“time”,“1 min”)在追加輸出模式下無效。

  • 水印聚合的語義保證
    水印延遲(用水印設(shè)置)為“2小時”,保證引擎不會丟棄任何延遲時間小于2小時的數(shù)據(jù)。也就是說,任何比最新處理的數(shù)據(jù)晚2小時(就事件時間而言)以內(nèi)的數(shù)據(jù)都保證被聚合。
    但是,擔保只在一個方向上是嚴格的。延遲超過2小時的數(shù)據(jù)不一定會被刪除;它可能會被聚合,也可能不會被聚合。數(shù)據(jù)越晚,引擎處理數(shù)據(jù)的可能性就越小。

4.4、Join Operations

結(jié)構(gòu)化流支持將流數(shù)據(jù)集/數(shù)據(jù)幀與靜態(tài)數(shù)據(jù)集/數(shù)據(jù)幀以及另一個流數(shù)據(jù)集/數(shù)據(jù)幀連接起來。流連接的結(jié)果是遞增生成的,類似于上一節(jié)中的流聚合結(jié)果。在本節(jié)中,我們將探討在上述情況下支持的連接類型(即內(nèi)部、外部等)。請注意,在所有支持的聯(lián)接類型中,使用流數(shù)據(jù)集/數(shù)據(jù)幀進行聯(lián)接的結(jié)果將與使用流中包含相同數(shù)據(jù)的靜態(tài)數(shù)據(jù)集/數(shù)據(jù)幀時的結(jié)果完全相同。

  • Stream-static Joins
    自從Spark2.0引入以來,結(jié)構(gòu)化流支持流和靜態(tài)數(shù)據(jù)幀/數(shù)據(jù)集之間的連接(內(nèi)部連接和某些類型的外部連接)。下面是一個簡單的例子。
Dataset<Row> staticDf = spark.read(). ...;
Dataset<Row> 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

注意,流靜態(tài)連接不是有狀態(tài)的,因此不需要狀態(tài)管理。但是,還不支持幾種類型的流靜態(tài)外部聯(lián)接。這些類型在后面會有詳細的介紹。

  • Stream-stream Joins
    在Spark2.3中,我們增加了對流流連接的支持,也就是說,您可以連接兩個流數(shù)據(jù)集/數(shù)據(jù)幀。在兩個數(shù)據(jù)流之間生成連接結(jié)果的挑戰(zhàn)在于,在任何時間點,數(shù)據(jù)集的視圖對于連接的兩邊都是不完整的,這使得在輸入之間查找匹配更加困難。從一個輸入流接收到的任何行都可以與將來的任何行匹配,但仍將從另一個輸入流接收到該行。因此,對于這兩個輸入流,我們將過去的輸入緩沖為流狀態(tài),這樣我們可以將未來的每個輸入與過去的輸入匹配,并相應(yīng)地生成聯(lián)接的結(jié)果。此外,與流聚合類似,我們自動處理延遲的無序數(shù)據(jù),并可以使用水印限制狀態(tài)。讓我們討論支持的流連接的不同類型以及如何使用它們。
    Inner Joins with optional Watermarking
    支持任何類型的列上的內(nèi)部聯(lián)接以及任何類型的聯(lián)接條件。但是,當流運行時,流狀態(tài)的大小將無限期地增長,因為必須保存所有過去的輸入,因為任何新輸入都可以與過去的任何輸入匹配。為了避免無邊界狀態(tài),您必須定義額外的連接條件,以便使不確定的舊輸入不能與將來的輸入匹配,因此可以從狀態(tài)中清除。換言之,您將不得不在聯(lián)接中執(zhí)行以下附加步驟。
    1.在兩個輸入上定義水印延遲,以便引擎知道輸入的延遲程度(類似于流聚合)
    2.在兩個輸入之間定義一個事件時間約束,這樣引擎就可以計算出一個輸入的舊行何時不需要(即不滿足時間約束)與另一個輸入匹配。這個約束可以用兩種方法之一定義。
    1.時間范圍聯(lián)接條件(例如,在RightTime和RightTime之間的LeftTime上聯(lián)接+間隔1小時),
    2.在事件時間窗口上加入(例如…在LeftTimeWindow上加入=RightTimeWindow)。

讓我們用一個例子來理解這一點。

假設(shè)我們想將一個廣告印象流(顯示廣告時)與另一個用戶點擊廣告流連接起來,以便在印象導(dǎo)致可貨幣化點擊時進行關(guān)聯(lián)。要允許此流連接中的狀態(tài)清理,您必須指定水印延遲和時間約束,如下所示。
1.水印延遲:例如,事件時間中的印痕和相應(yīng)的點擊可能延遲/無序,分別最多2小時和3小時。
2.事件時間范圍條件:例如,在相應(yīng)的印象后0秒到1小時的時間范圍內(nèi)可能會發(fā)生一次單擊。

代碼應(yīng)該是這樣的。

import static org.apache.spark.sql.functions.expr

Dataset<Row> impressions = spark.readStream(). ...
Dataset<Row> clicks = spark.readStream(). ...

// Apply watermarks on event-time columns
Dataset<Row> impressionsWithWatermark = impressions.withWatermark("impressionTime", "2 hours");
Dataset<Row> clicksWithWatermark = clicks.withWatermark("clickTime", "3 hours");

// Join with event-time constraints
impressionsWithWatermark.join(
  clicksWithWatermark,
  expr(
    "clickAdId = impressionAdId AND " +
    "clickTime >= impressionTime AND " +
    "clickTime <= impressionTime + interval 1 hour ")
);

帶水印的流內(nèi)部連接的語義保證
這類似于在聚合上添加水印提供的保證。水印延遲“2小時”保證引擎不會丟棄任何延遲時間小于2小時的數(shù)據(jù)。但延遲超過2小時的數(shù)據(jù)可能會被處理,也可能不會被處理。

Outer Joins with Watermarking
對于內(nèi)部聯(lián)接,水印+事件時間約束是可選的,而對于左側(cè)和右側(cè)外部聯(lián)接,則必須指定它們。這是因為為了在外部聯(lián)接中生成空結(jié)果,引擎必須知道將來何時輸入行將與任何內(nèi)容不匹配。因此,必須指定水印+事件時間約束以生成正確的結(jié)果。因此,具有外部聯(lián)接的查詢看起來很像前面的廣告貨幣化示例,只是有一個額外的參數(shù)將其指定為外部聯(lián)接。

impressionsWithWatermark.join(
  clicksWithWatermark,
  expr(
    "clickAdId = impressionAdId AND " +
    "clickTime >= impressionTime AND " +
    "clickTime <= impressionTime + interval 1 hour "),
  "leftOuter"                 // can be "inner", "leftOuter", "rightOuter"
);

帶水印的流外部連接的語義保證
外部聯(lián)接與內(nèi)部聯(lián)接在水印延遲以及是否刪除數(shù)據(jù)方面具有相同的保證。
告誡
關(guān)于外部結(jié)果是如何產(chǎn)生的,有幾個重要的特征需要注意:
1.外部空結(jié)果將根據(jù)指定的水印延遲和時間范圍條件生成延遲。這是因為引擎必須等待那么長的時間,以確保沒有匹配,將來也不會有更多的匹配。

2.在微批量引擎的當前實現(xiàn)中,水印是在微批量結(jié)束時進行的,下一個微批量使用更新后的水印來清除狀態(tài)并輸出外部結(jié)果。由于我們只在需要處理新數(shù)據(jù)時觸發(fā)一個微批處理,因此如果流中沒有接收到新數(shù)據(jù),則外部結(jié)果的生成可能會延遲。簡而言之,如果被聯(lián)接的兩個輸入流中的任何一個在一段時間內(nèi)沒有接收數(shù)據(jù),則外部(兩種情況下,左或右)輸出可能會延遲。

流式查詢中聯(lián)接的支持列表

有關(guān)支持的聯(lián)接的其他詳細信息

  • 聯(lián)接可以級聯(lián),也就是說,您可以執(zhí)行df1.join(df2,…).join(df3,…).join(df4,…)。
  • 從spark 2.3開始,只有當查詢處于追加輸出模式時,才能使用聯(lián)接。還不支持其他輸出模式。
  • 從spark 2.3開始,在連接之前不能使用其他非映射類操作。以下是一些無法使用的示例。
    1。在聯(lián)接之前不能使用流聚合。
    2。在聯(lián)接之前,不能在更新模式下使用MapGroupsWithState和FlatmapGroupsWithState。
4.5、流式重復(fù)數(shù)據(jù)消除

您可以使用事件中的唯一標識符來消除數(shù)據(jù)流中的重復(fù)記錄。這與使用唯一標識符列的靜態(tài)重復(fù)數(shù)據(jù)消除完全相同。查詢將存儲以前記錄中所需的數(shù)據(jù)量,以便篩選重復(fù)記錄。與聚合類似,您可以使用帶或不帶水印的重復(fù)數(shù)據(jù)消除。

  • 使用水印-如果重復(fù)記錄到達的時間有上限,則可以在事件時間列上定義水印,并使用guid和事件時間列進行重復(fù)數(shù)據(jù)消除。查詢將使用水印從過去的記錄中刪除舊的狀態(tài)數(shù)據(jù),這些記錄不希望再得到任何重復(fù)數(shù)據(jù)。這限制了查詢必須維護的狀態(tài)量。
  • 沒有水印-由于重復(fù)記錄可能到達的時間沒有界限,查詢將所有過去記錄的數(shù)據(jù)存儲為狀態(tài)。
Dataset<Row> 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");
4.6、處理多個水印的策略

流查詢可以有多個聯(lián)合或連接在一起的輸入流。每個輸入流可以有一個不同的延遲數(shù)據(jù)閾值,對于有狀態(tài)的操作,這些閾值需要被容忍。在每個輸入流上使用withWatermarks("eventtime",delay)指定這些閾值。例如,考慮使用inputstream1和inputstream2之間的流連接進行查詢。

inputStream1.withWatermark(“eventTime1”, “1 hour”) .join( inputStream2.withWatermark(“eventTime2”, “2 hours”), joinCondition)

在執(zhí)行查詢時,結(jié)構(gòu)化流單獨跟蹤每個輸入流中看到的最大事件時間,根據(jù)相應(yīng)的延遲計算水印,并選擇一個帶有它們的全局水印用于狀態(tài)操作。默認情況下,選擇最小值作為全局水印,因為這樣可以確保如果其中一個流落后于另一個流(例如,其中一個流由于上游故障而停止接收數(shù)據(jù)),則不會意外地將任何數(shù)據(jù)拖得太晚。換句話說,全局水印將以最慢流的速度安全移動,查詢輸出將相應(yīng)延遲。
但是,在某些情況下,您可能希望獲得更快的結(jié)果,即使這意味著從最慢的流中刪除數(shù)據(jù)。由于Spark 2.4,可以通過將SQL配置spark.sql.streaming.multipleWatermarkPolicy to max (default is min),將多水印策略設(shè)置為選擇最大值作為全局水印。這使全局水印以最快的流速度移動。但是,作為一個副作用,來自較慢流的數(shù)據(jù)將被大量丟棄。因此,明智地使用這個配置。

4.7、任意狀態(tài)操作

許多用例需要比聚合更高級的有狀態(tài)操作。例如,在許多用例中,您必須從事件的數(shù)據(jù)流中跟蹤會話。為了進行這種會話化,必須將任意類型的數(shù)據(jù)保存為狀態(tài),并使用每個觸發(fā)器中的數(shù)據(jù)流事件對狀態(tài)執(zhí)行任意操作。由于spark 2.2,可以使用操作mapGroupsWithState 和更強大的操作flatMapGroupsWithState來完成此操作。這兩個操作都允許您對分組數(shù)據(jù)集應(yīng)用用戶定義的代碼以更新用戶定義的狀態(tài)。

4.8、不支持的操作

流式數(shù)據(jù)幀/數(shù)據(jù)集不支持一些數(shù)據(jù)幀/數(shù)據(jù)集操作。其中一些如下。

  • 流數(shù)據(jù)集尚不支持多個流聚合(即流數(shù)據(jù)集中的聚合鏈)。

  • 流數(shù)據(jù)集不支持限制行和取前n行。

  • 不支持對流數(shù)據(jù)集執(zhí)行不同的操作。

  • 只有在聚合之后并且處于完全輸出模式時,流數(shù)據(jù)集才支持排序操作。

  • 不支持流數(shù)據(jù)集上的幾種類型的外部聯(lián)接。有關(guān)更多詳細信息,請參閱“連接操作”部分中的支持矩陣。

此外,還有一些數(shù)據(jù)集方法不適用于流數(shù)據(jù)集。它們是將立即運行查詢并返回結(jié)果的操作,這對流數(shù)據(jù)集沒有意義。相反,這些功能可以通過顯式啟動流式查詢來完成(請參見下一節(jié))。

  • count()-無法從流數(shù)據(jù)集中返回單個計數(shù)。相反,使用ds.groupby().count()返回包含運行計數(shù)的流數(shù)據(jù)集。

  • foreach()-改為使用ds.writestream.foreach(…)(參見下一節(jié))。

  • show()-而是使用控制臺接收器(參見下一節(jié))。

如果您嘗試這些操作中的任何一個,您將看到類似“流式數(shù)據(jù)幀/數(shù)據(jù)集不支持操作xyz”的分析異常。雖然其中一些可能在未來的Spark版本中得到支持,但還有一些基本上難以有效地在流數(shù)據(jù)上實現(xiàn)。例如,不支持對輸入流進行排序,因為它需要跟蹤流中接收的所有數(shù)據(jù)。因此,從根本上說,這很難有效地執(zhí)行。

5、開始流式查詢

一旦定義了最終結(jié)果數(shù)據(jù)幀/數(shù)據(jù)集,剩下的就是開始流計算。要做到這一點,您必須使用DataStreamWriter ,通過 Dataset.writeStream(),您必須在此接口中指定以下一個或多個選項。

  • Details of the output sink:輸出接收器的詳細信息:數(shù)據(jù)格式、位置等。

  • Output mode: 輸出模式:指定寫入輸出接收器的內(nèi)容。

  • Query name:查詢名稱:可以選擇指定查詢的唯一名稱以進行標識。

  • Trigger interval: 觸發(fā)間隔:可以選擇指定觸發(fā)間隔。如果未指定,系統(tǒng)將在上一次處理完成后立即檢查新數(shù)據(jù)的可用性。如果由于前一個處理未完成而錯過觸發(fā)時間,則系統(tǒng)將立即觸發(fā)處理。

  • Checkpoint location:檢查點位置:對于一些可以保證端到端容錯的輸出接收器,指定系統(tǒng)寫入所有檢查點信息的位置。這應(yīng)該是HDFS兼容的容錯文件系統(tǒng)中的一個目錄。下一節(jié)將更詳細地討論檢查點的語義。

5.1、Output Modes輸出模式

有幾種類型的輸出模式。

  • Append mode (default) -附加模式(默認)-這是默認模式,其中只有自上一個觸發(fā)器以來添加到結(jié)果表的新行才會輸出到接收器。只有添加到結(jié)果表的行永遠不會更改的查詢才支持此功能。因此,此模式保證每行只輸出一次(假設(shè)容錯接收器)。例如,只有select、where、map、flatmap、filter、join等的查詢將支持追加模式。

  • Complete mode -完全模式-每次觸發(fā)后,整個結(jié)果表都將輸出到接收器。聚合查詢支持此功能。

  • Update mode - 更新模式-(從spark 2.1.1開始可用)只有結(jié)果表中自上次觸發(fā)器以來更新的行將輸出到接收器。更多信息將添加到將來的版本中。

不同類型的流式查詢支持不同的輸出模式。這是兼容性矩陣。


5.2、Output Sinks

有幾種類型的內(nèi)置輸出接收器。

  • File sink - Stores the output to a directory.
writeStream
    .format("parquet")        // can be "orc", "json", "csv", etc.
    .option("path", "path/to/destination/dir")
    .start()
  • Kafka sink - Stores the output to one or more topics in Kafka.
writeStream
    .format("kafka")
    .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
    .option("topic", "updates")
    .start()
  • foreach sink-對輸出中的記錄執(zhí)行任意計算。有關(guān)詳細信息,請參閱本節(jié)后面的內(nèi)容。
writeStream
    .foreach(...)
    .start()
  • Console sink (for debugging) -控制臺接收器(用于調(diào)試)-每次有觸發(fā)器時都將輸出打印到控制臺/stdout。支持附加和完整輸出模式。這應(yīng)該用于在低數(shù)據(jù)量上進行調(diào)試,因為在每次觸發(fā)之后,整個輸出都被收集并存儲在驅(qū)動程序的內(nèi)存中。
writeStream
    .format("console")
    .start()
  • Memory sink (for debugging)存表存儲在內(nèi)存中。支持附加和完整輸出模式。當整個輸出被收集并存儲在驅(qū)動程序內(nèi)存中時,這應(yīng)該用于在低數(shù)據(jù)量上進行調(diào)試。因此,謹慎使用。
writeStream
    .format("memory")
    .queryName("tableName")
    .start()

有些接收器不能容錯,因為它們不能保證輸出的持久性,并且僅用于調(diào)試目的。請參見前面關(guān)于容錯語義的部分。以下是spark中所有水槽sinks的細節(jié)。



注意,必須調(diào)用start()才能實際開始執(zhí)行查詢。這將返回一個streamingquery對象,該對象是連續(xù)運行執(zhí)行的句柄。您可以使用這個對象來管理查詢,我們將在下一小節(jié)中討論這個問題。現(xiàn)在,讓我們用幾個例子來理解這一切。

// ========== DF with no aggregations ==========
Dataset<Row> 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 ==========
Dataset<Row> 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和foreachbatch
foreach和foreachbatch操作允許您對流式查詢的輸出應(yīng)用任意操作和寫入邏輯。它們有稍微不同的用例——雖然foreach允許在每一行上自定義寫入邏輯,但是foreach batch允許在每個微批的輸出上執(zhí)行任意操作和自定義邏輯。讓我們更詳細地了解它們的用法。
ForeachBatch
foreachbatch(…)允許您指定對流式查詢的每個微批的輸出數(shù)據(jù)執(zhí)行的函數(shù)。自Spark 2.4以來,這在scala、Java和Python中得到了支持。它需要兩個參數(shù):一個數(shù)據(jù)幀或數(shù)據(jù)集,該數(shù)據(jù)幀或數(shù)據(jù)集具有微批的輸出數(shù)據(jù)和微批的唯一ID。

streamingDatasetOfString.writeStream().foreachBatch(
  new VoidFunction2<Dataset<String>, Long> {
    public void call(Dataset<String> dataset, Long batchId) {
      // Transform and write batchDF
    }    
  }
).start();

使用foreachbatch,可以執(zhí)行以下操作。

  • 重用現(xiàn)有的批處理數(shù)據(jù)源-對于許多存儲系統(tǒng),可能還沒有可用的流接收器,但可能已經(jīng)存在用于批處理查詢的數(shù)據(jù)編寫器。使用foreachbatch,可以在每個微批的輸出上使用批處理數(shù)據(jù)編寫器。
  • 寫入多個位置-如果要將流式查詢的輸出寫入多個位置,則只需多次寫入輸出數(shù)據(jù)幀/數(shù)據(jù)集。但是,每次嘗試寫入都會導(dǎo)致重新計算輸出數(shù)據(jù)(包括可能重新讀取輸入數(shù)據(jù))。為了避免重新計算,應(yīng)該緩存輸出數(shù)據(jù)幀/數(shù)據(jù)集,將其寫入多個位置,然后取消緩存。這是一個大綱。
streamingdf.writestream.forachbatch{(batchdf:dataframe,batchid:long)=>
batchdf.persist()
batchdf.write.format(…)/位置1 
batchdf.write.format(…)/位置2 
batchdf.unpersist()
}
  • 應(yīng)用其他數(shù)據(jù)幀操作-流式數(shù)據(jù)幀中不支持許多數(shù)據(jù)幀和數(shù)據(jù)集操作,因為Spark在這些情況下不支持生成增量計劃。使用foreachbatch,可以對每個微批處理輸出應(yīng)用其中一些操作。但是,您必須對自己執(zhí)行該操作的端到端語義進行推理。

注:

  • 默認情況下,foreachbatch至少提供一次寫入保證。但是,您可以使用提供給函數(shù)的batchID作為消除重復(fù)輸出并獲得一次性保證的方法。
  • foreachbatch不使用連續(xù)處理模式,因為它從根本上依賴于流式查詢的微批處理執(zhí)行。如果以連續(xù)模式寫入數(shù)據(jù),請改用foreach。

Foreach
如果foreach batch不是一個選項(例如,相應(yīng)的批數(shù)據(jù)編寫器不存在,或者不存在連續(xù)處理模式),則可以使用foreach表示自定義編寫器邏輯。具體來說,您可以將數(shù)據(jù)寫入邏輯劃分為三種方法:open, process, and close. 。自從Scale 2.4以來,F(xiàn)oreach在Scala、Java和Python中可用。
In Java, you have to extend the class ForeachWriter

streamingDatasetOfString.writeStream().foreach(
  new ForeachWriter[String] {

    @Override public boolean open(long partitionId, long version) {
      // Open connection
    }

    @Override public void process(String record) {
      // Write string to connection
    }

    @Override public void close(Throwable errorOrNull) {
      // Close the connection
    }
  }
).start();

執(zhí)行語義:當啟動流式查詢時,spark以以下方式調(diào)用函數(shù)或?qū)ο蟮姆椒ǎ?/p>

  • 此對象的單個副本負責查詢中單個任務(wù)生成的所有數(shù)據(jù)。換句話說,一個實例負責處理以分布式方式生成的數(shù)據(jù)的一個分區(qū)。

  • 此對象必須是可序列化的,因為每個任務(wù)都將獲得所提供對象的新的序列化反序列化副本。因此,強烈建議對寫入數(shù)據(jù)進行任何初始化(例如。打開連接或啟動事務(wù))是在調(diào)用open()方法之后完成的,這意味著任務(wù)已準備好生成數(shù)據(jù)。

  • 方法的生命周期如下:
    For each partition with partition_id:

    • For each batch/epoch of streaming data with epoch_id:
      • Method open(partitionId, epochId) is called.
      • If open(…) returns true, for each row in the partition and batch/epoch, method process(row) is called.
      • Method close(error) is called with error (if any) seen while processing rows.
  • 如果存在open()方法并成功返回(與返回值無關(guān)),則調(diào)用close()方法(如果存在),除非jvm或python進程在中間崩潰。

  • 注意:當失敗導(dǎo)致重新處理某些輸入數(shù)據(jù)時,open()方法中的partitionId and epochId可用于對生成的數(shù)據(jù)進行重復(fù)數(shù)據(jù)消除。這取決于查詢的執(zhí)行模式。如果流式查詢是在微批處理模式下執(zhí)行的,那么由一個唯一元組(partition_id, epoch_id)表示的每個分區(qū)都保證具有相同的數(shù)據(jù)。因此,(partition_id,epoch_id)可以用于消除重復(fù)和/或事務(wù)性提交數(shù)據(jù),并實現(xiàn)一次性保證。但是,如果流式查詢是在連續(xù)模式下執(zhí)行的,則此保證不適用,因此不應(yīng)用于重復(fù)數(shù)據(jù)消除。

5.3、觸發(fā)器

流式查詢的觸發(fā)器設(shè)置定義了流式數(shù)據(jù)處理的時間,無論該查詢是作為具有固定批處理間隔的微批處理查詢還是作為連續(xù)處理查詢執(zhí)行。以下是支持的不同類型的觸發(fā)器。



下面是一些實例

import org.apache.spark.sql.streaming.Trigger

// Default trigger (runs micro-batch as soon as it can)
df.writeStream
  .format("console")
  .start();

// ProcessingTime trigger with two-seconds micro-batch interval
df.writeStream
  .format("console")
  .trigger(Trigger.ProcessingTime("2 seconds"))
  .start();

// One-time trigger
df.writeStream
  .format("console")
  .trigger(Trigger.Once())
  .start();

// Continuous trigger with one-second checkpointing interval
df.writeStream
  .format("console")
  .trigger(Trigger.Continuous("1 second"))
  .start();

6、管理流式查詢

啟動查詢時創(chuàng)建的StreamingQuery可用于監(jiān)視和管理查詢。

StreamingQuery 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中啟動任意數(shù)量的查詢。它們都將同時運行,共享集群資源。您可以使用sparkSession.streams()來獲得用于管理當前活動查詢的StreamingQueryManager

SparkSession spark = ...

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

7、監(jiān)控流式查詢

有多種方法可以監(jiān)視活動的流式查詢。您可以使用Spark的Dropwizard度量支持將度量推送到外部系統(tǒng),也可以通過編程方式訪問它們。

7.1、交互讀取度量值

您可以使用streamingQuery.lastProgress() and streamingQuery.status().lastProgress()直接獲取活動查詢的當前狀態(tài)和度量值。lastprogress()返回StreamingQueryProgress in scala and java和一個在python中具有相同字段的字典。它包含有關(guān)流最后一個觸發(fā)器中的進度的所有信息—處理了哪些數(shù)據(jù)、處理速率、延遲等。還有streamingQuery.recentProgress,它返回最后幾個進度的數(shù)組。

另外,streamingQuery.status()返回StreamingQueryStatus inscala and java和一個在python中具有相同字段的字典。它提供有關(guān)查詢正在立即執(zhí)行的操作的信息—觸發(fā)器是否處于活動狀態(tài)、數(shù)據(jù)是否正在處理等。

下面是幾個例子。

StreamingQuery query = ...

System.out.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"
  }
}
*/


System.out.println(query.status());
/*  Will print something like the following.
{
  "message" : "Waiting for data to arrive",
  "isDataAvailable" : false,
  "isTriggerActive" : false
}
*/
7.2、使用異步API以編程方式報告度量

還可以通過附加streamingQueryListener(scala/java docs)異步監(jiān)視與sparkSession關(guān)聯(lián)的所有查詢。一旦使用SparkSession.streams.addListener()附加了customStreamingQueryListener對象,在啟動和停止查詢以及在活動查詢中取得進展時,您將得到回調(diào)。下面是一個例子,

SparkSession spark = ...

spark.streams().addListener(new StreamingQueryListener() {
    @Override
    public void onQueryStarted(QueryStartedEvent queryStarted) {
        System.out.println("Query started: " + queryStarted.id());
    }
    @Override
    public void onQueryTerminated(QueryTerminatedEvent queryTerminated) {
        System.out.println("Query terminated: " + queryTerminated.id());
    }
    @Override
    public void onQueryProgress(QueryProgressEvent queryProgress) {
        System.out.println("Query made progress: " + queryProgress.progress());
    }
});
7.3、使用DropWizard報告度量值

Spark支持使用DropWizard庫報告度量。要同時報告結(jié)構(gòu)化流式查詢的指標,必須在SparkSession中顯式啟用configurationsPark.sql.streaming.metricsEnabled。

spark.conf().set("spark.sql.streaming.metricsEnabled", "true");
// or
spark.sql("SET spark.sql.streaming.metricsEnabled=true");

啟用此配置后在SparkSession中啟動的所有查詢都將通過DropWizard向配置的Versinkshave報告度量(例如Ganglia、Graphite、JMX等)。

8、使用檢查點從失敗中恢復(fù)

如果出現(xiàn)故障或有意關(guān)閉,您可以恢復(fù)以前查詢的進度和狀態(tài),并在停止的地方繼續(xù)。這是使用檢查點和提前寫入日志完成的。您可以使用檢查點位置配置查詢,查詢將把所有進度信息(即每個觸發(fā)器中處理的偏移范圍)和正在運行的聚合(如快速示例中的字數(shù))保存到檢查點位置。此檢查點位置必須是HDFS兼容文件系統(tǒng)中的路徑,并且可以在啟動查詢時在DatastreamWriter中設(shè)置為選項。

aggDF
  .writeStream()
  .outputMode("complete")
  .option("checkpointLocation", "path/to/HDFS/dir")
  .format("memory")
  .start();

9、流式查詢更改后的恢復(fù)語義

在從同一檢查點位置重新啟動之間,流式查詢中允許哪些更改存在限制。以下是一些不允許的更改,或者更改的效果沒有很好的定義。對于所有人:

  • termallowed意味著您可以進行指定的更改,但其效果的語義是否定義良好取決于查詢和更改。

  • termNot allowed意味著您不應(yīng)執(zhí)行指定的更改,因為重新啟動的查詢可能會因不可預(yù)知的錯誤而失敗。sdfre顯示了使用sparksession.readstream生成的流式數(shù)據(jù)幀/數(shù)據(jù)集。

變更的類型

  • 輸入源的編號或類型(即不同的源)發(fā)生更改:這是不允許的。

  • 輸入源參數(shù)的更改:是否允許,更改的語義是否定義良好,取決于源和查詢。下面是幾個例子。

    • 允許添加/刪除/修改速率限制:spark.readstream.format(“kafka”).option(“subscribe”,“topic”)tospark.readstream.format(“kafka”).option(“subscribe”,“topic”).option(“maxoffsetspertrigger”,…)

    • 通常不允許更改訂閱的主題/文件,因為結(jié)果是不可預(yù)測的:spark.readstream.format(“kafka”).option(“subscribe”,“topic”)tosbark.readstream.format(“kafka”).option(“subscribe”,“newtopic”)。

  • 輸出接收器類型的更改:允許在幾個特定接收器組合之間進行更改。這需要逐個驗證。下面是幾個例子。

    • 允許文件接收器到Kafka接收器。卡夫卡只能看到新的數(shù)據(jù)。

    • 不允許Kafka接收器到文件接收器。

    • 卡夫卡水槽改為foreach,反之亦然。

  • 輸出接收器參數(shù)的更改:是否允許,更改的語義是否定義良好,取決于接收器和查詢。下面是幾個例子。

    • 不允許更改文件接收器的輸出目錄:sdf.writestream.format(“parquet”).option(“path”,“/somepath”)to sdf.writestream.format(“parquet”).option(“path”,“/anotherpath”)。

    • 允許更改輸出主題:sdf.writestream.format(“kafka”).option(“topic”,“sometopic”)to sdf.writestream.format(“kafka”).option(“topic”,“anothertopic”)。

    • 允許對用戶定義的foreach接收器(即foreachwritercode)進行更改,但更改的語義取決于代碼。

  • *投影/過濾/類似地圖的操作中的更改:某些情況下是允許的。例如:

    • 允許添加/刪除篩選器:sdf.selectexpr(“a”)to sdf.where(…).selectexpr(“a”).filter(…)。

    • 允許更改具有相同輸出架構(gòu)的投影:sdf.selectexpr(“stringcolumn as json”).writestreamtosdf.selectexpr(“anotherStringcolumn as json”).writestream

    • 有條件地允許使用不同輸出架構(gòu)的投影中的更改:sdf.selectexpr(“a”).writestreamtosdf.selectexpr(“b”)。只有在輸出接收器允許架構(gòu)從“a”更改為“b”時,才允許使用writestream。

  • 狀態(tài)操作中的更改:流式查詢中的某些操作需要維護狀態(tài)數(shù)據(jù),以便持續(xù)更新結(jié)果。結(jié)構(gòu)化流自動檢查狀態(tài)數(shù)據(jù)到容錯存儲(例如,HDFS、AWS S3、Azure Blob存儲)并在重新啟動后將其還原。但是,這假定狀態(tài)數(shù)據(jù)的架構(gòu)在重新啟動時保持不變。這意味著在重新啟動之間不允許對流式查詢的有狀態(tài)操作進行任何更改(即添加、刪除或架構(gòu)修改)。以下是在重新啟動之間不應(yīng)更改其架構(gòu)的有狀態(tài)操作列表,以確保狀態(tài)恢復(fù):

    • 流聚合:例如,sdf.groupby(“a”).agg(…)。不允許對分組鍵或聚合的數(shù)量或類型進行任何更改。

    • 流式重復(fù)數(shù)據(jù)消除:例如,sdf.dropduplicates(“A”)。不允許對分組鍵或聚合的數(shù)量或類型進行任何更改。

    • 流流連接:例如,sdf1.join(sdf2,…)(即,兩個輸入都是用sparksession.readstream生成的)。不允許在架構(gòu)或同等聯(lián)接列中進行更改。不允許更改聯(lián)接類型(外部或內(nèi)部)。連接條件中的其他更改定義錯誤。

    • 任意狀態(tài)操作:例如,sdf.groupbykey(…).mapgroupswithstate(…)orsdf.groupbykey(…).flatmapgroupswithstate(…)。不允許更改用戶定義狀態(tài)的架構(gòu)和超時類型。允許在用戶定義狀態(tài)映射函數(shù)內(nèi)進行任何更改,但更改的語義效果取決于用戶定義的邏輯。如果您真的想支持狀態(tài)架構(gòu)更改,那么您可以使用支持架構(gòu)遷移的編碼/解碼方案將復(fù)雜的狀態(tài)數(shù)據(jù)結(jié)構(gòu)顯式編碼/解碼為字節(jié)。例如,如果將狀態(tài)保存為avro編碼的字節(jié),則可以在查詢重新啟動之間自由更改avro狀態(tài)模式,因為二進制狀態(tài)將始終成功還原。

10、連續(xù)的工作

[實驗]
連續(xù)處理是Spark 2.3中引入的一種新的、實驗性的流式執(zhí)行模式,它允許低(~1 ms)端到端延遲,并至少保證一次容錯。將其與默認的微批量處理引擎進行比較,后者可以實現(xiàn)一次完全保證,但最多只能實現(xiàn)約100毫秒的延遲。對于某些類型的查詢(在下面討論),您可以選擇在不修改應(yīng)用程序邏輯的情況下執(zhí)行它們的模式(即,不更改數(shù)據(jù)幀/數(shù)據(jù)集操作)。
要在連續(xù)處理模式下運行受支持的查詢,只需指定一個具有所需檢查點間隔的連續(xù)觸發(fā)器作為參數(shù)。例如,

import org.apache.spark.sql.streaming.Trigger;

spark
  .readStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
  .option("subscribe", "topic1")
  .load()
  .selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
  .writeStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
  .option("topic", "topic1")
  .trigger(Trigger.Continuous("1 second"))  // only change in query
  .start();

檢查點間隔為1秒意味著連續(xù)處理引擎將每秒記錄查詢的進度。生成的檢查點的格式與微批處理引擎兼容,因此任何查詢都可以用任何觸發(fā)器重新啟動。例如,支持的以微批處理模式啟動的查詢可以在連續(xù)模式下重新啟動,反之亦然。請注意,任何時候切換到連續(xù)模式時,至少會得到一次容錯保證。

10.1、支持的查詢

從spark 2.3開始,在連續(xù)處理模式中只支持以下類型的查詢。

  • Operations操作:在連續(xù)模式下只支持類似地圖的數(shù)據(jù)集/數(shù)據(jù)幀操作, only projections (select,map,flatMap,mapPartitions, etc.) and selections (where,filter, etc.)。
    • 除了聚合函數(shù)(因為還不支持聚合)、current_timestamp() and current_date()之外,所有SQL函數(shù)都受支持(使用時間的確定性計算具有挑戰(zhàn)性)。
  • Sources:
    kafka來源:支持所有選項。
    Rate 來源:適用于測試。只有在連續(xù)模式下支持的選項才是numPartitionsAndRowsPerSecond。
  • Sinks:
    kafka水槽:支持所有選項。
    內(nèi)存接收器:用于調(diào)試。
    控制臺接收器:便于調(diào)試。支持所有選項。注意,控制臺將打印在連續(xù)觸發(fā)器中指定的每個檢查點間隔。

有關(guān)詳細信息,請參閱輸入源和輸出下沉部分。盡管控制臺接收器適合測試,但可以最好地觀察到以Kafka為源和接收器的端到端低延遲處理,因為這允許引擎在輸入主題中輸入數(shù)據(jù)可用的毫秒內(nèi)處理數(shù)據(jù)并使結(jié)果在輸出主題中可用。

10.2、告誡
  • 連續(xù)處理引擎啟動多個長時間運行的任務(wù),這些任務(wù)不斷地從源讀取數(shù)據(jù)、處理數(shù)據(jù)并不斷地向接收器寫入數(shù)據(jù)。查詢所需的任務(wù)數(shù)取決于查詢可以并行從源讀取的分區(qū)數(shù)。因此,在開始連續(xù)處理查詢之前,必須確保集群中有足夠的核心來并行執(zhí)行所有任務(wù)。例如,如果您正在讀取具有10個分區(qū)的Kafka主題,那么集群必須至少有10個核心才能使查詢?nèi)〉眠M展。
  • 停止連續(xù)處理流可能會產(chǎn)生虛假的任務(wù)終止警告。這些可以被安全地忽略。
  • 當前沒有失敗任務(wù)的自動重試。任何失敗都將導(dǎo)致查詢停止,需要從檢查點手動重新啟動查詢。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容