Spark Streaming 指南--Spark2.4.3


本篇所述翻譯自Spark2.4.3版本的Spark Streaming Guide官方文檔,是自己在學習期間發現網上資料包括書籍都混亂不堪因而感覺直接看官方文檔比較靠譜,而很多同學英語水平受限所以就將其翻譯下來供直接使用。注意這其中實際僅僅包含Spark Streaming如何使用即how而沒有why。

總覽

Spark Streaming建立在Spark API的基礎之上,提供穩定,高吞吐量和故障恢復的數據流處理操作。Spark Streaming接受來自Kafka,Flume,Kinesis或者TCP套接字的數據源,採用map,reduce,join,window高級方法進行處理。最終處理的結果數據寫入文件系統,數據庫或者顯示到dashboard。實際上,也可以在數據流上應用Spark的機器學習圖處理算法。
在這裏插入圖片描述
內部流程實際是Spark Streaming不斷接受輸入數據流並且將數據分成批次,一批一批的數據之後由Spark引擎進行處理得到最終的多批次的結果數據。
在這裏插入圖片描述
Spark Streaming提供了高級抽象,離散stream或者說DStream,他代表的是連續的數據流。DStream由來自Kafka,Flume,Knesis等數據源的輸入數據或者其他DStream創建,而DStream內部是由一系列RDD組成。也就是說一個DStream對象代表着是這個數據流,而其中一個批次的數據表示成了一個RDD,作爲該DStream對象中的一個RDD元素。
這個指南想要說明如何藉助DStream編寫Spark Streaming程序。支持Python,Scala,Java編程語言(這裏只寫Python)Python在Spark1.2才引入的。

快速入門

在深入Spark Streaming程序細節之前,應該先快速看一看Spaark Streaming程序的樣貌。這裏想要計數數據流中的單詞數量,數據來源於TCP套接字。
首先導入StreamingContext,它是所有流處理函數的中心類,我們創建了一個本地的StreamingContext,有兩個線程,批次時間間隔是1秒。

from pyspark import SparkContext
from pyspark.streaming import StreamingContext

# 創建本地StreamingContext,2個線程,1s的時間間隔
sc = SparkContext("local[2]", "NetworkWordCount")
ssc = StreamingContext(sc, 1)

有了上下文環境就能創建表示流數據的DStream對象,接受TCP套接字數據,指定TCP套接字的主機名和端口號。

# 創建DStream,連接到本機的9999端口
lines = ssc.socketTextStream("localhost", 9999)

lines就是一個DStream對象,代表流數據。DStream中的一條記錄就是一行文本,之後想將文本行分解成單詞。

#將每行分解成爲多個單詞
words = lines.flatMap(lambda line:line.split(" "))

flatMap方法是一個一對多的DStream操作,從源DStream的每條記錄生成多個新的紀錄。這裏每一行分解成多個單詞,單詞流數據表示爲words 對象。接下來,需要計數單詞出現的次數。

#計數每一批次的每個單詞
pairs = words.map(lambda word:(word,1))
wordCount = pairs.reduceByKey(lambda x,y:x+y)

#打印每個RDD中的前10個元素
wordCount.pprint()

words對象通過一對一的map轉換爲(word,1)對DStream,之後通過reduce得到每個批次中單詞出現的頻率,最終wordCounts.pprint()打印每秒中計數情況。
到此爲止還沒真正執行流處理的進程,要開啓所有的轉換操作需要

ssc.start()                        #開啓計算進程
ssc.awaitTermination()    #等待計算終止

Spark Streaming例子的完整代碼在NetworkWordCount.
如果已經下載並安裝了Spark,就可以運行這個例子了。需要先運行Netcat來提供數據

$ nc -lk 9999

然後運行示例

$ ./bin/spark-submit examples/src/main/python/streaming/network_wordcount.py localhost 9999

打印結果

# TERMINAL 2: RUNNING network_wordcount.py

$ ./bin/spark-submit examples/src/main/python/streaming/network_wordcount.py localhost 9999
...
-------------------------------------------
Time: 2014-10-14 15:25:21
-------------------------------------------
(hello,1)
(world,1)
...

基本概念

接下來,揭示Spark Streaming基礎概念

庫依賴

類似於Spark,Spark Streaming也可以從Maven Central獲取。要編寫Spark Streaming 程序,需要將這個依賴加入到SBT或者Maven項目中

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming_2.10</artifactId>
    <version>1.3.1</version>
</dependency>

要接受來自Kafka,Flume,Kinesis這些不被包含在Spark Streaming核心API中的源,需要將對應的artifact spark-streaming-xyz_2.12加到依賴中,例如

Source Artifact
Kafka spark-streaming-kafka-0-10_2.12
Flume spark-streaming-flume_2.12
Kinesis spark-streaming-kinesis-asi_2.12[Amazon Software License]

最先列表請訪問Maven repository

初始化StreamingContext

要初始化Spark Streaming程序,必須創建StreamingContext,它是Spark Streaming功能的核心
StreamingContext對象是由SparkContext對象創建而來的。

from pyspark import SparkContext
from pyspark.streaming import StreamingContext

sc = SparkContext(master, appName)
ssc = StreamingContext(sc, 1)

appName參數代表在集羣UI中展示的應用名稱。master參數可以是Spark,Mesos或者YARN集羣URL或者“local[*]”字符串來在本地運行。實際上真在集羣中運行的時候不會寫死master參數而是通過spark-submit啓動參數來識別。這裏就在本地運行了,直接用local[*]來運行Spark Streaming處理程序。
批次間隔時間符合應用程序的需求和集羣情況。在性能調優細緻講解。
上下文定義之後必須:

  1. 通過創建輸入DStream來定義數據源
  2. 通過在DStream上使用轉換和輸出操作處理流數據
  3. 通過streamingContext.start()來開始接收數據並處理
  4. 通過streamingContext.awaitTermination()來終止處理(手動終止或由於錯誤)
  5. 採用streamingContext.stop()來手動停止處理

注意:

  • 一旦上下文開啓,就不能有新的流計算操作加入進去
  • 一旦上下文停止,就不能重啓
  • 一個JVM中一個時刻只能有一個StreamingContext運行
  • stop()終止了StreamingContext也會終止SparkContext.要想終止前者而不終止後者需要在stop()設置stopSparkContext爲false
  • 一個SparkContext只要終止前一個StreamingContext就能用來複用創建下一個StreamingContext。

離散Streams(DStreams)

離散Stream或DStream是由Spark Streaming提供的基礎抽象類。表示連續的數據流,來源於輸入數據或對數據流進行轉換操作而來。DStream內部由一系列RDD組成。一個DStream中的每個RDD包含某個時間段內的數據:
在這裏插入圖片描述
應用在DStream上的任何操作都會轉爲對內部RDD的操作。例如前面的將文本行數據流轉換爲單詞數據流,flatMap操作就應用在lines DStream對象中的每個RDD從而生成了words DStream中的RDD。例如:
在這裏插入圖片描述
內部的RDD transformation由Spark 引擎執行。DStream操作隱藏了大部分細節,爲開發者提供高層抽象。這些操作後面詳細討論。

輸入DStreams和接收器

輸入DStream表示由原始數據創建的DStream,在前面的例子中lines就是這樣的。每一個輸入DStream都和一個Receiver對象關聯,它接收數據並將其存入Spark內存中。
Spark Streaming提供兩類內置的流數據源:

  • 基礎數據源:直接從StreamingContext API可以得到的數據源對象。例如文件系統數據源,socket連接和Akka actors。
  • 高級數據源:通過外部工具類獲得的Kafka,Flume,Kinusis等。這要求依賴一節中那些額外的依賴。

接下來討論這些數據源API。
如果你想並行接受多個數據源的數據,可以創建多個DStream。這會同時創建多個接收器來同時接受數據。注意Spark worker/executor是一個長期運行的任務,他需要佔據分配給Spark Streaming應用的一個core。因此確保分配了足夠的core或者線程來處理接受的數據。
記住:

  • 當在本地運行Spark Streaming程序的時候,不要使用將local或者local[1]作爲master URL。因爲這都是意味着只有一個線程來運行本地任務。如果基於接收器來運行輸入DStream,則該線程只能用來運行receiver,沒有線程處理接受到的數據了。所以在本地運行時一直使用local[n],n>接收器的數量纔能有線程處理數據。
  • 集羣中運行要保證分配的核心數大於接收器的數量,否則只能接受數據無法處理

基本數據源

入門實例中使用ssc.socketTextStream(…)來從TCP socket連接接收的數據上建立DStream。除了socket,StreamingContext API提供方法從文件系統輸入源接收數據。

文件流

要從任意兼容HDFS的文件系統(HDFS,S3,NFS等)讀取數據,一個DStream可以通過StreamingContext.fileStream[KeyClass,ValueClass,InputFormatClass]來創建。
文件流並不需要運行接收器因爲不需要分配任何core來接收文件數據。
對於簡單的文本文件,最簡單的方法是StreamingContext.textFileStream(dataDirectory).
Python API不支持fileStream,只有textFileStream可以使用

streamingContext.textFileStream(dataDirectory)
如何監控目錄

Spark Streaming監控dataDirectory目錄並且處理該目錄中的任何文件:

  • 簡單的目錄"hdfs://namenode:8040/logs"會被監控,這個目錄中的一級文件都可以處理。
  • 現在也支持像"hdfs://namenode:8040/logs/2017/*"這樣的POSIX glob模式。DStream會包含匹配該模式的目錄中的所有文件。也就是說這是目錄的pattern不是文件的啊。
  • 所有文件必須是相同格式
  • 所有文件必須有修改時間而非創建時間
  • 一旦開始處理那麼在當前窗口期內對該文件所做的改變不會導致重讀,也就是忽略文件更新
  • 目錄中目錄越多,掃描文件更新的時間越長–雖然可能沒啥更新
  • 如果使用通配符定義目錄,例如"hdfs://namenode:8040/logs/2016-*"那麼將一個目錄重命名匹配上了該格式則該目錄也會被監控。只有當該目錄修改時間在當前窗口期之內,該目錄中的文件纔會包含在流中。
  • 調用FileSystem.setTimes()來修改文件時間戳可以使該文件包含在後期的窗口期內,即使文件沒啥改變這也行啊
使用對象存儲作爲數據源

像HDFS這樣的文件系統只要輸出流創建了那麼就會設置文件的修改時間。當文件打開了,即使這是在文件數據完全寫入之前,這個文件也會被包含在DStream中處理–當前窗口期之後寫入文件的內容會被忽略。也就是忽略更新的文件數據。
要想窗口期內也能獲取文件的改變,可以將文件移進未被監控的目錄中,然後在輸出流關閉之後立刻重命名那個目錄就可以了。該重命名文件在該目錄對應的那個窗口期內就可以被掃描到了,從而新的數據也會被處理。
與此相反,像Amazon S3或Azure Storage這樣的對象存儲通常重命名操作很慢,因爲涉及到數據拷貝。而且重命名對象可能會把rename()操作的時間當作其修改時間,所以可能不會被考慮進窗口期內。
需要仔細測試來判斷存儲文件的時間戳行爲滿不滿足Spark Streaming的要求。相比於對象存儲,直接把數據寫入目的目錄比較恰當。
查看Hadoop Filesystem細則瞭解更多細節。

基於自定義接收器的流

可以自定義接收器來接收流數據創建DStream。查看Custom Receiver Guide瞭解細節。

將RDD隊列作爲流數據

如果是爲了測試Spark Streaming應用程序,可以通過RDD隊列,使用streamingContext.queueStrearm(queueOfRDDs)來創建DStream。隊列中的每個RDD都會作爲一批次數據進行處理。
對於文件和socket流數據的細節,查看Scala的StreamingContext,Java的JavaStreamingContext和Python的StreamingContext的相關函數的文檔API。

高級數據源

在Spark2.4.3中,除了這些源之外,Kafka,Kinesis和Flume都支持Python了。
這類源需要非Spark的外部庫接口,其中一些伴隨着複雜的依賴關係。因此,爲了最小化版本衝突的問題,從這些源來創建DStream的方法分離出去成了顯式的庫了,必要的時候再連接。
注意這些高級數據源接口不能在Spark Shell中使用,所以如果想在Spark Shell中測試需要先下載對應的Maven JAR包以及其依賴庫並放在class path下。
高級源包括:

自定義源(Python不支持)

輸入DStream的數據源接口也可以是用戶自定義的,只是需要實現用戶自定義的receiver,從而接收數據並將其放入Spark。查看Custom Receiver Guide瞭解細節。

Receiver的可靠性

基於可靠性,數據源可以分爲兩種。例如Kafka和Flume的源允許acknowledge遷移數據。如果系統從這些可靠數據源正確接收數據則保證不會因爲錯誤而丟失數據。有兩種接收器:

  1. 可靠的接收器:可靠的接收器ack可靠的數據源,前提是數據被接收並被存入Spark副本。
  2. 不可靠的接收器:這是一些不支持ack數據源的接收器。即使對於可靠的數據源,也會有不可靠的接收器,不會有複雜的ack過程。

可靠的接收器細節在Custom Receiver 指南

DStreams上的transformations

類似於RDD,DStream的transformation也會改變數據。DStream支持許多種treasformation,他們也被RDD支持:

Trasformation 意義
map(func) 源DStream中的元素經過func函數生成新的DStream
flatMap(func) 類似於map,但是每個輸入元素會被映射成0個或多個元素輸出
filter(func) 僅僅選擇DStream中符合func函數的元素返回構成新的DStream
repartition(numPartitions) 通過創建更多或更少的分區來改變並行度
union(otherStream) 聯合兩個DStream
count() 返回一個單RDD的DStream,其中表示源DStream中每個RDD中的元素的個數
reduce(func) 返回一個單RDD的DStream,通過func來聚合源DStream中的每個RDD中的元素
countByValue() 當在一個元素類型爲K的DStream調用該方法時,返回元素時(K,Long)對兒,其中key的值代表該key在源DStream中每個RDD中的頻率
reduceByKey(func,[numTasks]) 源DStream的元素是(K,V)對,根據每個key進行reduce,返回(K,V)對DStream。這裏使用Spark默認的並行任務數量(本地執行默認並行度是2,而集羣中根據spark.default.parallelism),可以根據參數numTasks來改變
join(otherStream,[numTasks]) 當在(K,V) 和(K,W)類型的DStream上調用時返回(K,(V,W))對DStream
cogroup(otherStream,[numTasks]) (K,V)和(K,W)的DStream返回(K,seq(V),seq(W))DStream
transform(func) 用於自定義的RDD轉換操作
updateStateByKey(func) 基於key之前的狀態值和新值返回具有新狀態的DStream。這可以用於爲每個key保存任務狀態數據

UpdateStateByKey 操作

updateStateByKey操作使得可以根據新信息連續更新狀態值。這需要兩步:

  1. 定義狀態:狀態可以是任意數據類型
  2. 定義狀態更新函數:指定函數如何基於之前的狀態值和輸入流的新值來更新狀態值。

對於每一批次數據來說,Spark不管該批次中有沒有新數據都會將狀態更新函數應用在所有現存的鍵上。如果更新函數返回None那麼鍵值對就被刪除。
這裏使用例子來說明。假設想一直計數文本數據流中每個單詞的頻率。這裏running Count是狀態值,一個整數值。我們定義更新函數:

def updateFunction(newValues, runningCount):
    if runningCount is None:
       runningCount = 0
    return sum(newValues, runningCount)  #在原有的running count值之上增加新值來生成新值

將其應用到包含words的DStream(也就是說pairs DStream包含(word,1)):

runningCounts = pairs.updateStateByKey(updateFunction)

更新函數被每個word調用。完整的python程序請查閱stateful_network_wordcount.py
注意使用updateStateByKey要求配置檢查點目錄。

transformation操作

transform操作(包括其變體transformWith)允許將任意的RDD-TO-RDD的函數應用到DStream。他可以擴充現在沒有的API。例如,將數據流中每批次的數據和另一個數據集直接join並不在DStream API中。然而可以使用transorm做到。這很強大。例如如果想清洗數據,可以將數據流中的輸入數據和預垃圾信息進行join來基於此進行過濾操作:

spamInfoRDD = sc.pickleFile(...) # 該RDD 中包含垃圾信息

# 將數據與垃圾信息join來過濾
cleanedDStream = wordCounts.transform(lambda rdd: rdd.join(spamInfoRDD).filter(...))

注意提供的函數會在每批次間隔時間內調用。這就可以產生隨着時間變換的RDD操作,例如RDD操作/分區數量/廣播變量在批次間改變。

window操作

Spark Streaming支持窗口計算,可以將transformation應用一個滑動窗口數據上:
在這裏插入圖片描述
正如圖中顯示的,每一時刻窗口在源DStream上滑動,落入窗口中的RDD會組合起來進行操作來生成新的RDD。具體說來,操作是基於最後3個時間單元的,每次滑動2個時間單元。這說明任何窗口操作需要指定兩個參數:

  • window 長度:窗口長度(圖中的3)
  • 滑動間隔:滑動時間間隔(圖中的2)

這兩個參數必須是源DStream的批次間隔的整數倍。
用例子來說明窗口操作。這裏想擴展之前的應用,計算最後30秒的單詞計數,mei10秒爲一個單元。我們需要應用reduceByKey操作在最後30s的(word,1)pairs DStream。這可以採用reduceByKeyAndWindow:

windowedWordCounts = pairs.reduceByKeyAndWindow(lambda x, y: x + y, lambda x, y: x - y, 30, 10)

一些常用的窗口操作如下,所有函數都有兩個參數:window length和slide interval

transforma 意義
window(windowLength,slideInterval) 基於源DStream的批次窗口數據計算新的DStream
countByWindow(windowLength,slideInterval) 返回流中滑動窗口元素計數
reduceByWindow(func,windowLength,slideInterval) 返回單元素流,根據func函數在流中聚合元素。函數需要能夠正確並行運行
reduceByKeyAndWindow(func,windowLength,slideInterval,[numTasks]) 在KV對的DStream調用該函數時會返回新的KV對DStream,其中每個key的值使用給定的reduce函數在滑動窗口涉及到的批次數據中聚合。注意:默認情況下,這使用Spark默認的並行任務數(本地模式是2,集羣模式並行度由spark.default.parallelism決定)來實現grouping。也可以通過numTasks來設置不同的任務數
reduceByKeyAndWindow(func,invFunc,windowLength,slideInterval,[numTasks]) 這是相比於上面的reduceByKeyAndWindow()更有效的函數,其中每個窗口的reduce值使用之前窗口的reduce值遞增計算。計算原理是reduce窗口中的新數據,而inverse reduce離開窗口的舊數據。例如增減窗口中key的數目。然而這僅僅用於可逆的reduce函數,也就是那種有對應逆函數的函數(這由invFunc指定)。就像reduceByKeyAndWindow函數一樣可以通過傳參來設定reduce任務數。注意檢查點必須打開因爲得保存數據嘛
countByValueAndWindow(windowLength,slideInterval,[numTasks]) 當在(K,V)對DStream上調用該函數時,返回(K,Long)對的DStream,每個鍵的值是其在滑動窗口中的頻率。也可以設置任務數參數

Join操作

在Spark Streaming執行多種join操作

Stream-stream join

流與其他流很容易join

stream1 = ...
stream2 = ...
joinedStream = stream1.join(stream2)

stream1和stream2每一批次產生的RDD會join。還有leftOuterJoin,rightOuterJoin,fullOuterJoin。而且還可以窗口之間join

windowedStream1 = stream1.window(20)
windowedStream2 = stream2.window(60)
joinedStream = windowedStream1.join(windowedStream2)
Stream-dataset join

這個已經在之前的DStream.transform操作講到了。還有另一個例子說明窗口流和dataset進行join

dataset = ... # some RDD
windowedStream = stream.window(20)
joinedStream = windowedStream.transform(lambda rdd: rdd.join(dataset))

實際可以動態改變想join的數據集。提供給transform函數在每一個時間間隔都可以改變,因此將會使用當前的數據集。
API文檔庫中包含所有的DStream transformation。

DStreams上的輸出操作

DStream的輸出操作允許數據push到外部系統例如文件系統或數據庫等。由於輸出操作實際上允許外部系統消耗轉換的數據因此他們觸發了實際的DStream transormation的執行(類似於RDD)。當前輸出操作包括:

輸出操作 意義
print() 在運行流處理應用的Driver節點上打印每批次的前10個元素,利於開發和調試。python使用pprint()
saveAsTextFiles(prefix,[suffix]) 將DStream的內容保存爲文本文件。每個批次的文件名基於prefix和suffix產生。“prefix-TIME_IN_MS[.suffix]”
saveAsObjectFiles(prefix,[suffix]) 將DStreams內容保存爲java序列化對象SequenceFile文件。每個批次的文件名基於prefix和suffix產生。“prefix-TIME_IN_MS[.suffix]” 。python不支持該方法
saveAsHadoopFiles(prefix,[suffix]) 將DStreams內容保存爲Hadoop文件。每個批次的文件名基於prefix和suffix產生。“prefix-TIME_IN_MS[.suffix]” 。python不支持該方法
foreachRDD(func) 這是最基本的輸出操作,將func應用到每個RDD中。這個方法應該將每個RDDpush到外部系統,例如文件或網絡數據庫。注意func在Driver上運行,這會強迫計算RDD

使用foreachRDD的設計模式

dstream.foreachRDD是一個非常基本的函數,它將數據存儲到外部系統。然而,理解如何恰當使用該函數很重要。下面是常見的錯誤。
通常寫出數據的手,需要建立連接之後使用該連接發送數據到遠端系統。開發者可能會在Driver創建連接對象,在Spark worker中保存RDD的記錄。例如:

def sendRecord(rdd):
    connection = createNewConnection()  # executed at the driver
    rdd.foreach(lambda record: connection.send(record))
    connection.close()

dstream.foreachRDD(sendRecord)

這並不正確,因爲需要將Driver節點的連接對象序列化之後發送給worker端使用。這樣的連接對象很難在機器之間轉移。錯誤可能發生在初始化操作和序列化錯誤。正確的解決方法是在worker端創建連接對象。
但引入另一個問題:爲每條記錄都創建連接對象,例如:

def sendRecord(record):
    connection = createNewConnection()
    connection.send(record)
    connection.close()

dstream.foreachRDD(lambda rdd: rdd.foreach(sendRecord))

一般創建連接對象會涉及到時間和資源的利用負載。因此爲每條記錄創建連接對象代價太高,會降低系統的吞吐量。更好的方法是使用rdd.foreachPartition–在一個RDD分區內創建連接對象併發送所有記錄:

def sendPartition(iter):
    connection = createNewConnection()
    for record in iter:
        connection.send(record)
    connection.close()

dstream.foreachRDD(lambda rdd: rdd.foreachPartition(sendPartition))

我們可以在多個RDD/批次之間重用連接對象。可以維持一個靜態連接對象池,可以重複被RDD使用來將多個批次push到外部系統。更大的減小overhead:

def sendPartition(iter):
    # ConnectionPool is a static, lazily initialized pool of connections
    connection = ConnectionPool.getConnection()
    for record in iter:
        connection.send(record)
    # return to the pool for future reuse
    ConnectionPool.returnConnection(connection)

dstream.foreachRDD(lambda rdd: rdd.foreachPartition(sendPartition))

注意連接對象池應該依據需求延遲創建,並且設定超時時間。這就實現最有效的數據發送到外部系統的方式。
記住:

  • DStream也像RDD一樣懶執行。具體說,DStream中的RDD輸出操作強迫處理接收到的數據。如果沒有輸出操作或是dstream.foreachRDD()這種沒有RDD action的函數,那麼只是接收數據然後忽略掉,不會有任何處理。
  • 默認輸出操作一個時刻一次,而且順序按照應用中定義的那樣。

DataFrame&SQL操作

可以在流數據上很簡單的使用DataFrame和SQL操作。可以通過StreamingContext使用的SparkContext來創建SQLContext。而且必須這樣做纔可以重啓。這可以懶初始化一個SQLContext單例。接下來修改了之前的單詞計數例子來使用DataFrame和SQL生成單詞。每個RDD轉換爲DataFrame,註冊爲臨時表採用SQL查詢。

# Lazily instantiated global instance of SQLContext
def getSqlContextInstance(sparkContext):
    if ('sqlContextSingletonInstance' not in globals()):
        globals()['sqlContextSingletonInstance'] = SQLContext(sparkContext)
    return globals()['sqlContextSingletonInstance']

...

# DataFrame operations inside your streaming program

words = ... # DStream of strings

def process(time, rdd):
    print "========= %s =========" % str(time)
    try:
        # Get the singleton instance of SQLContext
        sqlContext = getSqlContextInstance(rdd.context)

        # Convert RDD[String] to RDD[Row] to DataFrame
        rowRdd = rdd.map(lambda w: Row(word=w))
        wordsDataFrame = sqlContext.createDataFrame(rowRdd)

        # Register as table
        wordsDataFrame.registerTempTable("words")

        # Do word count on table using SQL and print it
        wordCountsDataFrame = sqlContext.sql("select word, count(*) as total from words group by word")
        wordCountsDataFrame.show()
    except:
        pass

words.foreachRDD(process)

查看源代碼
也可以針對不同線程中的流數據執行SQL查詢操作,例如異步的運行StreamingContext。僅僅需要保證你設置StreamingContext記住足夠數量的流數據來執行查詢。否則如果它不知道任何異步SQL查詢,那就會在查詢完成之前刪掉舊的流數據。例如你想查詢最後一批,5分鐘的數據,那就需要streamingContext.remember(Minutes(5))

MLlib操作

你可以通過MLlib來輕鬆運行機器學習代碼。首先有很多流式的機器學習算法(Streaming LR,Streaming KMeans等)可以同時從流數據中學習還應用到流數據中。而且,對於很多機器學習算法,可以線下學習模型,線上應用模型。查看MLlib指南瞭解更多細節。

緩存/持久化操作

類似於RDD,DStream允許開發者持久化流數據到內存中。就是使用persist()方法自動持久化那個DStream中的RDD到內存中。如果DStream會多次計算則很有用(例如對同一個數據多次操作)。對於基於窗口的操作reduceByWindow,reduceByKeyAndWindow和基於狀態的操作updateStateByKey,持久化是默認操作。因此基於窗口的操作不需要persist()就持久化到內存了。
對於從網絡(Kafka,Flume,socket等)接收到的輸入流數據,默認的持久化水平就是將數據複製到兩個節點上來故障恢復。
注意,不像RDD,DStream默認的持久化方式是序列化到內存中。性能調優會講到。

檢查點機制

流處理應用需要7*24待機,所以必須可以處理與應用邏輯無關的故障(例如系統錯誤,JVM故障)。所以Spark Streaming需要將足夠的信息存儲在故障恢復系統中來從故障中恢復過來。有兩類數據需要checkpoint:

  • 元數據checkpoint–將定義流計算的信息存儲進故障恢復系統例如HDS。這可以用來恢復流應用的驅動節點。元數據包括:配置信息(配置信息用來創建流應用);DStream操作(定義了流應用);未完成的批次(被加到隊列中還未完成操作)
  • 數據checkpointing–將生成的RDD存入可靠的存儲介質中。對於那種需要組合多批次數據的有狀態轉換的應用來說很重要。這種轉換操作導致新生成的RDD依賴之前的RDD,隨着時間的增長,依賴鏈不斷增長。爲了避免恢復時間無限(正比於依賴鏈)的增長,有狀態的轉換中當前的RDD可以階段性的保存到可靠存儲中來切斷依賴鏈。

總結一下就是元數據checkpoint用來恢復驅動節點故障,而數據或RDD checkpoint對於狀態轉換是有必要的。

何時Checkpointing

如果有下列需要則必須能夠checkpoint:

  • 使用狀態轉換:如果應用中使用了updateStateByKey或reduceByKeyAndWindow,那麼就需要階段性地RDD checkpoint。
  • 驅動節點恢復:Metadata檢查點用於恢復進程信息

注意沒有狀態更新地簡單應用就可以不用checkpoint。驅動故障恢復也是一種(只接受不處理)。有的時候就是這樣。希望未來能夠提升對非Hadoop體系地支持。

如何配置checkpoint

Checkpointing需要配置一個用於故障恢復地可靠地文件系統(HDFS,S3),checkpoint信息會被存儲進去。使用streamingContext.checkpoint(checkpointDirectory)。這就能進行前面提到地狀態更新了。如果想使應用從驅動故障中恢復,需要按下面步驟重寫流應用:

  • 當程序第一次啓動的時候,創建新的StreamingContext,啓動所有的流,然後調用start()。
  • 當程序故障之後重啓時,將會從checkpoint目錄中重建StreamingContext.
# Function to create and setup a new StreamingContext
def functionToCreateContext():
    sc = SparkContext(...)   # new context
    ssc = new StreamingContext(...)
    lines = ssc.socketTextStream(...) # create DStreams
    ...
    ssc.checkpoint(checkpointDirectory)   # set checkpoint directory
    return ssc

# Get StreamingContext from checkpoint data or create a new one
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存在則context會從checkpoint 數據中重建起來。如果目錄不存在,就會調用上述的functionToCreateContext函數會被調用來新建context啓動DStream。源代碼在recoverable_network_wordcount.py。這個例子追加單詞計數到一個文件中。
也可以顯式地從checkpoint數據創建StreamingContext,通過使用StreamingContext.getOrCreate(checkpointDirectory,None)來啓動計算即可。
除了使用getOrCreate也要確保驅動進程能夠故障之後自動重啓。這得根據部署架構而定。部署一節詳細講解。
注意RDD的checkpoint保存時會消耗資源。這可能導致每個RDD的處理時間增長。因此,checkpoint的間隔需要仔細設置。採用短間隔(1s),checkpoint每批次數據會顯著降低應用吞吐量。而checkpoint太不頻繁則導致繼承體系和任務量增長,可能會有致命的影響。對於狀態更新應用需要checkpoint,默認的checkpoint間隔是多個批次的間隔至少10s。可以通過dstream.checkpoint(checkpointInterval)設置。例如可以試一試設置checkpoint間隔是5-10s

累加器,廣播變量和檢查點

Spark Streaming中不支持從檢查點中恢復累加器廣播變量。如果你又用了檢查點機制,又使用了累加器或廣播變量,那就必須累加器和廣播變量的懶初始化單例從而可以在驅動節點故障重啓之後能夠重實例化:

def getWordBlacklist(sparkContext):
    if ("wordBlacklist" not in globals()):
        globals()["wordBlacklist"] = sparkContext.broadcast(["a", "b", "c"])
    return globals()["wordBlacklist"]

def getDroppedWordsCounter(sparkContext):
    if ("droppedWordsCounter" not in globals()):
        globals()["droppedWordsCounter"] = sparkContext.accumulator(0)
    return globals()["droppedWordsCounter"]

def echo(time, rdd):
    # Get or register the blacklist Broadcast
    blacklist = getWordBlacklist(rdd.context)
    # Get or register the droppedWordsCounter Accumulator
    droppedWordsCounter = getDroppedWordsCounter(rdd.context)

    # Use blacklist to drop words and use droppedWordsCounter to count them
    def filterFunc(wordCount):
        if wordCount[0] in blacklist.value:
            droppedWordsCounter.add(wordCount[1])
            False
        else:
            True

    counts = "Counts at time %s %s" % (time, rdd.filter(filterFunc).collect())

wordCounts.foreachRDD(echo)

部署應用

這一節討論如何部署Spark Streaming應用的部署

要求

要想運行一個Spark Streaming應用,需要:

  • 帶有集羣管理者的集羣:這是一般Spark應用的要求,必須得有個集羣啊。
  • 應用JAR包:必須將流處理應用編譯成jar包。如果使用spark-submit啓動應用程序就不需要JAR包中包含Spark和Spark Streaming。但是如果使用了高級源(Kafka,Flume等)就需要將其依賴打包進JAR包中用於部署。例如,使用KafkaUtils的應用就必須在JAR包中包含spark-streaming-kafka-0-10_2.12以及其所有下行依賴。
  • 配置足夠的內存空間:由於接收器將數據放入內存因此需要配置足夠的內存hold住數據。如果是想進行10分鐘的窗口操作那內存中至少要存儲10分鐘的數據。內存需求量取決於應用要求。
  • 配置checkpointing:對於流處理應用,Hadoop目錄需要配置成checkpoint目錄用於故障恢復,並且流應用能夠基於目錄中的checkpoint信息故障恢復。
  • 配置應用驅動節點的自動重啓:要想自動地讓驅動節點從故障中恢復過來,則流應用地部署架構需要能控制驅動進程而且如果驅動節點失敗則重啓。不同地集羣管理器對應不同地實現工具:Spark Standalone-在Spark Standalone模式下需要提交Spark應用驅動器到Spark Standalone集羣,也就是應用驅動器本身運行在worker上面。而且,Standalone集羣管理器可以監督驅動程序在由於非0返回標誌或驅動程序節點失敗的時候重啓驅動程序。從Spark Standalone 指南中查看更多細節;YARN支持類似的機制,查看YARN文檔;Mesos-marathon用於Mesos的驅動程序重啓。
  • 配置write-ahead日誌:從Spark1.2開始,引入write ahead 日誌來實現強故障恢復保證。如果可以,所有接收器接收到的數據都會寫入配置的checkpoint目錄中。這防止了驅動成都恢復時數據丟失情況,只要設置配置參數spark.streaming.receiver.writeAheadLog.enable爲true即可。然而這種強語義保證降低接收器的吞吐量。這可以增加多個並行的接收器來增加聚合吞吐量。另外我們推薦當使用write ahead日誌時關閉Spark Streaming接收數據副本機制因爲沒有必要了。通過設置存儲級別爲StorageLevel.MEMORY_AND_DISK_SER即可。當使用S3(或任何不支持flushing的文件系統)來write-ahead logs的時候記住打開spark.streaming.driver.writeAheadLog.closeFileAfterWrite和spark.streaming.receiver.writeAheadLog.closeFileAfterWrite.。查看Spark Streaming Configuration瞭解細節。注意當I/O加密開啓時Spark也不支持加密數據寫入write-ahead log。如果想要這樣,那就得把數據存到支持本地加密的文件系統。
  • 設置最大接收速率:如果集羣的資源不足以讓處理數據的速度跟得上數據接收的速度那麼接收器可以通過設置最大接收速率來限速。查看配置參數中接收器的spark.streaming.receiver.maxRate和Direct Kafka方法的spark.streaming.kafka.maxRatePerPartition。Spark1.5中介紹了backpressure機制可以不必設定限速值,因爲Spark Streaming自動設定限速值並隨着處理狀態的改變而動態調整。通過spark.streaming.backpressure.enabled爲true來打開backpressure。

更新應用代碼

如果需要用新應用代碼來更新正在運行的Spark Streaming應用代碼,有兩種機制:

  • 啓動新的Spark Streaming應用並行地與現存系統運行。一旦新應用已經warm up(比如接收和現存系統相同的數據)並且已經準備好了,舊的應用即可關閉。注意這要求數據源支持將數據發送到兩個目的地接收器(舊的和新的)。
  • 當確保舊的接收器接收的數據已經被處理完了之後關閉舊應用(StreamingContext.stop()JavaStreamingContext.stop())新應用被啓動,接着舊應用處理的地方開始處理數據。注意這要求源數據供應器必須能夠緩存數據,因爲舊的應用關閉了新的還沒正式開始的時候需要緩存待發送的數據。在新應用代碼中重啓之前的checkpoint信息是不可取的,因爲checkpoint信息包含了序列化的Scala/Java/Python對象而使用新的修改過的類來發序列化會出現錯誤。這種情況下,要不就使用不同的ckeckpoint目錄來啓動新應用,要不就刪除舊的checkpoint目錄。

調控應用

除了Sprak自帶的調控能力,還有針對Spark Streaming的額外的功能。當使用StreamingContext時,Spark web UI會出現一個Streaming的選項展示正在運行的i接收器的數量(接收器是否活躍,接收記錄的數量和接收器錯誤等)以及完成了的批次數據(批次處理時間,隊列延遲時間)情況。這可以用於調控流應用的進程。
下面web UI中的兩個指標很重要:

  • 處理時間–處理一個批次的時間
  • 調度延遲–一個批次在隊列中等待上一個批次數據完成處理的時間。

如果批次處理時間一直大於批間隔時間並且/或隊列延遲不斷的增長,那就說明系統不能像數據產生的速度一樣快地處理它最終就會落後。此時應該降低批處理時間。
Spark Streaming程序的進程可以使用StreamingListener接口控制,它可以接收接收器狀態信息和處理時間。注意這是開發者API未來可能會有變化。

性能調優

要想得到Spark Streaming最好的性能就需要調優。這章介紹一些可以提升應用性能的參數和配置信息。從high level的角度來看,需要考慮兩個事項:

  • 通過有效的使用集羣資源來減少每個批次數據的處理時間。
  • 設置正確的批次大小使得處理批次數據的速度和接收到數據的速度一樣快。(也就是數據處理跟得上數據接收)

減小批量處理時間

由很多可以用來最小化每一批次數據處理時間的優化。詳細的討論在調優指南。這裏僅僅關注重要的幾個。

數據接收的並行度

數據從網絡(Kafka,Flume,socket等)接收之後需要反序列化存儲在Spark中。如果數據接收成爲系統瓶頸,那就考慮並行接收數據。注意每個輸入DStream都會在worker機器上創建一個接收器接收數據,因此要接收索格數據流則需要多個輸入DStream並配置接收不同部分的數據纔行。例如一個Kafka輸入DStream接收兩個主題的數據就可以分成兩個兩個Kafka輸入流,每一個接收一個。從而兩個worker有兩個接收器,並行接收數據增加總體的吞吐量。這多個DStream可以union成一個最終的DStream,然後再transformation從而得到結果:

numStreams = 5
kafkaStreams = [KafkaUtils.createStream(...) for _ in range (numStreams)]
unifiedStream = streamingContext.union(*kafkaStreams)
unifiedStream.pprint()

另一個需要考慮的參數是接收器接收數據塊的時間間隔,通過配置參數spark.streaming.blockInterval設置。對於多數接收器,在存入Spark內存中之前接收的數據都會聚合成數據塊。每批次塊的數量決定着處理這些數據的類似map任務的數量。每批次每個接收器的任務數可以估算出來:批次間隔/塊間隔。例如200ms的塊時間間隔在每2s的批次中可以創建10個任務。塊太少(例如少於該機器中的核數)則無法充分利用機器所有的core。減少塊時間間隔就可以增加批次中的任務數。但推薦的最小塊時間間隔大約50ms,任務太大也是個問題。
從多個輸入源或接收器接收數據的一個可選的方案時顯式地重分區輸入數據(使用inputStream.repartition())。這就將接收到的每批次的數據重新分佈在了集羣中以供後面處理。

數據處理的並行度

如果在任意一個stage並行的任務數不夠多則集羣的資源無法被充分利用。例如,對於reduceByKey和reduceByKeyAndWindow操作,默認的並行任務數由spark.default.parallelism配置屬性決定。可以通過參數設置並行度(查看PairDStreamFunctions文檔)或設置spark.default.parallelism配置屬性來改變默認值。

數據序列化

數據序列化的代價可以通過調整序列化格式減小。流處理應用中,有兩類數據會被序列化:

  • 輸入數據:默認情況下接收器接收的數據會以StorageLevel.MEMORY_AND_DISK_SER_2的級別存儲在執行器的內存中。也就是說數據被序列化來減少GC的代價,同時會以副本的形式恢復執行器的故障。數據會首先保存在內存中,只有當內存hold不住流數據之後纔會spill到磁盤上。這裏的序列化明顯會有代價–接收器必須反序列化接收數據(因爲網絡傳輸的信息一般都會經過序列化)然後再按照Spark序列化的格式序列化數據。
  • 持久化流處理得到的RDD:通過流處理得到的RDD可能會被持久化到內存中。例如,窗口操作會持久化數據到內存中因爲需要多次處理。然而不像Spark,流處理中默認的RDD持久化策略是StorageLevel.MEMORY_ONLY_SER來最小化GC代價。

兩種情況下,都可以使用Kryo序列化類減少CPU和內存的壓力。Spark調優指南中包含更多細節。這需要註冊自定義的類,去掉對象引用(查閱配置指南中的Kryo相關的配置)。
如果流應用中需要保存的數據量並不大,那就適合把數據反序列化持久化,並不產生額外的GC代價。例如,如果使用幾秒鐘的批次間隔並且沒有窗口操作,那就可以通過設置存儲級別來關閉反序列化機制來持久化數據。由於沒有序列化,就會減少CPU負載,沒有太多的GC負載則會提升系統性能。

啓動任務的代價

如果每秒鐘啓動的任務數非常多(一秒50個或更多)那麼發送任務到slave節點的代價就很高,而且很難實現亞秒級延遲。負載可以減輕:

  • 執行模式:一個Standalone或coarse-grained Mesos模式運行Spark相比於fine-grained Mesos模式會有更佳的啓動時間。查看Mesos運行指南瞭解更多細節。

設置恰當的批間隔時間

Spark Streaming應用要想在集羣上穩定運行,系統處理數據的能力要和接收數據一樣快。或者說處理數據的速度要和生成數據的速度一樣快。是不是這樣可以在流處理web UI中調整處理時間來發現。處理時間應該小於批次間隔時間。
由於流處理連續計算的特點,批次間隔時間對在固定資源的集羣中運行的應用可以支撐的數據速率有重要影響。例如考慮一下之前的WordCountNetwork例子,對於某一特定的數據速率,系統能夠每2s報告一下單詞數量(可能批次間隔是2s)而不能每500ms。所以批次間隔時間設置的要符合實際能支撐的數據速率。
比較好的去設置批次時間間隔的方法是用保守的間隔時間(5-10s)和低的數據速率來測試。要判斷系統能不能跟上數據速率,可以檢查每一處理批次需要的端到端的延遲時間(或者通過驅動程序log4j日誌以及使用StreamingListener接口來查看整體延遲)。如果延遲正比於批量大小則是穩定運行,否則如果延遲一直增長那就說明系統跟不上數據產生或接收速率了。一旦有穩定的配置了,就可以增加數據速率並且/或減小批次大小。注意由於臨時數據速率增加導致的延遲臨時增加無關緊要只要數據速率降下來延遲降低就行了。(小於批次大小)

內存調優

調整Spark應用的內存使用和GC已經在調優指南中詳細介紹過了。強烈推薦讀一讀。這部分討論針對Spark Streaming應用的調優參數。
Spark Streaming應用需要的內存大小取決於transformation類型。例如,如果想使用最後10分鐘的窗口操作,那麼集羣要能夠存下10分鐘的數據量。或者想使用updateStateByKey來處理很多key那就需要很多內存。與此相反,如果僅僅是map-filter-store操作就不要那麼多內存了。
一般來說接收器介紹的數據以StorageLevel.MEMORY_AND_DISK_SER_2存下來,內存中不夠用了再spill到磁盤上。這就降低了性能,因此建議保證足夠的內存空間。最好在一個小規模的數據集上試一試然後評估一下。
內存調優的另一方面是GC。流應用需要低延遲,很不希望由於JVM GC導致很大的延遲。
有幾個參數可以調優內存使用GC代價:

  1. DStream的持久化水平:在之前的數據序列化節提到的,輸入數據和RDD默認以序列化字節的形式持久化。相比於非序列化存儲,這降低了內存使用和GC代價。使用Kryo序列化還能降低序列化大小和內存使用。壓縮(Spark spark.rdd.compress配置)會減少內存使用但代價是CPU計算時間。
  2. 清理舊數據:默認所有輸入數據和由DStream生成的持久化RDD自動被清理。Spark Streaming決定着合適清理這些數據。例如你正在使用10分鐘的窗口操作,Spark Streaming保持最後10分鐘的數據,並且丟掉舊數據。通過設置streamingContext.remenber可以保留數據更長時間。
  3. CMS GC:強烈推薦使用併發的mark-sweep GC來最小化GC相關任務的暫停時間。即使併發GC會減少系統總體的吞吐量但仍然推薦使用它來實現更好的批次處理時間。確保在驅動節點額worker節點上設置了CMS GC(在spark-submit中使用–driver-java-options以及使用spark.executor.exraJavaOptions)
  4. 其他提示:還要減少GC負載,可以試一試:使用Tachyon來堆外存儲持久化RDD。查閱Spark Programming 指南瞭解細節;使用更多的執行器更小的堆大小。這可以降低每個JVM堆的GC壓力。
需要記住的important point
  • 一個DStream和一個接收器關聯。要想達到多讀取並行度就要創建多個DStream。接收器也運行在executor中。它佔據着一個core。確保在接收器佔了一個core還有足夠的cores處理數據,所以spark.cores.max需要考慮這點。接收器以RR的調用方法分配給executor。
  • 當數據從流數據源接收過來時接收器會創建數據塊。每個塊間隔就會生成新的數據塊。N塊數據會經過一個批次時間段創建出來,其中N=批次時間間隔/塊時間間隔。這些塊由當前executor的BlockManager管理並與其他executor的塊管理器分割開。然後驅動器上運行的Network input Tracker被通知塊的位置用於後續處理
  • 在驅動節點上一個RDD會基於這個batchinterval期間接收的數據而創建,期間產生的塊就是RDD的分區。每個分區對應Spark的一個任務。如果blockinterval==batchinterval那麼只有一個分區也就可能就在本都處理了。
  • 數據塊上的map任務就是擁有塊的executor(接收塊的那個以及擁有塊副本的那個)上運行了,不管block interval,除非非本地調度奏效了。blockinterval越高則塊越大,而spark.locality.wait越高則本地處理塊的機會越大(別被調度出去)。需要平衡這兩個參數來確保更大的塊能在本地處理。
  • 要不想依賴batchinteval和blockinterval,那就通過inputDStream.repartition(n)來定義分區數量。這種重洗牌RDD數據的方法創建了一些分區。它帶來更大的並行度以及shuffle代價。RDD的處理受驅動節點的Jobscheduler的調度。某一時刻只有一個job是活躍的(??),所以如果正運行一個job呢那別的job就在隊列中躺着。
  • 如果有兩個dstream,那就有同時有兩個RDD生成就有兩個job一個接一個地被調度。爲了避免這種情況可以union兩個dstream。生成一個unoinRDD當作一個job調度,而RDD的分區不受影響。
  • 如果批處理的時間多於batchinterval那麼接收器內存會被填滿然後以拋異常而終止。不能停止接收器接收數據但是可以通過設置spark.streaming.receiver.maxRate來限速啊。

Spark Streaming的故障恢復機制

這裏我們討論一下Spark Streaming應用的故障行爲。

背景

要理解Spark Streaming的機制,就要記住Spark RDD的故障恢復機制。

  1. RDD是一個不可變,可重算的分佈式數據集。每個RDD記住了操作的歷史從而能夠在故障恢復時根據輸入數據來重算出來。
  2. 如果由於某個worker節點出故障導致RDD某個分區數據丟失了,那分區數據就會在故障恢復時基於計算曆史信息重算。
  3. 假設所有RDD轉換操作都是確定性的,那麼最終的RDD和Spark 集羣故障無關。

Spark操作的數據都在HDFS或S3這種故障恢復文件系統中。因此所有從故障恢復數據中生成的RDD都支持故障恢復。然而Spark Streaming不是這樣的,因爲多數情況下數據來源於網絡(除了fileStream)。要想對於所有的生成的RDD故障恢復,接收的數據需要在多個worker節點的Spark執行器中備份(默認是2)。這導致故障恢復時兩種數據需要恢復:

  1. 接收的數據和副本:由於有備份從而數據可以從單節點故障中恢復。
  2. 接收的數據但僅僅緩存:這種出現故障了只能從source處恢復。

而且有兩種故障值得關注:

  1. worker節點故障:任何運行executor的worker節點都會出問題,其中內存中的數據則會丟失。運行在失效的節點上的接收器緩存的數據也會丟失。
  2. 驅動節點失效:驅動節點失效了,那SparkContext完蛋了,所有執行器的內存數據都沒了啊。

有了這些基本知識我們來理解Spark Streaming故障恢復的機制。

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