Spark Structured Streaming特性詳解

本文所有內容是基於spark 2.4.3版本官方文檔

Structured Streaming provides fast, scalable, fault-tolerant, end-to-end exactly-once stream processing without the user having to reason about streaming


Structured Streaming 是Spark流處理引擎在2.*版本後加入的模塊, 是基於微批(Micro-Batch)的流處理,其最低延時至少100ms。在2.3引入了Continuous Processing實驗性流處理模式,可達到端到端最低1毫秒的延時。

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

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

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

// prevent the process from exiting while the query is active.
query.awaitTermination()

一、基本概念

  1. Streaming數據流轉
           輸入的數據流可以看作一個輸入表,到達的每一條數據都可以當作在輸入表中新增一行數據。對輸入表的查詢會產生一個結果表,每一個trigger interval,輸入表中新增的數據都會更新結果表,同時會將改變的結果行寫到外部存儲(external sink),在這個過程中,會保存需要用於更新結果表最少的中間狀態數據。
  2. 處理Event-time和延遲數據
           Event-time時嵌入在數據內的時間,對於多數應用,需要處理的時數據產生的時間而非spark接收數據的時間。event-time可以看作數據行中的一列,這樣可以對event-time列進行基於窗口的分組聚合操作 -- 每一個時間窗口都是一個分組,每個數據行都屬於多個窗口/分組,這樣基於窗口事件事件聚合查詢可以在靜態數據集和數據流上具有一致的定義。
           當出現延遲數據時,Spark擁有完全的權控制對先前聚合結果的進行更新,也能清除過期聚合結果來限制中間狀態數據的量。spark2.1之後,支持使用wartermarking來制定數據延遲的閾值,處理引擎也可以據此清除過期中間狀態。
  3. 容錯語義
           實現端到端exactly-once語義時Structured Streaming設計的重要目標,輸入源、輸出端和執行引擎在設計上都能可靠地追蹤流處理的進程,進而通過重啓或重新處理解決各種失敗情況

二、主要特性

  1. streaming DataFrames/Datasets的創建
           Spark Structured Streaming內置的數據源有:File source、Kafka source、Socket source、Rate source,其中Socket 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")
           示例中的Streaming DataFrames是非強類型的,意味着不會再編譯時檢查DataFrame的schema,只會在查詢提交後運行時做檢查。類似map,flatMap等需要在編譯時確認類型的操作,需要將Streaming DataFrames轉換成強類型的Streaming Datasets後再操作。
     
  2. streaming DataFrames/Datasets的schema推斷和分區發現
           基於File source的Structured Streaming默認不會自行推斷schema,而是需要用戶明確提供。該限制用來保證即使再出現故障的情況下也能保證查詢使用schema的一致性。在有ad-hoc需要時,可以通過設置spark.sql.streaming.schemaInference=true開啓schema推斷
            分區發現是指如果schema中存在key字段,當出現/key=value/子目錄時,可以自動地將目錄放到分區列表中。組成分區schema的目錄在開始查詢時必須存在並且保持靜態(1.key存在 2.key不可變 3.value可變)
     
  3. 基本操作-選取、映射、聚合
    //定義schema
    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
    
    //create temp view
    df.createOrReplaceTempView("updates")
    spark.sql("select count(*) from updates")  // returns another streaming DF
           streaming Dataframes可以使用distinct()和dropDuplicates(colNames)進行去重,區別在於distinct根據每一條數據進行完整內容的比對和去重,而dropDuplicates可以根據指定的字段進行去重。
           streaming Datasets支持大多數通用DataSets操作,其不支持的操作有:
           · streaming Datasets不支持多留聚合
           · streaming Datasets不支持Limit和take(n)操作
           · streaming Datasets不支持Distinct操作
           · streaming Datasets不支持排序,排序必須跟在聚合操作後並使用complete輸出模式
           · streaming Datasets少數幾種outer join
           · streaming Datasets不能直接count(),用ds.groupBy().count()可以返回一個包含運行時計數的streaming Datasets
           · streaming Datasets不支持foreach(),需用ds.writeStream.foreach(...) 代替
           · streaming Datasets不支持show(), 需用console sink代替
     
  4. 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()

    WarterMarking
           數據流是基於時間的窗口操作,但每個窗口的數據不一定會及時的在窗口時間內到來,因此需要窗口數據作爲中間狀態,當屬於該窗口的數據到來時更新狀態。但系統能保存的中間狀態時有限的,不可能無限地等待數據,因此spark2.1後引入了WarterMarking讓系統可以知道在什麼時候可以觸發窗口計算並丟棄窗口的中間狀態。Watermark是一種平衡處理延時和完整性的靈活機制。
           在系統每次觸發數據落地(trigger)時,系統會基於( WaterMark= 當前窗口最大可見event time  - 允許延遲時間)作爲當前窗口可處理數據event_time最小的時間,早於這個時間的窗口狀態會被認爲過期並清除,早於這個時間的數據可能會也可能不會被丟棄
           WarterMark觸發Window計算有2個條件:(a).watermark時間>=window最大可見event time (b).窗口內有數據

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

     

  5. Structured Streaming的Join操作
           Structured Streaming支持streaming DataSets/DataFrames 連接 static Structured Streaming或者streaming DataSets/DataFrames。
           stream-static 連接是無狀態的,因此不需要管理狀態。stream-stream 連接的主要挑戰是Stream是不完整的數據集,很難匹配join的輸入,每個流輸入的一行數據都可能要匹配另一個流還沒出現的數據。因此必須爲把所有流過去的數據緩存爲流狀態,這樣就可以把過去的輸入和未來的輸入相應地匹配起來生成連接結果。同樣地,使用wartermarking來處理延遲、亂序的數據以及限制中間狀態的量。在join操作中需要有下面的設定
           1.定義兩個輸入流控制延遲的WaterMark,告知引擎每個流數據延遲處理的範圍
           2.定義對兩個輸入流事件時間的限制,這樣引擎可以知道兩個流新老數據連接的最大時間差

    import org.apache.spark.sql.functions.expr
    
    val impressions = spark.readStream. ...
    val clicks = spark.readStream. ...
    
    // Apply watermarks on event-time columns
    val impressionsWithWatermark = impressions.withWatermark("impressionTime", "2 hours")
    val 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
        """)
    )

     

  6. Trigger操作
    trigger操作用於設置流數據生成微批和處理微批的時間間隔,當前支持的不同類型trigger如下:
    1.不指定:如果沒有顯式指定,也就是採用默認的微批處理模式,即只要前一個微批處理結束就會立即處理下一個微批(在處理期間積累的數據)。
    2.固定時間間隔:按固定時間間隔生成微批。如果前一個微批在間隔內完成,那下一個微批要等到間隔結束才生成並處理;如果前一個微批超過間隔完成,那麼下一個微批會在前一個結束後立即生成並處理;如果沒有可用的下一個微批的時不做任何處理。
    3.一次性:所有的數據當作一個微批一次性處理。此類型適用於週期性的啓停一個作業進行處理的場景,從數據層面類似定時執行的etl作業,其優勢在於自行管理每次執行的數據,而etl需要用戶指定;執行作業具有原子性,而etl一旦失敗需要清理已寫入數據;通過合理配置watermark可以通過dropDuplicates實現跨多個執行作業去重,etl無法實現;如果接受更高的延時,週期按小時或按天作業相比全天運行的流作業節省更多資源和成本。
    4.連續性(Continuous): 持續處理,更低延時。屬於實驗性功能,不多做介紹

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

     

  7. 實現任意的狀態操作
           前面提到的狀態主要是指對流進行分組聚合產生的狀態,很多情況下我們可能需要保存更復雜的狀態,比如我們可能希望在數據流中保存會話狀態。Spark2.2之後,可通過使用mapGroupsWithState或者更強大的flatMapGroupsWithState操作來實現。該操作可以在每次trigger時,將自定義函數作用在每個分組上(groupByKey)來生成、更新、清除任意自定義的狀態(只會作用在當前trigger中出現的分組)。想進一步瞭解可參考GroupState(mapGroupsWithState/flatMapGroupsWithState) 

  8. 輸出端和輸出模式
    有以下幾種內置的輸出端:File sink, kafka sink, Foreach sink, Console sink(調試用), Memory sink(調試用)

    //file sink
    writeStream
        .format("parquet")        // can be "orc", "json", "csv", etc.
        .option("path", "path/to/destination/dir")
        .start()
    
    //kafka sink
    writeStream
        .format("kafka")
        .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
        .option("topic", "updates")
        .start()
    
    //foreach sink
    writeStream
        .foreach(...)
        .start()
    
    //write sink
    writeStream
        .format("console")
        .start()
    
    //memory sink
    writeStream
        .format("memory")
        .queryName("tableName")
        .start()


    到輸出端的輸出模式有一下三種模式:
    Complet Mode - 僅支持aggregation查詢,每次trigger都會將更新後全量的結果表寫入外部存儲
    Append Mode  - 默認模式,根據watermark延遲輸出結果表新增的行數據到外部存儲,並清理過期狀態
    Update Mode   - 每次trigger都會將結果表中更新的行會寫入外部存儲(2.1.1版本後可用)

  9. 基於檢查點的故障恢復
           
    爲了防止系統故障或者意外關閉,spark使用checkpoint和WAL機制恢復故障前的查詢狀態繼續運行,實現exactly-once語義。從checkpoint進行故障恢復時,下面的變更時不允許的:
    1.變更輸入源的數量或類型
    2.變更輸入源訂閱的topics/files
    3.變更輸出端的文件目錄或topic
    4.變更映射輸出結果的schema是否允許要看輸出端是否允許這種變更
           checkpoint持久化的時候會保存Scala/Java/Python對象(如果有)序列化後的數據,如果應用升級變更了對象數據結構,從checkpoint中恢復狀態數據可能會導致錯誤。這種情況在重啓應用時要麼刪除先前的checkpoint目錄,要麼更改目錄

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

     

  10. 監控
    待補充

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