Spark每日半小時(30)——結構化流式編程:Dataset/DataFrame API1:基本操作

從Spark 2.0開始,DataFrames和Dataset可以表示靜態的,有界的數據,以及流式無界數據。與靜態Dataset/DataFrame類似,我們可以使用公共入口點SparkSession從流源創建流式Dataset/DataFrame,並對它們應用與靜態Dataset/DataFrame相同的操作。如果我們不熟悉Dataset/DataFrame,可以看之前Spark SQL內容熟悉它們。

創建流式DataFrame和流式Dataset

可以通過SparkSession.readStream()方法返回DataStreamReader接口來創建Streaming DataFrame。與用於創建靜態DataFrame的讀取接口類似,我們可以指定源的詳細信息:數據格式,架構,選項等。

輸入源

有一些內置源:

  • 文件來源:將目錄中寫入的文件作爲數據流讀取。支持的文件格式爲text,csv,json,orc,parquet。有關更新的列表,請參閱DataStreamReader接口文檔,以及每種文件格式支持的選項。請注意,文件必須以原子形式放在指定目錄中,在大多數文件系統中,可以通過文件移動操作來實現。
  • Kafka來源:從kafka讀取數據。它與kafka 0.10.0或更高版本兼容。
  • 套接字源(用於測試):從套接字(socket)連接讀取UTF-8文本數據。偵聽服務器中位於驅動程序的套接字。注意,此種輸入源應僅用於測試,因爲這不提供端到端的容錯保證。
  • 速率源(用於測試):以每秒指定的行數生成數據,每個輸出行包含一個timestamp和value。其中timestamp是一個Timestamp含有信息分配的時間類型,並且value是包含消息計數的Long型參數,從0開始作爲第一行。此輸入源用於測試和基準測試。

某些源不具有容錯能力,因爲它們無法保證在發生故障後可以使用檢查點(checkpoint這可是個好東西,我們專門用其作爲我們斷線續讀的功能實現。)偏移重放數據。

數據源 選項 容錯 節點

File Source

path:輸入目錄的路徑,並且對所有文件格式都是通用的。 
maxFilesPerTrigger:每個觸發器中要考慮的最大新文件數(默認值:無最大值) 
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“ 
Yes 支持glob路徑,但不支持多個以逗號分隔的路徑/globs。
Socket Source host:要連接的主機,必須指定
port:要連接的端口,必須指定
No  
Rate Source

rowsPerSecond(例如100,默認值:1):每秒應生成多少行。

rampUpTime(例如5s,默認值:0s):在生成速度變爲之前加速多長時間

rowsPerSecond。使用比秒更精細的粒度將被截斷爲整數秒。

numPartitions(例如10,默認值:Spark的默認並行性):生成的行的分區號。

源代碼將盡力達到目標rowsPerSecond,但查詢可能會受到資源限制,並且numPartitions可以進行調整以幫助達到所需的速度。

Yes  
Kafka Source 請參閱Kafka集成指南 Yes  
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的架構,僅在提交查詢時在運行時檢查。某些操作(如map,flatMap等)需要在編譯時知道類型。要執行這些操作,我們可以使用與靜態DataFrame相同的方法將這些無類型流式DataFrame轉換爲類型化流式數據集。

流式DataFrames/Datasets的模式推理和分區

默認情況下,基於文件的源和結構化流需要我們指定架構,而不是依靠Spark自動推斷它。此限制可確保即使再出現故障的情況下,也將使用一致的架構進行流式查詢。對於臨時用例,我們可以通過設置spark.sql.streaming.schemaInference爲重新啓用架構推斷true。

當命名的子目錄/key=value/存在且列表將自動遞歸到這些目錄中時,確實回發生分區發現。如果這些列出現在用戶提供的模式中,則Spark將根據正在讀取的文件的路徑填充它們。構成分區方案的目錄必須在查詢開始時存在,並且必須保持靜態。例如,當/data/year=2015已經存在添加/data/year/2016也是可以的,但更改分區列(及通過創建目錄/data/data=2016-04-17/)無效。

流式傳輸DataFrames/Datasets的操作

我們可以將DataFrames/Datasets從無類型轉換爲流操作,類似於Spark SQL中的操作(select,where,groupBy),爲鍵入RDD般的操作(例如map,filter,flatMap)。

基本操作:選擇,映射,聚合

DataFrame/Dataset上的大多數常見操作都支持流式傳輸。

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()));

我們還可以將流式DataFrame/Dataset註冊爲臨時視圖,然後在其上應用SQL命令。

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

注意,我們可以使用df.isStreaming()方法確定DataFrame/Dataset是否具有流數據

df.isStreaming();

事件時間的窗口操作

使用結構化流式傳輸時,滑動事件時間窗口上的聚合非常簡單,並且與分組聚合非常相似。在分組聚合中,爲用戶指定的分組列中的每個唯一值維護聚合值(例如,計數)。在基於窗口的聚合的情況下,爲每個窗口維護一行的時間時間的聚合值。讓我們通過一個例子來理解這一點。

想象一下,我們的示例已被修改,流現在包含行以及生成行的時間。我們不想獲取字數,而是計算10分鐘內的單詞,每5分鐘更新一次。也就是說,在10分鐘窗口12:00-12:10,12:05-12:15,12:10-12:20等之間收到的單詞數量。注意,12:00-12:10表示數據在12:00之後但在12:10之前到達。現在,考慮一下在12:07收到的一個字。這個字應該增加對應於兩個窗口12:00-12:10和12:05-12:15的計數。因此,計數將由分組鍵(即單詞)和窗口(以可從事件事件計算)索引。

結果表看起來如下所示。

çªå£æä½

由於此窗口類似於分組,因此在代碼中,我們可以使用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();

處理延遲數據和水印

現在考慮如果其中一個事件到達應用程序的後期會發生什麼。例如,應用程序在12:11可以接收在12:04(即事件時間)生成的單詞。應用程序應用使用時間12:04而不是12:11來更新窗口的舊計數12:00-12:10.這在我們基於窗口的分組中自然發生:結構化可以長時間維持部分聚合的中間狀態,以便於後期數據可以正確更新舊窗口的聚合,如下所示。

å¤çåææ°æ®

但是,要運行此查詢數天,系統必須限制它累積的中間內存中狀態的數量。這意味着系統需要知道何時可以從內存狀態中刪除舊聚合,因爲應用程序不再接收該聚合的後期數據。爲了實現這一點,在Spark2.1中引入了水印,使引擎能夠自動跟蹤數據中的當前事件事件並嘗試相應地清理舊狀態。我們可以通過指定事件時間列以及根據事件時間預計數據地延遲時間來定義查詢的水印。對於從時間開始的特定窗口T,引擎將保持狀態並允許延遲數據更新狀態直到(max event time seen by the engine - late threshold > T)。換句話說,閾值內的後期數據將被聚合,但是晚於閾值的數據將開始被丟棄。讓我們通過一個例子來理解這一點。我們可以使用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分鐘”定義爲允許數據延遲的閾值。如果此查詢在更新輸出模式下運行,則引擎將繼續更新結果表中窗口的計數,直到窗口早於水印,該水印落後於列中的當前事件事件“時間戳”10分鐘。

æ´æ°æ¨¡å¼ä¸çæ°´å°

如圖所示,藍色虛線是引擎跟蹤的最大事件時間,並且在(max event time - ‘10 mins’)每個觸發器開始時設置的水印是紅線。例如,當引擎觀察數據時(12:14,dog),它設置水印爲下一個觸發器爲12:04.該水印使發動機保持中間狀態另外10分鐘,以允許計算延遲數據。例如,數據(12:09,cat)是亂序的,而且很晚,它落在窗口12:00-12:10和12:05-12:15。由於12:04仍然位於觸發器中的水印之前,因此引擎仍將中間計數保持爲狀態並正確更新相關窗口的計數。但是,當水印更新爲12:11,窗口的中間狀態(12:00-12:10)被清除,所有後續數據(例如12:04,donkey)被認爲“太晚”,因此被忽略。請注意,在每次觸發後,更新的計數(即紫色行)將作爲觸發輸出寫入sink,如更新模式所示。

某些接收器(例如文件)可能不支持更新模式所需的細粒度更新。爲了使用它們,Spark還支持附加模式,其中只有最終計數被寫入接收器。

注意,withWatermark在非流式數據集上使用是no-op。由於水印不應以任何方式影響任何批量查詢,Spark將直接忽略它。

éå æ¨¡å¼ä¸­çæ°´å°

與之前的更新模式類似,引擎維護每個窗口的中間計數。但是,部分計數不會更新到結果表,也不會寫入接收器。引擎等待“10分鐘”以計算延遲日期,然後丟棄窗口水印的中間狀態,並將最終計數附加到結果表/接收器。例如,12:00 - 12:10只有在水印更新到之後,窗口的最終計數纔會附加到結果表中12:11

用於清除聚合狀態的水印的條件

值得注意的是,在聚合查詢中,水印清除狀態必須滿足以下條件。

  • 輸出模式必須爲Append或Update。完整模式要求保留所有聚合數據,因此不能使用水印來降低中間狀態。
  • 聚合必須具有事件時間或事件時間列上的一個window。
  • withWatermark必須在與聚合中使用的時間戳列相同的列上調用。例如, df.withWatermark("time", "1 min").groupBy("time2").count()在追加輸出模式下無效,因爲水印是在與聚合列不同的列上定義的。
  • withWatermark必須在聚合之前調用要使用的水印細節。例如,df.groupBy("time").count().withWatermark("time", "1 min")在追加輸出模式下無效。

帶水印聚合的語義保證

  • 水印延遲(設置爲withWatermark)爲“2小時”可確保引擎永遠不會丟棄延遲小於2小時的任何數據。換句話說,任何不到2小時(在事件時間方面)的數據都保證彙總到那時處理的最新數據。
  • 但是,保證只在一個方向嚴格。延遲2小時以上的數據不能保證被丟棄;它可能會也可能不會聚合。更多延遲的數據,引擎進行處理的可能性較小。

接下來會講解Join()的主要用法。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章