一、概述
Spark Streaming是基于Core Spark API的可擴展,高吞吐量,并具有容錯能力的用于處理實時數據流的一個組件。Spark Streaming可以接收各種數據源傳遞來的數據,比如Kafka, Flume, Kinesis或者TCP等,對接收到的數據還可以使用一些用高階函數(比如map, reduce, join
及window
上圖展示了Spark Streaming的整體數據流轉情況。在Spark Streaming中的處理過程可以參考下圖,Spark Streaming接收實時數據,然后把這些數據分割成一個個batch,然后通過Spark Engine分別處理每一個batch并輸出。
Spark Streaming中一個最重要的概念是DStream,即離散化數據流(discretized stream),DStream由一系列連續的數據集組成。DStream的創建有兩種辦法,一種是從數據源接收數據生成初始DStream,另一種是由DStream A通過轉換生成DStream B。一個DStream實質上是由一系列的RDDs組成。本文介紹了如何基于DStream寫出Spark Streaming程序。Spark Streaming提供了Scala, Java以及Python接口,在官方文檔中對這三種語言都有示例程序的實現,在這里只分析Scala寫的程序。
二、示例程序
在深入分析Spark Streaming的特性和原理之前,以寫一個簡單的Spark Streaming程序并運行起來為入口先了解一些相關的基礎知識。這個示例程序從TCP socket中接收數據,進行Word Count操作。
1. Streaming程序編寫
首先需要導入Spark Streaming相關的類,其中StreamingContext是所有Streaming程序的主要入口。接下來的代碼中創建一個local StreamingContext,batch時間為1秒,execution線程數為2。
import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3
// 創建一個local StreamingContext batch時間為1秒,execution線程數為2
// master的線程數數最少為2,后面會詳細解釋
val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, econds(1))
使用上面這個ssc對象,就可以創建一個lines變量用來表示從TCP接收的數據流了,指定機器名為localhost端口號為9999
// 創建一個連接到hostname:port的DStream, 下面代碼中使用的是
localhost:9999
val lines = ssc.socketTextStream("localhost", 9999)
lines中的每一條記錄都是TCP中的一行文本信息。接下來,使用空格將每一行語句進行分割。
// 將每一行分割成單詞
val words = lines.flatMap(_.split(" "))
上面使用的flatMap操作是一個一對多的DStream操作,在這里表示的是每輸入一行記錄,會根據空格生成多個單詞,這些單詞形成一個新的DStream words。接下來統計單詞個數。
import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3
// 統計每個batch中的不同單詞個數
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)
// 打印出其中前10個單詞出現的次數
wordCounts.print()
上面代碼中,將每一個單詞使用map方法映射成(word, 1)的形式,即paris變量。然后調用reduceByKey方法,將相同單詞出現的次數進行疊加,最終打印出統計的結果。
寫完上面的代碼,Spark Streaming程序還沒有運行起來,需要寫入以下兩行代碼使Spark Streaming程序能夠真正的開始執行。
ssc.start() // 開始計算
ssc.awaitTermination() // 等待計算結束
2. TCP發送數據并運行Spark Streaming程序
(1)運行Netcat 使用以下命令啟動一個Netcat
nc -lk 9999
接下來就可以在命令行中輸入任意語句了。
(2)運行Spark Streaming程序
./bin/run-example streaming.NetworkWordCount localhost 9999
程序運行起來后Netcat中輸入的任何語句,都會被統計每個單詞出現的次數,例如三、基本概念
這一部分詳細介紹Spark Streaming中的基本概念。
1. 依賴配置
Spark Streaming相關jar包的依賴也可以使用Maven來管理,寫一個Spark Streaming程序的時候,需要將下面的內容寫入到Maven項目中
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
<version>2.0.0</version>
</dependency>
對于從Kafka,Flume,Kinesis這些數據源接收數據的情況,Spark Streaming core API中不提供這些類和接口,需要添加下面這些依賴。
2. 初始化StreamingContext
Spark Streaming程序的主要入口是一個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是當前應用的名稱,可以在Cluster UI上進行顯示。master是Spark的運行模式,可以參考 Spark, Mesos or YARN cluster URL,或者設置成local[*]的形式在本地模式下運行。在生產環境中運行Streaming應用時,一般不會將master參數寫死在代碼中,而是在使用spark-submit
命令提交時動態傳入--master參數,具體可以參考 launch the application with spark-submit 。至于batch時間間隔的設置,需要綜合考慮程序的性能要求以及集群可提供的資源情況。也可以基于SparkContext對象,生成一個StreamingContext對象,使用如下代碼
import org.apache.spark.streaming._
val sc = ... // 已有的SparkContext對象
val ssc = new StreamingContext(sc, Seconds(1))
當context初始化后,還需要做的工作有:
- 根據數據源類型生成輸入DStreams
- 通過調用transformation以及輸出操作處理輸入的DStreams
- 使用代碼streamingContext.start()啟動程序,開始接收并處理數據
- 使用代碼streamingContext.awaitTermination()等待程序運行終止(包括手動停止,或者遇到Error后退出應用)
- 可以使用streamingContext.stop()手動停止應用
需要注意的點:
- 當一個context開始運行后,不能再往其中添加新的計算邏輯
- 當一個context被停止后,不能restart
- 在一個JVM中只能同時有一個StreamingContext
- 對象處于運行狀態StreamingContext中的stop()方法同樣會終止SparkContext。如果只需要停止StreamingContext,將stop()
方法的可選參數設置成false,避免SparkContext被終止 - 一個SparkContext對象,可以用于構造多個StreamingContext
對象,只要在新的StreamingContext對象被創建前,舊的treamingContext對象被停止即可。
3. 離散化數據流(Discretized Streams, DStreams)
DStream是Spark Streaming中最基本最重要的一個抽象概念。DStream由一系列的數據組成,這些數據既可以是從數據源接收到的數據,也可以是從數據源接收到的數據經過transform操作轉換后的數據。從本質上來說一個DStream是由一系列連續的RDDs組成,Stream中的每一個RDD包含了一個batch的數據。DStream上的每一個操作,最終都反應到了底層的RDDs上。比如,在前面那個Word Count代碼中將lines轉化成words的邏輯,lines上的flatMap操作就以下圖中所示的形式,作用到了每一個底層的RDD
這些底層RDDs上的轉換操作會有Spark Engine進行計算。對于開發者來說,DStream提供了一個更方便使用的高階API,從而開發者無需過多的關注每一個轉換操作的細節。DStream上可以執行的操作后續文章中會有進一步的介紹。
4. 輸入和接收DStream
(1)基本數據源
在前面Word Count的示例程序中,已經使用到了ssc.socketTextStream(...),這個會根據TCP socket中接收到的數據創建一個DStream。除了sockets之外,StreamingContext API還支持以文件為數據源生成DStream。
- 文件數據源:如果需要從文件系統,比如HDFS,S3,NFS等中接收數據,可以使用以下代碼
streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory)
Spark Streaming程序會監控用戶輸入的dataDirectory路徑,接收并處理該路徑中的所有文件,不過不支持子文件夾中的文件。 需要注意的地方有:
a、所有的文件數據格式必須相同 。
b、該路徑下的文件應該是原子性的移動到該路徑,或者重命名到該路徑。
c、文件進入該路徑后不可再發生變化,所以這種數據源不支持數據連續寫入的形式對于簡單的text文件,有一個簡單的StreamingContext.textFileStream(dataDirectory)方法來進行處理。并且文件數據源的形式不需要運行一個receiver進程,所以對Execution的核數沒有要求。
- 基于自定義Receiver的數據源:DStream也支持從用戶自定義Receivers中讀取數據。
- RDDs序列數據源:使用streamingContext.queueStream(queueOfRDDs)
,可以將一系列的RDDs轉化成一個DStream。該queue中的每一個RDD會被當做DStream
中的一個batcn,然后以Streaming的形式處理這些數據。
(2)自定義數據源 除了上面兩類數據源之外,也可以支持自定義數據源。自定義數據源時,需要實現一個可以從自定義數據源接收數據并發送到Spark中的用戶自定義receiver。具體可以參考 Custom Receiver Guide。
(4)數據接收的可靠性
5. DStreams上的Transformations
類似于RDDs,transformations可以使輸入DStream中的數據內容根據特定邏輯發生轉換。DStreams上支持很多RDDs
上相同的一些transformations。具體含義和使用方法可參考另一篇博客:Spark Streaming中的操作函數分析在上面這些transformations中,有一些需要進行進一步的分析
(1)UpdateStateByKey操作
(2)Transform操作
transform操作及其類似的一些transformwith操作,可以使DStream中的元素能夠調用任意的RDD-to-RDD的操作。可以使DStream調用一些只有RDD才有而DStream API沒有提供的算子。例如,DStream PI就不支持一個data DStream中的每一個batch數據可以直接和另外的一個數據集做join操作,但是使用transform就可以實現這一功能。這個操作可以說進一步豐富了DStream的操作功能。再列舉一個這個操作的使用場景,將某處計算到的重復信息與實時數據流中的記錄進行join,然后進行filter操作,可以當做一種數據清理的方法。
val spamInfoRDD = ssc.sparkContext.newAPIHadoopRDD(...) // 一個包含重復信息的RDD
val cleanedDStream = ordCounts.transform(rdd => { rdd.join(spamInfoRDD).filter(...) // 將重復信息與實時數據做join,然后根據指定規則filter,用于數據清洗 ...})
這里需要注意的是,transform傳入的方法是被每一個batch調用的。這樣可以支持在RDD上做一些時變的操作,即RDD,分區數以及廣播變量可以在不同的batch之間發生變化。
(3)Window操作
Spark Streaming提供一類基于窗口的操作,這類操作可以在一個滑動窗口中的數據集上進行一些transformations操作。下圖展示了窗口操作的示例
上圖中,窗口在一個DStream源上滑動,DStream源中暴露在該窗口中的RDDs可以讓這個窗口進行相關的一些操作。在上圖中可以看到,該窗口中任一時刻都只能看到3個RDD,并且這個窗口每2秒中往前滑動一次。這里提到的兩個參數,正好是任意一個窗口操作都必須指定的。
- 窗口長度:例如上圖中,窗口長度為3
- 滑動間隔:指窗口多長時間往前滑動一次,上圖中為2。
需要注意的一點是,上面這兩個參數,必須是batch時間的整數倍,上圖中的batch時間為1。
接下來展示一個簡單的窗口操作示例。比如說,在前面那個word count示例程序的基礎上,我希望每隔10秒鐘就統計一下當前30秒時間內的每個單詞出現的次數。這一功能的簡單描述是,在paris DStream
的當前30秒的數據集上,調用reduceByKey操作進行統計。為了實現這一功能,可以使用窗口操作reduceByKeyAndWindow。
val windowedWordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b), Seconds(30), Seconds(10))
更多的窗口操作可以參考:Spark Streaming中的操作函數分析
6. DStreams上的輸出操作
DStream上的輸出操作,可以使DStream中的數據發送到外部系統,比如數據庫或文件系統中。DStream只有經過輸出操作,其中的數據才能被外部系統使用。并且下面這些輸出操作才真正的觸發DStream對象上調用的transformations操作。這一點類似于RDDs上的Actions算子。輸出操作的使用和功能請參考:Spark Streaming中的操作函數分析
下面主要進一步分析foreachRDD操作往外部數據庫寫入數據的一些注意事項。dstream.foreachRDD是DStream輸出操作中最常用也最重要的一個操作。關于這個操作如何正確高效的使用,下面會列舉出一些使用方法和案例,可以幫助讀者在使用過程中避免踩到一些坑。通常情況下,如果想把數據寫入到某個外部系統中時,需要為之創建一個連接對象(比如提供一個TCP連接工具用于連接遠程服務器),使用這個連接工具才能將數據發送到遠程系統。在Spark Streaming中,開發者很可能會在Driver端創建這個對象,然后又去Worker端使用這個對象處理記錄。比如下面這個例子
dstream.foreachRDD { rdd => val connection = createNewConnection() // 在driver端執行 rdd.foreach { record => connection.send(record) // 在wroker端執行 }}
上面這個使用方法其實是錯誤的,當在driver端創建這個連接對象后,需要將這個連接對象序列化并發送到wroker端。通常情況下,連接對象都是不可傳輸的,即wroker端無法獲取該連接對象,當然也就無法將記錄通過這個連接對象發送出去了。這種情況下,應用系統的報錯提示可能是序列化錯誤(連接對象無法序列化),或者初始化錯誤(連接對象需要在wroker端完成初始化),等等。正確的做法是在worker端創建這個連接對象。但是,即使是在worker創建這個對象,又可能會犯以下錯誤。
dstream.foreachRDD { rdd => rdd.foreach { record => val connection = createNewConnection() connection.send(record) connection.close() }}
上面代碼會為每一條記錄創建一個連接對象,導致連接對象太多。 連接對象的創建個數會受到時間和系統資源情況的限制,因此為每一條記錄都創建一個連接對象會導致系統出現不必要的高負載,進一步導致系統吞吐量降低。 一個好的辦法是使用rdd.foreachPartition
操作,然后為RDD的每一個partition
,使一個partition
中的記錄使用同一個連接對象。如下面代碼所示
dstream.foreachRDD { rdd => rdd.foreachPartition {
partitionOfRecords => val connection = createNewConnection()
partitionOfRecords.foreach(
record => connection.send(record)
) connection.close() }
}
最后,可以通過使用連接對象池進一步對上面的代碼進行優化。使用連接對象池可以進一步提高連接對象的使用效率,使得多個RDDs/batches之間可以重復使用連接對象。
dstream.foreachRDD { rdd => rdd.foreachPartition {
partitionOfRecords => // 連接對象池是靜態的,并且建立對象只有在真正使用時才被創建
val connection = ConnectionPool.getConnection()
partitionOfRecords.foreach(record => connection.send(record))
ConnectionPool.returnConnection(connection) // 使用完之后,將連接對象歸還到池中以便下一次使用 }}
需要注意的是,連接對象池中的對象最好設置成懶生成模式,即在真正使用時才去創建連接對象,并且給連接對象設置一個生命周期,一定時間不使用則注銷該連接對象。
總結一下關鍵點:
DStreams的transformations操作是由輸出操作觸發的,類似于RDDs中的actions操作。上面列舉出某些DStream的輸出操作中可以將其中的元素轉化成RDD,進而可以調用RDD提供的一些API操作,這時如果對RDD調用actions操作會立即強制對接收到的數據進行處理。因此,如果用戶應用程序中DStream不需要任何的輸出操作,或者僅僅對DStream使用一些類似于dstream.foreachRDD操作但是在這個操作中不調用任何的RDD action操作時,程序是不會進行任何實際運算的。系統只會簡單的接收數據,任何丟棄數據。默認情況下,輸出操作是順序執行的。
7. 累加器和廣播變量
Spark Streaming的累加器和廣播變量無法從checkpoint
恢復。如果在應用中既使用到checkpoint又使用了累加器和廣播變量的話,最好對累加器和廣播變量做懶實例化操作,這樣才可以使累加器和廣播變量在driver失敗重啟時能夠重新實例化。參考下面這段代碼
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: Accumulator[Long] = null
def getInstance(sc: SparkContext): Accumulator[Long] =
{
if (instance == null)
{
synchronized
{
if (instance == null)
{ instance = sc.accumulator(0L, "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 += count false } else { true } }.collect() val output = "Counts at time " + time + " " + counts})
查看完整代碼請移步 source code
8. DataFrame和SQL操作
在streaming數據上也可以很方便的使用到DataFrames和SQL操作。為了支持這種操作,需要用StreamingContext對象使用的SparkContext對象初始化一個SQLContext對象出來,SQLContext
對象設置成一個懶初始化的單例對象。下面代碼對前面的Word Count進行一些修改,通過使用DataFrames和SQL來實現Word Count的功能。每一個RDD都被轉化成一個DataFrame對象,然后注冊成一個臨時表,最后就可以在這個臨時表上進行SQL查詢了。
val words: DStream[String] = ...
words.foreachRDD { rdd =>
// 獲取單例SQLContext對象 val sqlContext = SQLContext.getOrCreate(rdd.sparkContext)
import sqlContext.implicits._
// 將RDD[String]轉化成DataFrame val wordsDataFrame = rdd.toDF("word")
// 注冊表
wordsDataFrame.registerTempTable("words")
// 在該臨時表上執行sql語句操作
val wordCountsDataFrame =
sqlContext.sql("select word, count(*) as total from words group by word") wordCountsDataFrame.show()}
查看完整代碼請移步 source code. 也可以在另一線程獲取到的Streaming數據上進行SQL操作(這里涉及到異步運行StreamingContext)。StreamingContext對象無法感知到異步SQL查詢的存在,因此有StreamingContext對象有可能在SQL查詢完成之前把歷史數據刪除掉。為了保證StreamingContext
不刪除需要用到的歷史數據,需要告訴StreamingContext保留一定量的歷史數據。例如,如果你想在某一個batch的數據上執行SQL查詢操作,但是你這個SQL需要執行5分鐘的時間,那么,需要執行streamingContext.remember(Minutes(5))語句告訴StreamingContext
將歷史數據保留5分鐘。有關DataFrames的更多介紹,參考另一篇博客:Spark-SQL之DataFrame操作大全
9. 緩存和持久化
類似于RDDs,DStreams也允許開發者將stream中的數據持久化到內存中。在DStream對象上使用persist()方法會將DStream對象中的每一個RDD自動持久化到內存中。這個功能在某個DStream的數據需要進行多次計算時特別有用。對于窗口操作比如reduceByWindow,以及涉及到狀態的操作比如updateStateByKey,默認會對DStream
對象執行持久化。因此,程序在運行時會自動將窗口操作和涉及到狀態的這些操作生成的DStream對象持久化到內存中,不需要開發者顯示的執行persist()操作。對那些通過網絡接收到的streams數據(比如Kafka, Flume, Socket等),默認的持久化等級是將數據持久化到兩個節點上,以保證其容錯能力。
注意,不同于RDDs,默認情況下DStream的持久化等級是將數據序列化保存在內存中。這一特性會在后面的性能調優中進一步分析。有關持久化級別的介紹,可以參考rdd-persistence
10. 檢查點
當Streaming應用運行起來時,基本上需要7 * 24的處于運行狀態,所以需要有一定的容錯能力。檢查點的設置就是能夠支持Streaming應用程序快速的從失敗狀態進行恢復的。檢查點保存的數據主要有兩種:
1 . 元數據(Metadata)檢查點:保存Streaming應用程序的定義信息。主要用于恢復運行Streaming應用程序的driver節點上的應用。元數據包括:
a、配置信息:創建Streaming應用程序的配置信息
b、DStream操作:在DStream上進行的一系列操作方法
c、未處理的batch:記錄進入等待隊列但是還未處理完成的批次
2 . 數據(Data)檢查點:將計算得到的RDD保存起來。在一些跨批次計算并保存狀態的操作時,必須設置檢查點。因為在這些操作中基于其他批次數據計算得到的RDDs,隨著時間的推移,計算鏈路會越來越長,如果發生錯誤重算的代價會特別高。元數據檢查點信息主要用于恢復driver端的失敗,數據檢查點主要用于計算的恢復。
(1)什么時候需要使用檢查點
當應用程序出現以下兩種情況時,需要配置檢查點。 -
- 使用到狀態相關的操作算子-比如updateStateByKey或者reduceByKeyAndWindow等,這種情況下必須為應用程序設置檢查點,用于定期的對RDD進行檢查點設置。
- Driver端應用程序恢復-當應用程序從失敗狀態恢復時,需要從檢查點中讀取相關元數據信息。
(2)檢查點設置
一般是在具有容錯能力,高可靠的文件系統上(比如HDFS, S3等)設置一個檢查點路徑,用于保存檢查點數據。設置檢查點可以在應用程序中使用streamingContext.checkpoint(checkpointDirectory)來指定路徑。
如果想要應用程序在失敗重啟時使用到檢查點存儲的元數據信息,需要應用程序具有以下兩個特性,需要使用StreamingContext.getOrCreate代碼在失敗時重新創建StreamingContext
對象:
- 當應用程序是第一次運行時,創建一個新的StreamingContext
對象,然后開始執行程序處理DStream。 - 當應用程序失敗重啟時,可以從設置的檢查點路徑獲取元數據信息,創建一個StreamingContext對象,并恢復到失敗前的狀態。
下面用Scala代碼實現上面的要求。
def functionToCreateContext(): StreamingContext = {
val ssc = new StreamingContext(...) // 創建一個新的StreamingContext對象
val lines = ssc.socketTextStream(...) // 得到DStreams ...
ssc.checkpoint(checkpointDirectory) // 設置checkpoint路徑 ssc}
// 用checkpoint元數據創建StreamingContext對象或根據上面的函數創建新的對象
val context = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext _)
// 設置context的其他參數
context. ...
// 啟動
contextcontext.start()context.awaitTermination()
如果checkpointDirectory路徑存在,會使用檢查點元數據恢復一個StreamingContext對象。如果路徑不存在,或者程序是第一次運行,則會使用functionToCreateContext來創建一個新的StreamingContext對象。
需要注意的是,想要用到上面的getOrCreate功能,需要在應用程序運行時使其支持失敗自動重跑的功能。這一功能,在接下來一節中有分析。
另外,在往檢查點寫入數據這一過程,是會增加系統負荷的。因此,需要合理的設置寫入檢查點數據的時間間隔。對于小批量時間間隔(比如1秒)的應用,如果每一個batch都執行檢查點寫入操作,會顯著的降低系統的吞吐性能。相反的,如果寫入檢查點數據間隔太久,會導致lineage過長。對那些狀態相關的需要對RDD進行檢查點寫入的算子,檢查點寫入時間間隔最好設置成batch時間間隔的整數倍。比如對于1秒的batch間隔,設置成10秒。有關檢查點時間間隔,可以使用dstream.checkpoint(checkpointInterval)。一般來說,檢查點時間間隔設置成5~10倍滑動時間間隔是比較合理的。
11. 部署應用程序
這一節主要討論如何將一個Spark Streaming應用程序部署起來。
(1)需求
運行一個Spark Streaming應用程序,需要滿足一下要求。
需要有一個具有集群管理器的集群 - 可以參考Spark應用部署文檔
應用程序打成JAR包 - 需要將應用程序打成JAR包。接下來使用spark-submit命令來運行應用程序的話,在該JAR包中無需打入Spark和Spark Streaming相關JAR包。然而,如果應用程序中使用到了比如Kafka或者Flume等高階數據源的話,需要將這些依賴的JAR包,以及這些依賴進一步的依賴都打入到應用JAR包中。比如,應用中使用到了KafkaUtils
的話,需要將spark-streaming-kafka-0.8_2.11
以及其依賴都打入到應用程序JAR包中。為Executor設置足夠的內存 - 由于接收到的數據必須保存在內存中,必須為Executor設置足夠的內存能容納這些接收到的數據。注意,如果在應用程序中做了10分鐘長度的窗口操作,系統會保存最少10分鐘的數據在內存中。所以應用程序需要的內存除了由接收的數據決定之外,還需要考慮應用程序中的操作類型。
設置檢查點 - 如果應用程序需要用到檢查點,則需要在文件存儲系統上設置好檢查點路徑。
為應用程序的Driver設置自動重啟 - 為了實現driver失敗后自動重啟的功能,應用程序運行的系統必須能夠監控driver進程,并且如果發現driver失敗時能夠自動重啟應用。不同的集群使用不同的方式實現自動重啟功能。
1.Spark Standalone - 在這種模式下,driver程序運行在某個wroker節點上。并且,Standalone集群管理器會監控driver程序,如果發現driver停止運行,并且其狀態碼為非零值或者由于運行driver程序的節點失敗導致driver失敗,就會自動重啟該應用。具體的監控和失敗重啟可以進一步參考Spark Standalone guide
2.YARN - Yarn支持類似的自動重啟應用的機制。更多的細節可以進一步參考YARN的相關文檔
3.Mesos - Mesos使用Marathon實現了自動重啟功能設置write ahead logs - 從Spark-1.2版本開始,引入了一個write head log機制來實現容錯。如果設置了WAL功能,所有接收到的數據會寫入write ahead log中。這樣可以避免driver重啟時出現數據丟失,因此可以保證數據的零丟失,這一點可以參考前面有關介紹。通過將spark.streaming.receiver.writeAheadLog.enable=true來開啟這一功能。然而,這一功能的開啟會降低數據接收的吞吐量。這是可以通過同時并行運行多個接收進程(這一點在后面的性能調優部分會有介紹)進行來抵消該負面影響。另外,如果已經設置了輸入數據流的存儲級別為Storagelevel.MEMORY_AND_DISK_SET,由于接收到的數據已經會在文件系統上保存一份,這樣就可以關閉WAL功能了。當使用S3以及其他任何不支持flushng功能的文件系統來write ahead logs時,要記得設置spark.streaming.driver.writeAheadLog.closeFileAfterWrite以及spark.streaming.receiver.writeAheadLog.closeFileAfterWrite兩個參數。
設置Spark Streaming最大數據接收率 - 如果運行Streaming應用程序的資源不是很多,數據處理能力跟不上接收數據的速率,可以為應用程序設置一個每秒最大接收記錄數進行限制。對于Receiver模式的應用,設置spark.streaming.receiver.maxRate,對于Direct Kafka模式,設置spark.streaming.kafka.maxRatePerPartition限制從每個Kafka的分區讀取數據的速率。假如某個Topic有8個分區,spark.streaming.kafka.maxRatePerpartition=100,那么每個batch最大接收記錄為800。從Spark-1.5版本開始,引入了一個backpressure
的機制來避免設置這個限制閾值。Spark Streaming會自動算出當前的速率限制,并且動態調整這個閾值。通過將spark.streaming.backpressure.enabled為true開啟backpressure功能。
(2)升級應用代碼
如果運行中的應用程序有更新,需要運行更新后的代碼,有以下兩種機制。
- 升級后的應用程序直接啟動,與現有的應用程序并行執行。在新舊應用并行運行的過程中,會接收和處理一部分相同的數據。
- Gracefully停掉正在運行的應用,然后啟動升級后的應用程序,新的應用程序會從舊的應用程序停止處開始繼續處理數據。需要注意的是,使用這種方式,需要其數據源具有緩存數據的能力,否則在新舊應用程序銜接的間歇期內,數據無法被處理。比如Kafka和Flume都具有數據緩存的能力。并且,在這種情況下,再從舊應用程序的檢查點重新構造SparkStreamingContext
對象不再合適,因為檢查點中的信息可能不包含更新的代碼邏輯,這樣會導致程序出現錯誤。在這種情況下,要么重新指定一個檢查點,要么刪除之前的檢查點。
13. 監控應用程序
在Spark Streaming應用程序運行時,Spark Web UI頁面上會多出一個Streaming的選項卡,在這里面可以顯示一些Streaming相關的參數,比如Receiver是否在運行,接收了多少記錄,處理了多少記錄等。以及Batch相關的信息,包括batch的執行時間,等待時間,完成的batch數,運行中的batch數等等。這里面有兩個時間參數需要注意理解一些:
1)Processing Time - 每一個batch中數據的處理時間
2)Scheduling Delay - 當前batch從進入隊列到開始執行的延遲時間
如果處理時間一直比batch時間跨度要長,或者延遲時間逐漸增長,表示系統已經無法處理當前的數據量了,這時候就需要考慮如何去降低每一個batch的處理時間。如何降低batch處理時間,可以參考第四節。
除了監控頁面之外,Spark還提供了StreamingListener接口,通過這個接口可以獲取到receiver以及batch的處理時間等信息。