Spark Streaming Programming Guide

Table of Contents

簡述

簡易樣例

基本概念

依賴

StreamingContext初始化

Discretized Streams (DStreams)

輸入數據流和接收器

基礎Sources

高級Sources

自定義Sources

Receiver 可靠性

DStreams上的算子

UpdateStateByKey Operation

Transform Operation

Window Operations

Join Operations

DStreams上的輸出操作

使用foreachRDD的設計模式

DataFrame和SQL操作

MLlib Operations

Caching / Persistence

Checkpointing

When to enable Checkpointing

How to configure Checkpointing

Accumulators, Broadcast Variables, and Checkpoints

Deploying Applications

Requirements

Upgrading Application Code

Monitoring Applications

Performance Tuning

Reducing the Batch Processing Times

Level of Parallelism in Data Receiving

Level of Parallelism in Data Processing

Data Serialization

Task Launching Overheads

Setting the Right Batch Interval

Memory Tuning

Fault-tolerance Semantics

Background

容錯Definitions

Basic Semantics

Semantics of Received Data

With Files

With Receiver-based Sources

With Kafka Direct API

Semantics of output operations


簡述

Spark Streaming 是核心 Spark API 的擴展,支持可伸縮、高吞吐量、容錯的實時數據流處理。數據可以從許多來源獲取,如Kafka、Flume、Kinesis 或 TCP sockets,可以使用複雜的算法處理數據,這些算法用高級函數表示,如 map、reduce、join和window。最後,處理後的數據可以推送到文件系統、數據庫和活動儀表板。實際上,您可以將 Spark 的機器學習和圖形處理算法應用於數據流。

在內部,它是這樣工作的。Spark Streaming 接受實時輸入數據流,並將數據分成批次,然後由 Spark engine 處理,以批量生成最終的結果流。

 

Spark Streaming 提供了一種高級抽象,稱爲離散流或 DStream,它表示連續的數據流。DStreams 可以從 Kafka、Flume和Kinesis 等源的輸入數據流創建,也可以通過在其他 DStreams 上應用高級操作創建。在內部,DStream 表示爲 RDDs 序列。

本指南向您展示瞭如何使用 DStreams 編寫 Spark 流程序。您可以用 Scala、Java或 Python(在Spark 1.2中引入)編寫 Spark 流程序,所有這些都在本指南中介紹。

注意:有一些api是不同的,或者在Python中不可用的。

簡易樣例

在詳細介紹如何編寫自己的 Spark Streaming 程序之前,讓我們先快速瞭解一下簡單的 Spark 流程序是什麼樣子的。假設我們想要計算從監聽 TCP 套接字的數據服務器接收到的文本數據中的單詞數。你所需要做的就是如下所示。

首先,我們將 Spark 流類的名稱和一些從 StreamingContext 的隱式轉換導入到我們的環境中,以便向我們需要的其他類(如DStream)添加有用的方法。StreamingContext 是所有流功能的主要入口點。我們使用兩個執行線程創建一個本地StreamingContext,批處理間隔爲1秒。

import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3

// Create a local StreamingContext with two working thread and batch interval of 1 second.
// The master requires 2 cores to prevent from a starvation scenario.

val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))

使用這個上下文,我們可以創建一個表示來自 TCP 源的流數據的 DStream,指定爲主機名(例如localhost)和端口(例如9999)。

// Create a DStream that will connect to hostname:port, like localhost:9999
val lines = ssc.socketTextStream("localhost", 9999)

這行 DStream 表示將從數據服務器接收的數據流。DStream 中的每個記錄都是一行文本。接下來,我們希望按空格字符將行分割爲單詞。

// Split each line into words
val words = lines.flatMap(_.split(" "))

flatMap 是一個一對多的 DStream 操作,它通過從源 DStream 中的每個記錄生成多個新記錄來創建一個新的 DStream。在本例中,每一行將被分成多個單詞,單詞流表示爲單詞 DStream。接下來,我們要數這些單詞。

import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3
// Count each word in each batch
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)

// Print the first ten elements of each RDD generated in this DStream to the console
wordCounts.print()

單詞DStream被進一步映射(一對一轉換)到一個(單詞,1)對的 DStream,然後對其進行簡化以獲得每批數據中單詞的頻率。最後,wordcount .print()將打印每秒生成的一些計數。

請注意,當這些行被執行時,Spark Streaming 僅設置它將在啓動時執行的計算,而沒有真正的處理已經啓動。爲了在設置完所有轉換之後開始處理,我們最後調用

ssc.start()             // Start the computation
ssc.awaitTermination()  // Wait for the computation to terminate

如果您已經下載並構建了 Spark,您可以按如下方式運行這個示例。您首先需要使用 Netcat(在大多數類unix系統中可以找到的一個小實用程序)作爲數據服務器運行

$ nc -lk 9999

然後,在另一個終端中,您可以使用以下命令啓動示例

$ ./bin/run-example streaming.NetworkWordCount localhost 9999

然後,在運行netcat服務器的終端中鍵入的任何行都將被計數並在屏幕上每秒打印一次。它看起來就像下面這樣。

基本概念

接下來,我們將繼續簡單的示例,詳細介紹 Spark 流的基礎知識。

依賴

與 Spark 類似,Spark Streaming 也可以通過 Maven Central 獲得。要編寫自己的Spark流程序,您必須將以下依賴項添加到您的SBT或Maven項目中。

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming_2.11</artifactId>
    <version>2.1.1</version>
</dependency>

要從 Kafka、Flume 和 Kinesis 等 Spark 流核心 API 中沒有的數據源中獲取數據,您必須將相應的工件 Spark -stream -xyz_2.11添加到依賴項中。例如,一些常見的例子如下。

Source Artifact
Kafka spark-streaming-kafka-0-8_2.11
Flume spark-streaming-flume_2.11
Kinesis spark-streaming-kinesis-asl_2.11 [Amazon Software License]

StreamingContext初始化

要初始化一個 Spark Streaming 程序,必須創建一個 StreamingContext 對象,該對象是所有 Spark Streaming 功能的主要入口點。

可以從 SparkConf 對象創建 StreamingContext 對象。

import org.apache.spark._
import org.apache.spark.streaming._

val conf = new SparkConf().setAppName(appName).setMaster(master)
val ssc = new StreamingContext(conf, Seconds(1))

appName參數是應用程序在集羣UI上顯示的名稱。master是一個Spark、Mesos或YARN集羣URL,或者一個特殊的“local[*]”字符串,在本地模式下運行。

實際上,在集羣上運行時,您不希望在程序中寫死 master,而是使用 spark-submit 啓動應用程序並在那裏接收它。但是,對於本地測試和單元測試,您可以通過“local[*]”來運行 Spark Streaming in-process(檢測本地系統中的內核數量)。

批處理間隔必須根據應用程序的延遲需求和可用的集羣資源來設置。

StreamingContext 對象也可以從現有的 SparkContext 對象創建。

import org.apache.spark.streaming._

val sc = ...                // existing SparkContext
val ssc = new StreamingContext(sc, Seconds(1))

在定義了上下文之後,您必須執行以下操作。

  1. 通過創建輸入 DStreams 來定義輸入源。
  2. 通過對 DStreams 應用轉換和輸出操作來定義流計算。
  3. 開始接收數據並使用 streamingContext.start()進行處理。
  4. 使用 streamingContext.awaitTermination()等待處理停止(手動或由於任何錯誤)。
  5. 可以使用 streamingContext.stop()手動停止處理。

注意:

  1. 一旦上下文啓動,就不能設置或添加新的流計算。
  2. 一旦上下文被停止,就不能重新啓動它。
  3. JVM 中只能同時激活一個 StreamingContext。
  4. StreamingContext 上的 stop()也會停止 SparkContext。要僅停止 StreamingContext,請將名爲 stopSparkContext 的 stop()的可選參數設置爲 false。
  5. 只要在創建下一個 StreamingContext 之前停止前一個 StreamingContext(不停止SparkContext),就可以重用 SparkContext來創建多個 StreamingContext。

Discretized Streams (DStreams)

離散流或 DStream 是 Spark 流提供的基本抽象。它表示連續的數據流,可以是從源接收到的輸入數據流,也可以是通過轉換輸入流生成的經過處理的數據流。在內部,DStream由一系列連續的RDDs表示,RDDs是Spark對不可變的分佈式數據集的抽象(有關詳細信息,請參閱Spark編程指南)。DStream中的每個RDD包含來自特定時間間隔的數據,如下圖所示。

應用於 DStream 上的任何操作都轉換爲底層 RDDs 上的操作。例如,在前面將一個行流轉換爲單詞的示例中,flatMap 操作應用於行 DStream 中的每個RDD,以生成單詞 DStream的 rrds。如下圖所示。

這些底層的RDD轉換是由Spark引擎計算的。DStream操作隱藏了這些細節中的大部分,併爲開發人員提供了更高級的API。這些操作將在後面的小節中詳細討論。

輸入數據流和接收器

輸入數據流是表示從流媒體源接收的輸入數據流的數據流。在這個簡單的示例中,行是一個輸入DStream,因爲它表示從netcat服務器接收到的數據流。每個輸入 DStream(本節後面討論的文件流除外)都與接收方(Scala doc、Java doc)對象相關聯,接收方接收來自源的數據並將其存儲在Spark內存中進行處理。

Spark流媒體提供了兩類內置的流媒體源。

  • Basic Sources:StreamingContext API中直接可用的資源。示例:file systems, and socket connections。
  • Advanced Sources:像Kafka, Flume, Kinesis等資源可以通過額外的工具類獲得。如鏈接部分所述,這些需要針對額外依賴項進行鏈接。

注意,如果希望在流應用程序中並行接收多個數據流,可以創建多個輸入DStreams(將在性能調優一節中進一步討論)。這將創建多個接收器,同時接收多個數據流。但是請注意,Spark worker/executor 是一個長時間運行的任務,因此它佔用分配給 Spark  流應用程序的一個核心。因此,重要的是要記住,Spark流應用程序需要分配足夠的 core(或線程,如果在本地運行)來處理接收到的數據。

牢記:

  • 在本地運行 Spark 流程序時,不要使用“local”或“local[1]”作爲主URL。這兩種方法都意味着只有一個線程將用於在本地運行任務。如果您正在使用基於接收器的輸入DStream(例如sockets、Kafka、Flume等),那麼將使用單個線程來運行接收器,不留下任何線程來處理接收到的數據。因此,在本地運行時,始終使用“local[n]”作爲主URL,其中要運行n個接收方(有關如何設置主接收方的信息,請參閱https://blog.csdn.net/zpf_940810653842/article/details/103372744#spark-properties)。
  • 將邏輯擴展到在集羣上運行,分配給Spark流應用程序的內核數量必須大於接收器的數量。否則,系統將接收數據,但無法處理它。

基礎Sources

我們已經在這個簡易的示例中看到了ssc.socketTextStream(…),它根據通過 TCP 套接字連接接收的文本數據創建 DStream。除了套接字之外,StreamingContext API 還提供了將文件創建爲輸入源的方法。

文件流:爲了從任何與HDFS API(即HDFS、S3、NFS等)兼容的文件系統上的文件中讀取數據,可以創建一個DStream:

  streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory)

Spark 流將監視目錄 dataDirectory 並處理在該目錄中創建的任何文件(不支持在嵌套目錄中編寫的文件)。請注意,

  • 這些文件必須具有相同的數據格式。
  • 這些文件必須在 dataDirectory 中創建,方法是自動地將它們移動或重命名到數據目錄中。
  • 文件一旦被移動,就不能被改變。因此,如果文件是連續追加的,則不會讀取新數據。

對於簡單的文本文件,有一個更簡單的方法 streamingContext.textFileStream(dataDirectory)。文件流不需要運行接收器,因此不需要分配 core。

Python API中沒有fileStream,只有textFileStream可用。

基於自定義接收器的流:DStreams 可以使用通過自定義接收器接收的數據流創建。有關詳細信息,請參閱

Spark Streaming Custom Receivers

RDDs作爲流的隊列:爲了測試帶有測試數據的 Spark 流應用程序,還可以使用 streamingContext.queueStream(queueOfRDDs)創建基於 RDDs 隊列的 DStream。推入隊列的每個 RDD 將被視爲 DStream 中的一批數據,並像流一樣進行處理。

高級Sources

在這些源代碼中,Kafka、Kinesis 和 Flume都可以在 Python API中找到。

這類源需要與外部非 spark 庫交互,其中一些具有複雜的依賴關係(例如Kafka和Flume)。因此,爲了最小化與版本依賴衝突相關的問題,從這些源創建 DStreams 的功能已經轉移到單獨的庫中,在必要時可以顯式地鏈接到這些庫。

注意,這些高級源代碼在 Spark shell 中不可用,因此基於這些高級源代碼的應用程序不能在 shell 中進行測試。如果您真的想在Spark shell 中使用它們,那麼您必須下載相應的 Maven工件及其依賴項,並將其添加到類路徑中。

Kafka:http://spark.apache.org/docs/2.1.1/streaming-kafka-integration.html

......

自定義Sources

這在Python中還不支持。

還可以從自定義數據源創建輸入DStreams。您所要做的就是實現一個用戶定義的接收器(請參閱下一節瞭解它是什麼),它可以接收來自自定義源的數據並將其推入Spark。有關詳細信息,請參閱 Spark Streaming Custom Receivers

Receiver 可靠性

基於數據源的可靠性,可以有兩種數據源。數據源(如Kafka和Flume)允許確認傳輸的數據。如果從這些可靠來源接收數據的系統正確地確認接收到的數據,則可以確保不會由於任何類型的故障而丟失任何數據。這就導致了兩種類型的 receivers:

  1. Reliable Receiver - 當數據已被接收並存儲在帶有複製的Spark中時,可靠的接收方正確地向可靠的源發送確認。
  2. Unreliable Reciver - 不可靠的接收器不向源發送確認。這可以用於不支持確認的源,甚至可以用於不希望或不需要進入確認複雜性的可靠源。

DStreams上的算子

與 RDDs類似,轉換允許修改輸入 DStream 中的數據。DStreams 支持許多在普通 Spark RDD 上可用的轉換。一些常見的例子如下。

Transformation Meaning
map(func) 通過函數func傳遞源 DStream 的每個元素來返回一個新的 DStream。
flatMap(func) 與map類似,但是每個輸入項可以映射到0或多個輸出項。
filter(func) 通過只選擇 func 返回true的源 DStream 的記錄來返回一個新的DStream。
repartition(numPartitions) 通過創建更多或更少的分區來改變 DStream 中的並行度。
union(otherStream) 返回一個新的 DStream,它包含源 DStream 和 otherDStream 中元素的並集。
count() 通過計算源 DStream 的每個RDD中的元素數量,返回一個新的單元素 RDDs DStream。
reduce(func) 通過使用函數func(它接受兩個參數並返回一個參數)聚合源 DStream 的每個 RDD中的元素,返回一個新的單元素 RDDs DStream。這個函數應該是結合律和交換律,這樣才能並行計算。
countByValue() 當對類型爲K的元素的DStream調用時,返回一個新的DStream (K, Long)對,其中每個鍵的值是它在源DStream的每個RDD中的頻率。
reduceByKey(func, [numTasks]) 當在(K, V)對的DStream上調用時,返回一個新的(K, V)對的DStream,其中每個鍵的值使用給定的reduce函數進行聚合。注意:在默認情況下,這將使用Spark的默認並行任務數(本地模式爲2,而在集羣模式下,該數量由配置屬性Spark .default.parallelism決定)來進行分組。您可以傳遞一個可選的numTasks參數來設置不同數量的任務。
join(otherStream, [numTasks]) 當調用兩個DStream (K, V)和(K, W)對時,返回一個新的DStream (K, (V, W))對,每個鍵的所有對的元素。
cogroup(otherStream, [numTasks]) 當調用一個(K, V)和(K, W)對的DStream時,返回一個(K, Seq[V], Seq[W])元組的新DStream。
transform(func) 通過對源 DStream 的每個RDD應用一個RDD-to-RDD函數來返回一個新的DStream。這可以用來在DStream上執行任意的RDD操作。
updateStateByKey(func) 返回一個新的“狀態”DStream,其中通過對鍵的前一個狀態和鍵的新值應用給定的函數來更新每個鍵的狀態。這可以用來維護每個鍵的任意狀態數據。

UpdateStateByKey Operation

updateStateByKey 操作允許您維護任意狀態,同時不斷地用新信息更新它。要使用它,您必須執行兩個步驟。

  • 定義狀態——狀態可以是任意的數據類型。
  • 定義狀態更新函數——使用一個函數來指定如何使用輸入流中的前一個狀態和新值來更新狀態。

在每個批處理中,Spark將對所有現有密鑰應用狀態更新功能,而不管它們在批處理中是否有新數據。如果更新函數返回None,則鍵值對將被刪除。

讓我們用一個例子來說明這一點。假設您希望維護在文本數據流中看到的每個單詞的運行計數。這裏,運行計數是狀態,它是一個整數。我們將更新函數定義爲:

def updateFunction(newValues: Seq[Int], runningCount: Option[Int]): Option[Int] = {
    val newCount = ...  // 使用前一個運行的計數添加新值以獲得新計數
    Some(newCount)
}

這將應用於包含單詞的 DStream(例如,前面示例中包含(word,1)對的 DStream)。

val runningCounts = pairs.updateStateByKey[Int](updateFunction _)

每個單詞都將調用 update 函數,newValues 的序列爲1(來自(word, 1)對),runningCount 的序列爲前一個計數。

注意,使用updateStateByKey需要配置檢查點目錄

Transform Operation

轉換操作(及其變體,如transformWith)允許在DStream上應用任意的RDD-to-RDD函數。它可以用於應用DStream API中沒有公開的任何RDD操作。例如,將數據流中的每個批處理與另一個數據集連接的功能並沒有直接在DStream API中公開。但是,您可以很容易地使用transform來實現這一點。這帶來了非常強大的可能性。例如,可以通過將輸入數據流與預先計算的垃圾信息(也可以使用Spark生成)a連接起來,從而進行實時數據清理。

val spamInfoRDD = ssc.sparkContext.newAPIHadoopRDD(...) // RDD containing spam information

val cleanedDStream = wordCounts.transform { rdd =>
  rdd.join(spamInfoRDD).filter(...) // join data stream with spam information to do data cleaning
  ...
}

注意,所提供的函數在每個批處理間隔中被調用。這允許您執行時變的RDD操作,即RDD操作、分區數量、廣播變量等可以在批之間更改。

Window Operations

Spark 流還提供了窗口計算,它允許您在數據的滑動窗口上應用轉換。下圖演示了這個滑動窗口。

如圖所示,每當窗口在源 DStream 上滑動時,位於窗口內的源 RDDs 就會被合併並操作,以生成加了窗口的 DStream的RDDs。在本例中,操作應用於數據的最後3個時間單位,幻燈片應用於2個時間單位。這表明任何窗口操作都需要指定兩個參數。

  • 窗口大小:窗口的持續時間(圖中爲3)。
  • 滑動間隔:窗口操作執行的時間間隔(圖中爲2)。

這兩個參數必須是源 DStream 的批處理間隔的倍數(圖中爲1)。

讓我們用一個例子來說明窗口操作。例如,您希望通過每10秒在最後30秒的數據中生成字數計數來擴展前面的示例。爲此,我們必須在最後30秒的數據中對(word, 1)對的 DStream 應用 reduceByKey 操作。這是使用 reduceByKeyAndWindow 操作完成的。

// Reduce last 30 seconds of data, every 10 seconds
val windowedWordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b), Seconds(30), Seconds(10))

下面是一些常見的窗口操作。所有這些操作都採用上述兩個參數——窗口長度和滑動間隔。

Transformation Meaning
window(windowLength, slideInterval) 返回一個新的DStream,它是基於源DStream的加窗批量計算的。
countByWindow(windowLength, slideInterval) 返回流中元素的滑動窗口計數。
reduceByWindow(func, windowLength, slideInterval) 返回一個新的單元素流,它是通過使用func將流中的元素在一個滑動區間內聚合而創建的。這個函數應該是結合律和交換律,這樣才能正確地並行計算。
reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]) 當在一個(K, V)對的DStream上調用時,返回一個新的(K, V)對的DStream,其中每個鍵的值在滑動窗口中使用給定的reduce函數func over batch進行聚合。Note: B默認情況下,這將使用Spark的默認並行任務數量(本地模式爲2個,在集羣模式下,該數量由配置屬性Spark .default.parallelism決定)來進行分組。您可以傳遞一個可選的numTasks參數來設置不同數量的任務。
reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks])

上面的reduceByKeyAndWindow()的一個更有效的版本,其中每個窗口的reduce值是使用前一個窗口的reduce值遞增計算的。這是通過減少進入滑動窗口的新數據和“反向減少”離開窗口的舊數據來實現的。例如,在窗口滑動時“添加”和“減去”鍵數。

但是,它只適用於“可逆約簡函數”,即具有相應的“逆約簡”函數的約簡函數(取參數invFunc)。與reduceByKeyAndWindow一樣,reduce任務的數量可以通過一個可選參數進行配置。

注意,必須啓用檢查點才能使用此操作。

countByValueAndWindow(windowLength, slideInterval, [numTasks]) 當調用一個(K, V)對的DStream時,返回一個新的(K, Long)對的DStream,其中每個鍵的值是它在滑動窗口中的頻率。與reduceByKeyAndWindow一樣,reduce任務的數量可以通過一個可選參數進行配置。

Join Operations

最後,值得強調的是,您可以多麼容易地在Spark流中執行不同類型的連接。

Stream-stream joins

流可以很容易地與其他流連接。

val stream1: DStream[String, String] = ...
val stream2: DStream[String, String] = ...
val joinedStream = stream1.join(stream2)

在這裏,在每個批處理間隔中,由stream1生成的RDD將與由stream2生成的RDD相連接。你也可以使用 leftOuterJoin, rightOuterJoin, fullOuterJoin。此外,在流的窗口上進行連接通常非常有用。這也很簡單。

val windowedStream1 = stream1.window(Seconds(20))
val windowedStream2 = stream2.window(Minutes(1))
val joinedStream = windowedStream1.join(windowedStream2)

Stream-dataset joins

在前面的explain DStream中已經顯示了這一點。變換操作。下面是另一個將窗口化的流與數據集連接起來的示例。

val dataset: RDD[String, String] = ...
val windowedStream = stream.window(Seconds(20))...
val joinedStream = windowedStream.transform { rdd => rdd.join(dataset) }

實際上,您還可以動態地更改要加入的數據集。提供的轉換函數在每個批處理間隔進行評估,因此將使用數據集引用點所指向的當前數據集。

DStream轉換的完整列表可以在API文檔中找到。有關Scala API,請參見 DStream 和 PairDStreamFunctions。有關Java API,請參見 JavaDStream 和 JavaPairDStream。有關Python API,請參閱 DStream

DStreams上的輸出操作

輸出操作允許將DStream的數據推送到外部系統,如數據庫或文件系統。由於輸出操作實際上允許外部系統使用轉換後的數據,因此它們會觸發所有DStream轉換的實際執行(類似於RDDs的操作)。目前定義了以下輸出操作:

Output Operation Meaning
print() 在運行流應用程序的驅動節點上打印 DStream 中每批數據的前十個元素。這對於開發和調試非常有用。
這在Python API中稱爲pprint()。
saveAsTextFiles(prefix, [suffix]) 將 DStream 的內容保存爲文本文件。每個批處理間隔的文件名是根據前綴和後綴“prefix-TIME_IN_MS[.suffix]”生成的。
saveAsObjectFiles(prefix, [suffix]) 將這個DStream的內容保存爲序列化的Java對象的序列文件。每個批處理間隔的文件名是根據前綴和後綴“prefix-TIME_IN_MS[.suffix]”生成的。
這在Python API中不可用。
saveAsHadoopFiles(prefix, [suffix]) 將DStream的內容保存爲Hadoop文件。每個批處理間隔的文件名是根據前綴和後綴“prefix-TIME_IN_MS[.suffix]”生成的。
這在Python API中不可用。
foreachRDD(func) 將函數func應用於從流生成的每個RDD的最通用的輸出操作符。該函數應該將每個RDD中的數據推送到外部系統,例如將RDD保存到文件中,或者通過網絡將其寫入數據庫。請注意,func函數是在運行流應用程序的驅動程序進程中執行的,並且通常會有RDD動作,這將強制流RDDs的計算。

使用foreachRDD的設計模式

dstream.foreachRDD 是一個功能強大的原語,它允許將數據發送到外部系統。然而,理解如何正確和有效地使用這個原語是很重要的。要避免的一些常見錯誤如下。

通常,將數據寫入外部系統需要創建一個連接對象(例如,到遠程服務器的TCP連接)並使用它將數據發送到遠程系統。爲此,開發人員可能會無意中嘗試在Spark驅動程序中創建連接對象,然後嘗試在Spark worker中使用它來保存RDDs中的記錄。例如(在Scala中),

dstream.foreachRDD { rdd =>
  val connection = createNewConnection()  // executed at the driver
  rdd.foreach { record =>
    connection.send(record) // executed at the worker
  }
}

這是不正確的,因爲這需要將連接對象序列化並從驅動程序發送到工作程序。這樣的連接對象很少能跨機器轉移。此錯誤可能表現爲序列化錯誤(連接對象不可序列化)、初始化錯誤(連接對象需要在工作人員處初始化)等。正確的解決方案是在worker上創建連接對象。

然而,這可能會導致另一個常見錯誤——爲每個記錄創建一個新連接。例如,

dstream.foreachRDD { rdd =>
  rdd.foreach { record =>
    val connection = createNewConnection()
    connection.send(record)
    connection.close()
  }
}

通常,創建連接對象需要時間和資源開銷。因此,爲每個記錄創建和銷燬一個連接對象可能導致不必要的高開銷,並可能顯著降低系統的總體吞吐量。更好的解決方案是使用 rdd.foreachPartition——創建一個連接對象,並使用該連接發送 RDD 分區中的所有記錄。

dstream.foreachRDD { rdd =>
  rdd.foreachPartition { partitionOfRecords =>
    val connection = createNewConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    connection.close()
  }
}

這會將創建連接的開銷分攤到許多記錄上。

最後,可以通過跨多個RDDs/batch重用連接對象進一步優化這一點。可以維護一個靜態的連接對象池,在將多個批的RDDs推送到外部系統時可以重用這些對象,從而進一步減少開銷。

dstream.foreachRDD { rdd =>
  rdd.foreachPartition { partitionOfRecords =>
    // ConnectionPool is a static, lazily initialized pool of connections
    val connection = ConnectionPool.getConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    ConnectionPool.returnConnection(connection)  // return to the pool for future reuse
  }
}

請注意,池中的連接應該按需延遲創建,如果不使用一段時間就會超時。這實現了向外部系統發送數據的最有效的方式。

其他需要注意的點:

  • DStreams 由輸出操作延遲執行,就像RDD操作延遲執行rds一樣。具體來說,DStream 輸出操作中的RDD操作強制處理接收到的數據。因此,如果您的應用程序沒有任何輸出操作,或者有像 dstream.foreachRDD()這樣的輸出操作,但是其中沒有任何RDD操作,那麼什麼也不會執行。系統將簡單地接收數據並丟棄它。
  • 默認情況下,一次執行一個輸出操作。它們是按照在應用程序中定義的順序執行的。

DataFrame和SQL操作

您可以輕鬆地在流數據上使用 DataFrames 和 SQL 操作。您必須使用 StreamingContext 正在使用的 SparkContext 創建一個SparkSession。此外,這樣做可以在驅動程序失敗時重新啓動。這是通過創建一個延遲實例化的 SparkSession 單例實例來實現的。如下面的例子所示。它修改了前面的字數統計示例,以使用 DataFrames 和 SQL 生成字數統計。每個 RDD 都被轉換爲一個DataFrame,註冊爲一個臨時表,然後使用 SQL 進行查詢。

/** DataFrame operations inside your streaming program */

val words: DStream[String] = ...

words.foreachRDD { rdd =>

  // Get the singleton instance of SparkSession
  val spark = SparkSession.builder.config(rdd.sparkContext.getConf).getOrCreate()
  import spark.implicits._

  // Convert RDD[String] to DataFrame
  val wordsDataFrame = rdd.toDF("word")

  // Create a temporary view
  wordsDataFrame.createOrReplaceTempView("words")

  // Do word count on DataFrame using SQL and print it
  val wordCountsDataFrame = 
    spark.sql("select word, count(*) as total from words group by word")
  wordCountsDataFrame.show()
}

您還可以對來自不同線程(即與運行中的 StreamingContext 異步)的流數據上定義的表運行 SQL 查詢。只要確保您設置了StreamingContext 來記住足夠數量的流數據,以便查詢可以運行。否則,StreamingContext (它不知道任何異步SQL查詢)將在查詢完成之前刪除舊的流數據。例如,如果您想要查詢最後一批數據,但是您的查詢可能需要5分鐘才能運行,那麼可以調用streamingContext.remember(minutes(5))(在Scala中是這樣,或者在其他地方是相同的)

參見 Spark SQL, DataFrames and Datasets Guide,以瞭解更多關於DataFrames的信息。

MLlib Operations

您還可以輕鬆地使用由 MLlib 提供的機器學習算法。首先,有流式機器學習算法(如流式線性迴歸、流式KMeans等)可以同時從流式數據中學習,也可以將模型應用到流式數據中。除此之外,對於更大類別的機器學習算法,您可以離線學習一個學習模型(即使用歷史數據),然後在線將該模型應用於流數據。

Caching / Persistence

與 RDDs 類似,DStreams 還允許開發人員將流的數據持久化到內存中。也就是說,在 DStream上使用 persist()方法將自動在內存中持久化該 DStream 的每個 RDD。如果 DStream 中的數據將被多次計算(例如,對同一數據的多次操作),那麼這是非常有用的。對於基於窗口的操作,如 reduceByWindow 和 reduceByKeyAndWindow,以及基於狀態的操作,如 updateStateByKey,這是隱式正確的。因此,由基於窗口的操作生成的 DStreams 將自動持久化到內存中,而無需開發人員調用persist()

對於通過網絡接收數據的輸入流(例如,Kafka、Flume、sockets等),默認的持久性級別被設置爲將數據複製到兩個節點以實現容錯。

注意,與RDDs不同,DStreams的默認持久性級別將數據序列化在內存中。這將在性能調優一節中進一步討論。有關不同持久性級別的更多信息可以在 Spark Programming Guide 中找到。

Checkpointing

流應用程序必須全天候運行,因此必須對與應用程序邏輯無關的故障具有彈性(例如,系統故障、JVM崩潰等)。爲了實現這一點,Spark 流需要將足夠的信息檢查點到容錯存儲系統,以便能夠從故障中恢復。有兩種類型的數據是檢查點的。

  • 元數據:將定義流計算的信息保存到容錯存儲(如HDFS)。這用於從運行流應用程序的驅動程序的節點的故障中恢復(稍後將詳細討論)。元數據包括:
    • Configuration - 用於創建流應用程序的配置。
    • DStream operations - 定義流應用程序的一組DStream操作。
    • Incomplete batches - 任務排隊但尚未完成的批次。
  • 數據:將生成的RDDs保存到可靠的存儲中。在跨多個批組合數據的一些有狀態轉換中,這是必需的。在這種轉換中,生成的RDDs依賴於前一批的RDDs,這導致依賴鏈的長度隨時間不斷增加。爲了避免這種恢復時間的無界增長(與依賴項鍊成比例),有狀態轉換的中間rds會定期檢查可靠存儲(如HDFS),以切斷依賴項鍊。

總之,元數據檢查點主要用於從驅動程序故障中恢復,而數據或RDD檢查點甚至對於使用有狀態轉換的基本功能也是必要的。

When to enable Checkpointing

必須啓用檢查點的應用程序有下列任何一項要求:

  • 使用有狀態轉換 — 如果在應用程序中使用 updateStateByKey 或 reduceByKeyAndWindow(帶有逆函數),那麼必須提供檢查點目錄來允許定期的 RDD 檢查點。
  • 從運行應用程序的驅動程序的故障中恢復 — 使用元數據檢查點來恢復進度信息。

注意,沒有上述有狀態轉換的簡單流應用程序可以在不啓用檢查點的情況下運行。在這種情況下,從驅動程序故障的恢復也將是部分的(一些已接收但未處理的數據可能會丟失)。這通常是可以接受的,許多以這種方式運行Spark流應用程序。對非hadoop環境的支持有望在未來得到改善。

How to configure Checkpointing

檢查點可以通過在一個容錯的、可靠的文件系統(如 HDFS、S3 等)中設置一個目錄來啓用,檢查點信息將被保存到這個目錄中。這是通過使用 streamingContext.checkpoint(checkpointDirectory)實現的。這將允許您使用前面提到的有狀態轉換。另外,如果您想讓應用程序從驅動程序故障中恢復,您應該重寫您的流應用程序,使其具有以下行爲。

  • 當程序第一次啓動時,它將創建一個新的 StreamingContext,設置所有的流,然後調用 start()。
  • 當程序在失敗後重新啓動時,它將從檢查點目錄中的檢查點數據重新創建一個 StreamingContext

通過使用 StreamingContext.getOrCreate 可以簡化此行爲。它的用法如下。

// Function to create and setup a new StreamingContext
def functionToCreateContext(): StreamingContext = {
  val ssc = new StreamingContext(...)   // new context
  val lines = ssc.socketTextStream(...) // create DStreams
  ...
  ssc.checkpoint(checkpointDirectory)   // set checkpoint directory
  ssc
}

// Get StreamingContext from checkpoint data or create a new one
val context = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext _)

// Do additional setup on context that needs to be done,
// irrespective of whether it is being started or restarted
context. ...

// Start the context
context.start()
context.awaitTermination()

如果存在 checkpointDirectory,那麼將從檢查點數據重新創建上下文。如果該目錄不存在(即,然後調用 functionToCreateContext 函數來創建新上下文並設置 DStreams。參見Scala示例 RecoverableNetworkWordCount

除了使用 getOrCreate 之外,還需要確保驅動程序進程在失敗時自動重新啓動。這隻能由用於運行應用程序的部署基礎設施來完成。

注意,RDDs的檢查點會增加將數據保存到可靠存儲的成本。這可能會導致RDDs被檢查點的那些批次的處理時間增加。因此,需要仔細設置檢查點的間隔。在小批量情況下(比如1秒),每批檢查可能會顯著降低操作吞吐量。相反,檢查點太少會導致沿襲和任務大小增長,這可能會產生有害的影響。對於需要RDD檢查點的有狀態轉換,默認間隔是至少10秒的批處理間隔的倍數。它可以通過使用 dstream.checkpoint(checkpointInterval)來設置。通常,一個 DStream的5 - 10個滑動間隔的檢查點間隔是一個很好的設置。

Accumulators, Broadcast Variables, and Checkpoints

無法從 spark 流中的檢查點恢復累加器和廣播變量。如果啓用了檢查點並同時使用累加器或廣播變量,則必須爲累加器和廣播變量創建延遲實例化的單例實例,以便在驅動程序失敗重新啓動後重新實例化它們。如下面的例子所示。

object WordBlacklist {

  @volatile private var instance: Broadcast[Seq[String]] = null

  def getInstance(sc: SparkContext): Broadcast[Seq[String]] = {
    if (instance == null) {
      synchronized {
        if (instance == null) {
          val wordBlacklist = Seq("a", "b", "c")
          instance = sc.broadcast(wordBlacklist)
        }
      }
    }
    instance
  }
}

object DroppedWordsCounter {

  @volatile private var instance: LongAccumulator = null

  def getInstance(sc: SparkContext): LongAccumulator = {
    if (instance == null) {
      synchronized {
        if (instance == null) {
          instance = sc.longAccumulator("WordsInBlacklistCounter")
        }
      }
    }
    instance
  }
}

wordCounts.foreachRDD { (rdd: RDD[(String, Int)], time: Time) =>
  // Get or register the blacklist Broadcast
  val blacklist = WordBlacklist.getInstance(rdd.sparkContext)
  // Get or register the droppedWordsCounter Accumulator
  val droppedWordsCounter = DroppedWordsCounter.getInstance(rdd.sparkContext)
  // Use blacklist to drop words and use droppedWordsCounter to count them
  val counts = rdd.filter { case (word, count) =>
    if (blacklist.value.contains(word)) {
      droppedWordsCounter.add(count)
      false
    } else {
      true
    }
  }.collect().mkString("[", ", ", "]")
  val output = "Counts at time " + time + " " + counts
})

Deploying Applications

本節討論部署Spark流應用程序的步驟。

Requirements

要運行Spark流應用程序,您需要具備以下條件。

  • 有集羣管理器的集羣——這是任何Spark應用程序的一般需求,並在部署指南中詳細討論。
  • 打包應用程序 JAR——必須將流應用程序編譯到一個JAR中。如果您使用 Spark -submit 來啓動應用程序,那麼您將不需要在JAR中提供Spark和Spark流。但是,如果您的應用程序使用高級的源代碼(例如Kafka、Flume),那麼您必須將它們鏈接到的額外工件及其依賴項打包到用於部署應用程序的JAR中。例如,使用 KafkaUtils 的應用程序必須在應用程序JAR中包含 spark- streamingkafka -0-8_2.11及其所有傳遞依賴項。
  • 爲執行器配置足夠的內存——因爲接收到的數據必須存儲在內存中,所以必須爲執行器配置足夠的內存來保存接收到的數據。注意,如果您正在執行10分鐘的窗口操作,則系統必須在內存中保留至少10分鐘的數據。因此,應用程序的內存需求取決於它所使用的操作。
  • 配置檢查點——如果流應用程序需要它,那麼必須將 Hadoop API 兼容容錯存儲中的一個目錄(如HDFS、S3等)配置爲檢查點目錄,並以檢查點信息可用於故障恢復的方式編寫流應用程序。有關更多細節,請參見檢查點部分。
  • 配置應用程序驅動程序的自動重啓——爲了從驅動程序失敗中自動恢復,用於運行流應用程序的部署基礎設施必須監視驅動程序進程,並在驅動程序失敗時重新啓動驅動程序。不同的集羣管理器有不同的工具來實現這一點。
    • Spark單機——可以提交一個 Spark 應用程序驅動程序在 Spark 單機集羣中運行,也就是說,應用程序驅動程序本身在一個工作節點上運行。此外,可以指示獨立集羣管理器監視驅動程序,並在驅動程序由於非零退出碼或由於運行驅動程序的節點失敗而失敗時重新啓動它。
    • YARN——YARN支持類似的自動重新啓動應用程序的機制。
    • Mesos -馬拉松已經被用來實現這與Mesos。
  • 配置寫前日誌——從 Spark 1.2開始,我們就引入了寫前日誌,以實現強大的容錯保證。如果啓用,則從接收器接收到的所有數據都將寫入配置檢查點目錄中的寫前日誌。這可以防止在驅動程序恢復時丟失數據,從而確保零數據丟失(在容錯語義一節中詳細討論)。這可以通過設置配置參數 spark.stream .receiver. writeaheadlog 爲true來啓用。然而,這些更強的語義可能會以單個接收器的接收吞吐量爲代價。這可以通過並行運行更多的接收器來糾正,以增加總吞吐量。此外,建議在啓用寫前日誌時禁用Spark中接收數據的複製,因爲日誌已經存儲在複製的存儲系統中。這可以通過將輸入流的存儲級別設置爲 StorageLevel.MEMORY_AND_DISK_SER 來實現。在使用S3(或任何不支持刷新的文件系統)寫前日誌時,請記住啓用 spark.streaming.driver.writeAheadLog.closeFileAfterWrite and spark.streaming.receiver.writeAheadLog.closeFileAfterWrite.有關詳細信息,請參閱 Spark Configuration Guide 。請注意,當啓用I/O加密時,Spark不會加密寫到寫前日誌的數據。如果需要對寫前日誌數據進行加密,則應該將其存儲在本地支持加密的文件系統中。
  • 設置最大接收速率—如果集羣資源不夠大,流應用程序無法像接收數據那樣快速處理數據,則可以通過設置記錄/秒的最大速率限制來限制接收方的速率。在Spark 1.5中,我們引入了一個稱爲回壓的特性,它消除了設置速率限制的需要,因爲Spark流會自動計算速率限制,並在處理條件發生變化時動態調整它們。通過設置配置參數 spark.stream .backpressure 爲 true 可以啓用此背壓。 之前的版本通過 spark.streaming.receiver.maxRate for receivers and spark.streaming.kafka.maxRatePerPartition 這兩個參數控制接受速率。

Upgrading Application Code

如果正在運行的Spark流應用程序需要使用新的應用程序代碼進行升級,那麼有兩種可能的機制。

  • 升級後的Spark流應用程序將與現有應用程序並行啓動和運行。一旦新服務器(接收與舊服務器相同的數據)被預熱並準備好進入黃金時間,舊服務器就可以被關閉。請注意,對於支持將數據發送到兩個目的地(即、早期和升級的應用程序)。
  • 現有的應用程序被優雅地關閉(有關優雅的關閉選項,請參閱 StreamingContext.stop(…)或 JavaStreamingContext.stop(…)),以確保在關閉之前已經接收到的數據被完全處理。然後可以啓動升級後的應用程序,它將從前面的應用程序停止的地方開始處理。請注意,這隻能在支持源端緩衝的輸入源(如Kafka和Flume)中完成,因爲需要在前一個應用程序宕機而升級的應用程序尚未啓動時對數據進行緩衝。並且無法從升級前代碼的早期檢查點信息重新啓動。檢查點信息本質上包含序列化的 Scala/Java/Python 對象,嘗試使用新的、修改過的類來反序列化對象可能會導致錯誤。在這種情況下,可以使用不同的檢查點目錄啓動升級後的應用程序,也可以刪除以前的檢查點目錄

Monitoring Applications

除了 Spark 的監視功能之外,還有其他特定於 Spark 流的功能。當使用 StreamingContext 時,Spark web UI 會顯示一個附加的流選項卡,其中顯示關於正在運行的接收方(接收方是否活動、接收到的記錄數量、接收方錯誤等)和完成的批(批處理時間、隊列延遲等)的統計信息。這可以用來監視流應用程序的進度。

web UI中的以下兩個指標特別重要:

  • 處理時間——處理每批數據的時間。
  • 調度延遲——批處理在隊列中等待前一批處理完成的時間。

如果批處理時間始終大於批處理間隔和/或隊列延遲不斷增加,則表明系統無法像生成批處理那樣快速地處理它們,並且正在落後。在這種情況下,可以考慮減少批處理時間。

Spark 流程序的進程也可以使用 StreamingListener 接口進行監視,該接口允許您獲得接收方狀態和處理時間。請注意,這是一個開發人員API,它可能會得到改進(即,更多信息報道)在未來。

Performance Tuning

要獲得集羣上 Spark 流應用程序的最佳性能,需要進行一些調優。本節解釋一些參數和配置,可以對它們進行調優以提高應用程序的性能。在高層次上,你需要考慮兩件事:

  • 通過有效地使用集羣資源,減少每批數據的處理時間。
  • 設置正確的批大小,以便能夠在接收數據時快速處理這些數據(即,數據處理與數據攝入同步)。

Reducing the Batch Processing Times

在Spark中可以進行許多優化,以最小化每個批的處理時間。這些在 Tuning Spark 調優 中有詳細的討論。本節重點介紹一些最重要的問題。

Level of Parallelism in Data Receiving

通過網絡接收數據(如Kafka、Flume、socket等)需要將數據反序列化並存儲在 Spark 中。如果數據接收成爲系統中的瓶頸,那麼可以考慮將數據接收並行化。請注意,每個輸入 DStream 創建一個接收方(運行在工作機器上),接收一個數據流。

因此,通過創建多個輸入數據流並配置它們以從源接收不同的數據流分區,可以實現接收多個數據流。例如,一個接收兩個數據主題的 Kafka 輸入 DStream 可以分成兩個 Kafka 輸入流,每個 Kafka 輸入流只接收一個主題。這將運行兩個接收器,允許並行接收數據,從而提高總體吞吐量。這些多個 DStreams 可以聯合在一起創建單個 DStream。然後,應用於單個輸入 DStream 上的轉換可以應用於統一的流。這是這樣做的。

val numStreams = 5
val kafkaStreams = (1 to numStreams).map { i => KafkaUtils.createStream(...) }
val unifiedStream = streamingContext.union(kafkaStreams)
unifiedStream.print()

另一個需要考慮的參數是接收機的塊間隔,它是由配置參數 spark.stream . blockinterval 決定的。對於大多數接收方來說,接收到的數據在存儲到 Spark 內存之前會被合併成數據塊。每個批處理中的塊的數量決定了在類映射轉換中用於處理接收數據的任務的數量。每批每個接收方的任務數量大約是(批處理間隔/塊間隔)。例如,200 ms的塊間隔將每2秒創建10個任務。如果任務的數量過低(即少於每臺機器的內核數量),那麼它將是低效的,因爲所有可用的內核都不會被用來處理數據。若要增加給定批處理間隔的任務數量,請減少塊間隔。但是,建議的最小塊間隔值爲50 ms左右,低於這個值可能會導致任務啓動開銷出現問題。

使用多個輸入流/接收器接收數據的另一種方法是顯式地重新劃分輸入數據流(使用 inputStream)。重新分區(<分區數量>))。在進一步處理之前,它將接收到的數據批分佈到集羣中指定數量的機器上。

Level of Parallelism in Data Processing

如果在計算的任何階段中使用的並行任務的數量都不夠高,則集羣資源可能沒有得到充分利用。例如,對於 reduceByKey 和reduceByKeyAndWindow 這樣的分佈式 reduce 操作,並行任務的默認數量由 spark.default.parallelism 配置屬性控制。您可以將並行度級別作爲參數傳遞(請參閱PairDStreamFunctions文檔),或者設置 spark.default.parallelism 配置屬性來更改默認值。

Data Serialization

通過調整序列化格式,可以減少數據序列化的開銷。對於流,有兩種類型的數據正在被序列化。

  • Input data:默認情況下,通過接收者接收到的輸入數據通過 StorageLevel.MEMORY_AND_DISK_SER_2 存儲在執行器的內存中。也就是說,將數據序列化爲字節以減少 GC 開銷,並複製數據以容忍執行程序失敗。此外,數據首先保存在內存中,只有當內存不足以容納流計算所需的所有輸入數據時,纔會溢出到磁盤。這種序列化顯然有開銷——接收方必須反序列化接收到的數據,然後使用 Spark 的序列化格式重新序列化它。
  • Persisted RDDs generated by Streaming Operations:通過流計算生成的 RDDs 可以持久化到內存中。例如,窗口操作將數據保存在內存中,因爲它們將被多次處理。然而,不像Spark 核心默認的 StorageLevel。通過流計算生成的持久化的 RDDs 使用 StorageLevel 持久化。默認是MEMORY_ONLY_SER(即序列化),以最小化GC開銷。

在這兩種情況下,使用 Kryo 序列化可以減少 CPU 和內存開銷。有關更多詳細信息,請參閱spark調優指南。對於 Kryo,可以考慮註冊自定義類,並禁用對象引用跟蹤(請參閱配置指南中與Kryo相關的配置)。

在需要爲流應用程序保留的數據量不大的特定情況下,可以將數據(兩種類型)作爲反序列化對象持久存儲,而不會導致過多的 GC開銷。例如,如果您正在使用幾秒鐘的批處理間隔,並且沒有窗口操作,那麼您可以通過顯式地相應地設置存儲級別來嘗試禁用持久數據中的序列化。這將減少由於序列化而造成的 CPU 開銷,從而在沒有太多 GC 開銷的情況下提高性能。

Task Launching Overheads

如果每秒啓動的任務數量很高(比如每秒50個或更多),那麼向從屬服務器發送任務的開銷可能會很大,並且很難實現次秒延遲。可以通過以下改變來減少開銷:

  • Execution mode:在獨立模式或粗粒度Mesos模式下運行Spark比細粒度Mesos模式的任務啓動時間更長。詳情請參閱Mesos上的運行指南

這些更改可能會將批處理時間減少100毫秒,從而使次秒級的批大小成爲可能。

Setting the Right Batch Interval

要使運行在集羣上的 Spark 流應用程序穩定,系統應該能夠在接收數據時快速處理數據。換句話說,處理批量數據的速度應該與生成數據的速度一樣快。通過監視流 web UI 中的處理時間可以發現應用程序是否如此,在流 web UI中,批處理時間應該小於批處理間隔。

根據流計算的性質,所使用的批處理間隔可能對應用程序在一組固定的集羣資源上能夠維持的數據速率產生重大影響。例如,讓我們考慮前面的 WordCountNetwork 示例。對於特定的數據速率,系統可能能夠每2秒報告一次字數計數(即,批處理間隔爲2秒),但不是每500毫秒一次。因此,需要設置批處理間隔,以便能夠維持生產中的預期數據速率。

爲應用程序確定正確的批大小的一種好方法是使用保守的批處理間隔(例如,5-10秒)和較低的數據速率對其進行測試。要驗證系統是否能夠跟上數據速率,您可以檢查每個處理批處理所經歷的端到端延遲的值(在 Spark 驅動程序 log4j 日誌中查找“Total delay”,或者使用 StreamingListener 接口)。如果延遲保持與批處理大小相當,則系統是穩定的。否則,如果延遲持續增加,則意味着系統無法跟上,因此不穩定。一旦有了穩定配置的概念,就可以嘗試增加數據速率和/或減少批處理大小。請注意,由於臨時數據速率的增加而導致的暫時延遲的增加可能是正確的,只要延遲減少到一個較低的值(即,小於批處理大小)。

Memory Tuning

調優 Spark 應用程序的內存使用和 GC 行爲在調優指南中有詳細的討論。強烈建議你讀一讀。在本節中,我們將討論一些特定於 Spark 流應用程序上下文的調優參數。

Spark 流應用程序所需的集羣內存量在很大程度上取決於所使用的轉換類型。例如,如果您想在最後10分鐘的數據上使用窗口操作,那麼您的集羣應該有足夠的內存來在內存中保存10分鐘的數據。或者,如果您希望使用帶有大量鍵的 updateStateByKey,那麼所需的內存將會很大。相反,如果您想要執行簡單的 map-filter-store 操作,那麼所需的內存將會很低。

一般來說,由於通過接收器接收到的數據是用 StorageLevel.MEMORY_AND_DISK_SER_2 存儲的。不適合內存的數據將溢出到磁盤。這可能會降低流應用程序的性能,因此建議根據流應用程序的需要提供足夠的內存。最好嘗試在小範圍內查看內存使用情況並進行相應的估計。

內存調優的另一個方面是垃圾收集。對於需要低延遲的流應用程序,JV M垃圾收集導致的大量暫停是不可取的。

有幾個參數可以幫助你調優內存使用和GC開銷:

  • Persistence Level of DStreams:正如前面在數據序列化一節中提到的,輸入數據和 RDDs 在默認情況下是作爲序列化字節持久化的。與反序列化持久性相比,這減少了內存使用和 GC 開銷。啓用 Kryo 序列化進一步減少了序列化的大小和內存使用。通過壓縮(參見Spark配置 Spark .rdd.compress )可以進一步減少內存使用量,但要以 CPU 時間爲代價。
  • Clearing old data: 默認情況下,由 DStream 轉換生成的所有輸入數據和持久的 RDDs 將被自動清除。Spark 流根據所使用的轉換決定何時清除數據。例如,如果您使用的是10分鐘的窗口操作,那麼 Spark 流將保留最後10分鐘的數據,並主動丟棄舊的數據。通過設置 streamingContext.remember,可以更長時間地保留數據(例如交互式地查詢舊數據)。

  • CMS Garbage Collector: 強烈建議使用併發標記-清除 GC,以保持與 GC 相關的暫停始終較低。儘管衆所周知併發GC會降低系統的總體處理吞吐量,但仍然建議使用它來實現更一致的批處理時間。確保在驅動程序(在 Spark -submit 中使用—driver-java-options)和執行器(使用Spark配置Spark .executor. extrajavaoptions)上都設置了CMS GC。

  • Other tips: 爲了進一步減少GC開銷,這裏還有一些技巧可以嘗試。

    • 使用OFF_HEAP存儲級別持久化RDDs. See more detail in the Spark Programming Guide.
    • 使用更多的執行器和更小的堆大小。這將減少每個JVM堆中的GC壓力。

Important points to remember:

  • DStream 與單個接收器相關聯。爲了獲得讀並行性,需要創建多個接收器,即多個 DStreams。接收器在執行程序中運行。它佔據一個核。確保在接收槽被預定後有足夠的 core 用於處理,即 spark.core.max 應該考慮接收槽。接收者以循環方式分配給執行者。

  • 當從流源接收數據時,receiver 創建數據塊。每隔一毫秒就會產生一個新的數據塊。在 batchInterval 期間創建N個數據塊,其中N = batchInterval/blockInterval。這些塊由當前執行程序的塊管理器分發給其他執行程序的塊管理器。之後,運行在驅動程序上的網絡輸入跟蹤器將被告知進一步處理的塊位置。

  • 在 batchInterval 期間創建的塊的驅動程序上創建了一個RDD。batchInterval 期間生成的塊是 RDD 的分區。每個分區都是spark 中的一個任務。blockInterval== batchinterval 將意味着創建一個單獨的分區,並且可能在本地處理它。

  • 塊上的映射任務在執行器(一個接收塊,另一個在複製塊)中處理,不管塊的間隔如何,除非出現非本地調度。擁有更大的塊間隔意味着更大的塊。spark.locality.wait 增加了在本地節點上處理一個塊的機會。需要在這兩個參數之間找到平衡,以確保在本地處理較大的塊。

  • 您可以通過調用 inputDstream.repartition(n) 來定義分區的數量,而不是依賴於 batchInterval 和 blockInterval。這將隨機重組 RDD 中的數據,以創建n個分區。是的,爲了更好的並行性。不過代價是洗牌。RDD 的處理是由驅動程序的 jobscheduler 作爲作業調度的。在給定的時間點上,只有一個作業是活動的。因此,如果一個作業正在執行,其他作業將排隊。

  • 如果您有兩個 dstreams,則會形成兩個 RDDs,並創建兩個作業,這些作業將一個接一個地安排。爲了避免這種情況,您可以聯合兩個 dstreams。這將確保 dstreams 的兩個rds形成一個 unionRDD。然後,這 個unionRDD 被視爲單個作業。但是,RDDs 的分區不受影響。

  • 如果批處理時間大於 batchinterval,那麼顯然接收方的內存將開始填滿,並最終拋出異常(很可能是BlockNotFoundException)。目前沒有辦法暫停接收器。使用 SparkConf 配置 spark.stream .receiver.maxRate,接收速率是有限的。


Fault-tolerance Semantics

在本節中,我們將討論Spark流應用程序在發生故障時的行爲。

Background

爲了理解 Spark 流提供的語義,讓我們記住 Spark 的 RDDs 的基本容錯語義。

  1. RDD 是一個不可變的、可確定地重新計算的分佈式數據集。每個 RDD 都會記住在容錯輸入數據集上用於創建它的確定性操作的血緣。
  2. 如果 RDD 的任何分區由於工作節點故障而丟失,那麼可以使用操作的血緣從原始的容錯數據集重新計算該分區。
  3. 假設所有的 RDD 轉換都是確定的,那麼不管Spark集羣中的故障如何,最終轉換的RDD中的數據總是相同的。

Spark對容錯文件系統(如HDFS或S3)中的數據進行操作。因此,從容錯數據生成的所有RDDs也都是容錯的。但是,這與Spark流的情況不同,因爲大多數情況下數據是通過網絡接收的(使用fileStream時除外)。要爲所有生成的RDDs實現相同的容錯屬性,需要在集羣中工作節點的多個Spark執行器之間複製接收到的數據(默認的複製因子爲2)。這將導致系統中有兩種數據需要在發生故障時進行恢復:

  • 接收和複製的數據——由於單個工作節點的副本存在於其他節點上,所以該數據在單個工作節點失敗後仍然存在。
  • 接收到但爲複製而緩衝的數據——由於這不是複製,因此恢復此數據的惟一方法是從源獲取它。

此外,有兩種失敗是我們應該關注的:

  • 工作節點失敗——運行 executor 的任何工作節點都可能失敗,這些節點上的所有內存數據都將丟失。如果任何接收器在失敗的節點上運行,那麼它們的緩衝數據將丟失。
  • 驅動節點失敗——如果運行 Spark 流應用程序的驅動節點失敗,那麼很明顯,SparkContext 丟失了,所有執行器及其內存中的數據都丟失了。

有了這些基礎知識,我們就可以理解Spark流的容錯語義。

容錯Definitions

流系統的語義通常是根據系統可以處理每個記錄的次數來捕獲的。在所有可能的操作條件下(除了故障等),系統可以提供三種類型的保證。

  • 最多一次:每個記錄要麼處理一次,要麼根本不處理。
  • 至少一次:每個記錄將被處理一次或多次。這比最多一次強,因爲它確保不會丟失任何數據。但是可能有重複的。
  • 精確一次:每條記錄將被精確處理一次——沒有數據會丟失,也沒有數據會被多次處理。這顯然是三者中最有力的保證。

Basic Semantics

一般來說,在任何流處理系統中,處理數據有三個步驟。

  1. 接收數據:數據從使用接收器或其他方式的源接收。
  2. 轉換數據:使用 DStream 和 RDD 轉換轉換接收的數據。
  3. 輸出數據:最終轉換後的數據被輸出到外部系統,如文件系統、數據庫、儀表板等。

如果流媒體應用程序必須實現端到端的精確一次保證,那麼每個步驟都必須提供精確一次保證。也就是說,每個記錄必須精確地接收一次,精確地轉換一次,精確地推送到下游系統一次。讓我們在 Spark 流上下文中理解這些步驟的語義。

  1. 接收數據:不同的輸入源提供不同的保證。這將在下一小節中詳細討論。
  2. 數據轉換:由於 RDDs 提供的保證,所有接收到的數據都將被精確地處理一次。即使存在故障,只要接收的輸入數據是可訪問的,最終轉換的 RDDs 將始終具有相同的內容。
  3. 輸出數據:默認情況下,輸出操作至少確保一次語義,因爲它取決於輸出操作的類型(冪等性,或非冪等性)和下游系統的語義(支持或不支持事務)。但是用戶可以實現自己的事務機制來實現精確的一次語義。本節後面將更詳細地討論這一點。

Semantics of Received Data

不同的輸入源提供不同的保證,從至少一次到正好一次。

With Files

如果所有的輸入數據都已經存在於一個容錯的文件系統(如HDFS)中,那麼 Spark 流始終可以從任何故障中恢復並處理所有的數據。這提供了嚴格的一次語義,這意味着所有的數據都將被精確地處理一次,不管什麼失敗。

With Receiver-based Sources

對於基於接收器的輸入源,容錯語義依賴於故障場景和接收器的類型。如前所述,有兩種類型的接收器:

  1. 可靠的接收器-這些接收器確認可靠的來源後,才確保收到的數據已被複制。如果這樣的接收器失敗,源將不會收到緩衝(未複製)數據的確認。因此,如果重新啓動接收方,則源將重新發送數據,並且不會因爲失敗而丟失任何數據。
  2. 不可靠的接收器——這類接收器不發送確認信息,因此,當它們由於工人或驅動程序故障而失敗時,可能會丟失數據。

根據所使用的接收器類型,我們可以實現以下語義。如果工作節點失敗,那麼可靠的接收器就不會丟失數據。對於不可靠的接收器,接收到但沒有複製的數據可能會丟失。如果驅動節點失敗,那麼除了這些丟失之外,所有在內存中接收和複製的過去數據都將丟失。這將影響有狀態轉換的結果。

爲了避免丟失過去接收到的數據,Spark 1.2引入了寫前日誌,將接收到的數據保存到容錯存儲中。啓用寫前日誌和可靠的接收器,數據丟失爲零。在語義方面,它至少提供了一次保證。

下表總結了故障下的語義:

Deployment Scenario Worker Failure Driver Failure
Spark 1.1 or earlier, OR
Spark 1.2 or later without write ahead logs
緩衝數據丟失與不可靠的接收器
零數據損失與可靠的接收器
至少一次語義
緩衝數據丟失與不可靠的接收器
過去的數據丟失與所有的接收器
未定義的語義
Spark 1.2 or later with write ahead logs 零數據損失與可靠的接收器
至少一次語義
零數據丟失與可靠的接收器和文件
至少一次語義

With Kafka Direct API

在Spark 1.3中,我們引入了一個新的 Kafka 直接 API,它可以保證所有的 Kafka 數據被 Spark 流一次接收。與此同時,如果您實現精確一次輸出操作,則可以實現端到端的精確一次輸出保證。Kafka集成指南進一步討論了這種方法。

Semantics of output operations

輸出操作(如foreachRDD)至少具有一次語義,也就是說,在工作者失敗的情況下,轉換後的數據可能被多次寫入外部實體。雖然這對於使用 saveAs***Files 操作將文件保存到文件系統是可以接受的(因爲文件將被相同的數據覆蓋),但是爲了實現精確的一次語義,可能需要額外的工作。有兩種方法。

  • 冪等更新:多次嘗試總是寫入相同的數據。例如,saveAs***文件總是將相同的數據寫入生成的文件。
  • 事務性更新:所有的更新都是通過事務方式進行的,這樣就可以原子性地進行一次更新。一種方法是這樣的。
    • 使用批處理時間(在 foreachRDD 中可用)和 RDD 的分區索引來創建標識符。這個標識符惟一地標識流應用程序中的blob數據。
    • 使用標識符以事務方式(即原子方式)更新外部系統。也就是說,如果還沒有提交標識符,就自動提交分區數據和標識符。否則,如果已經提交,則跳過更新。
dstream.foreachRDD { (rdd, time) =>
  rdd.foreachPartition { partitionIterator =>
    val partitionId = TaskContext.get.partitionId()
    val uniqueId = generateUniqueId(time.milliseconds, partitionId)
    // use this uniqueId to transactionally commit the data in partitionIterator
  }
}

 

原文地址:http://spark.apache.org/docs/2.1.1/streaming-programming-guide.html#initializing-streamingcontext

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