1 概述
1.1 SparkStreaming是什麼
Spark Streaming 是個批處理的流式(實時)計算框架。其基本原理是把輸入數據以某一時間間隔批量的處理,當批處理間隔縮短到秒級時,便可以用於處理實時數據流。
支持從多種數據源獲取數據,包括Kafk、Flume、Twitter、ZeroMQ、Kinesis以及TCP sockets,從數據源獲取數據之後,可以使用諸如map、reduce、join等高級函數進行復雜算法的處理。最後還可以將處理結果存儲到文件系統,數據庫等。
Spark Streaming處理的數據流圖:
以上的連續4個圖,分別對應以下4個段落的描述:
- Spark Streaming接收Kafka、Flume、HDFS和Kinesis等各種來源的實時輸入數據,進行處理後,處理結果保存在HDFS、Databases等各種地方。
- Spark Streaming接收這些實時輸入數據流,會將它們按批次劃分,然後交給Spark引擎處理,生成按照批次劃分的結果流。
- Spark Streaming提供了表示連續數據流的、高度抽象的被稱爲離散流的DStream。DStream本質上表示RDD的序列。任何對DStream的操作都會轉變爲對底層RDD的操作。
- Spark Streaming使用數據源產生的數據流創建DStream,也可以在已有的DStream上使用一些操作來創建新的DStream。
1.2 2. Spark Streaming能做什麼
目前而言SparkStreaming 主要支持以下三種業務場景
- 無狀態操作:只關注當前批次中的實時數據,例如:
- 商機標題分類,分類http請求端 -> kafka -> Spark Streaming -> http請求端Map -> 響應結果
- 網庫Nginx訪問日誌收集,flume->kafka -> Spark Streaming -> hive/hdfs
- 數據同步,網庫主站數據通過“主站”->kafka->Spark Streaming -> hive/hdfs
- 有狀態操作:對有狀態的DStream進行操作時,需要依賴之前的數據 除了當前新生成的小批次數據,但還需要用到以前所生成的所有的歷史數據。新生成的數據與歷史數據合併成一份流水錶的全量數據例如:
- 實時統計網庫各個站點總的訪問量
- 實時統計網庫每個商品的總瀏覽量,交易量,交易額。
- 窗口操作:定時對指定時間段範圍內的DStream數據進行操作,例如:
- 網庫主站的惡意訪問、爬蟲,每10分鐘統計30分鐘內訪問次數最多的用戶。
1.3 特性
1.3.1 優點:
- 吞吐量大、速度快。
- 容錯:SparkStreaming在沒有額外代碼和配置的情況下可以恢復丟失的工作。checkpoint。
- 社區活躍度高。生態圈強大。
- 數據源廣泛。
1.3.2 缺點:
- 延遲。500毫秒已經被廣泛認爲是最小批次大小,這個相對storm來說,還是大很多。所以實際場景中應注意該問題,就像標題分類場景,設定的0.5s一批次,加上處理時間,分類接口會佔用1s的響應時間。實時要求高的可選擇使用其他框架。
2 基礎概念-開發
2.1 簡單示例
2.1.1 Word count詞頻計算demo
- object NetworkWordCount {
- def main(args: Array[String]) {
- val sparkConf = new SparkConf()
- val ssc = new StreamingContext(sparkConf, Seconds(1))
- val lines = ssc.socketTextStream(hostname, port)
- val words = lines.flatMap(_.split(" "))
- val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
- wordCounts.print()
- ssc.start()
- ssc.awaitTermination()
- }
2.1.2 說明
1. 通過創建輸入DStreams來定義輸入源。
2. 通過將轉換和輸出操作應用於DStream來定義流式計算。
3. 開始接收數據並使用它進行處理streamingContext.start()
。
4. 等待處理停止(手動或由於任何錯誤)使用streamingContext.awaitTermination()
。
5. 可以手動停止處理streamingContext.stop()
。
2.1.3 注意
1. 一旦上下文開始,就不能設置或添加新的流計算。
2. 一旦上下文停止,它將無法重新啓動。
3. 只有一個StreamingContext可以在JVM中同時處於活動狀態。
2.2 輸入源
Spark Streaming提供了兩類輸入源。
· 基本來源:StreamingContextAPI中直接提供的資源。示例:文件系統,套接字連接。
1.文件系統:streamingContext.fileStream(hdfsDataDirectory)
SparkStreaming將監聽目錄dataDirectory
並處理在該目錄中創建的任何文件(不支持嵌套目錄中寫入的文件)
文件必須具有相同的數據格式。
必須dataDirectory通過將數據原子移動或重命名爲數據目錄來創建文件。
移動後,文件不能更改。因爲,如果文件被不斷附加,則不會讀取新的數據。
Sparkstreaming
監聽對應主機-端口,處理髮送到該端口的數據。
· 高級來源:Kafka,Flume等資源可以通過額外的實用類來獲得。
實際應用場景中,Kafak使用較多,主要介紹Kafka的使用:
KafkaUtils.createStream(ssc, zkQuorum, groupId, topicsMap)
ssc:streamingContext
zkQuorum:kafka元數據在zookeeper中的存儲地址(示例:node1:2181/kafka)
groupId:spark streaming接受kafka數據使用的用戶組id,可通過該參數控制每次接受kafka數據的索引位置,spark streaming每次啓動都會從該groupId上次接收到的數據位置開始接收。
topicsMap:Map[String, Int]類型對象,key對應接收的數據 topic名稱,value爲線程數量。sparkstreaming接收kafka數據的啓動的線程數量,即併發量
如果要在流式應用程序中並行接收多個數據流,則可以創建多個輸入DStream
2.3 DStream轉換操作
操作 | 含義 |
map(func) | 通過傳遞源DStream的每個元素通過函數func返回一個新的DStream |
flatMap(func) | 與map類似,但每個輸入項可以映射到0個或更多的輸出項。 |
filter(func) | 通過僅選擇func返回true 的源DStream的記錄來返回新的DStream |
repartition(numPartitions) | 通過創建更多或更少的分區來更改此DStream中的並行級別。 |
union(otherStream) | 返回一個新的DStream,它包含源DStream和otherDStream中元素的並集。 |
count() | 通過計算源DStream的每個RDD中的元素數量來返回單元素RDD的新DStream |
reduce(func) | 通過使用函數func(它需要兩個參數並返回一個),通過聚合源DStream的每個RDD中的元素來返回單元素RDD的新DStream 。該函數應該是關聯的,以便可以並行計算。 |
countByValue() | 當調用類型爲K的元素的DStream時,返回一個新的DStream(K,Long)對,其中每個鍵的值是源DStream的每個RDD中的頻率。 |
reduceByKey(func,[numTasks]) | 當(K,V)對的DStream被調用時,返回(K,V)對的新DStream,其中使用給定的reduce函數聚合每個鍵的值。注意:默認情況下,使用Spark的默認並行任務數(2爲本地模式,羣集模式中的數字由config屬性決定spark.default.parallelism )進行分組。您可以傳遞可選numTasks 參數來設置不同數量的任務。 |
join(otherStream,[numTasks]) | 當(K,V)和(K,W)對的兩個DStream被調用時,返回一個新的(K,(V,W))對的DStream與每個鍵的所有元素對。 |
cogroup(otherStream,[numTasks]) | 當調用(K,V)和(K,W)對的DStream時,返回一個新的DStream(K,Seq [V],Seq [W])元組。 |
transform(func) | 通過對源DStream的每個RDD應用RDD到RDD函數來返回一個新的DStream。這可以用於對DStream進行任意RDD操作。 |
updateStateByKey(func) | 返回一個新的“狀態”DStream,其中通過對鍵的先前狀態應用給定的功能和鍵的新值來更新每個鍵的狀態。這可以用於維護每個密鑰的任意狀態數據。 |
Transform操作:
- val spamInfoRDD = ssc.sparkContext.newAPIHadoopRDD(...)
- val cleanedDStream = wordCounts.transform(rdd => {
- rdd.join(spamInfoRDD).filter(...) ...
- })
UpdateStateByKey 操作:
- val lines = KafkaUtils.createStream(ssc, zkQuorum, group, topicMap).map(_._2)
- //產生我們需要的pair rdd
- val linerdd = lines.map{row =>{
- ···
- (key, amt)
- }}
- val addFunc = (currValues: Seq[Int], preValueState: Option[Int]) =>{
- //通過spark內部的reducebykey按key規約,然後這裏傳入某key當前批次的seq,再計算key的總和
- val currentCount = currValues.sum
- //已經累加的值
- val previousCount = preValueState.getOrElse(0)
- //返回累加後的結果,是一個Option[Int]類型
- Some(currentCount + previousCount)
- }
- linerdd.updateStateByKey[Int](addFunc _).print()
Windows操作
下圖說明了這個窗口。
如圖:
1. 紅色的矩形就是一個窗口,窗口hold的是一段時間內的數據流。
2.這裏面每一個time都是時間單元,在官方的例子中,每隔window size是3 time unit, 而且每隔2個單位時間,窗口會slide一次。
所以基於窗口的操作,需要指定2個參數:
· window length - The duration of the window (3 inthe figure)
· slide interval - The interval at which the window-basedoperation is performed (2 in the figure).
舉個例子吧:
還是以wordcount舉例,每隔10秒,統計一下過去30秒過來的數據。
val windowedWordCounts = pairs.reduceByKeyAndWindow(_ + _, Seconds(30), Seconds(10))
這裏的paris就是一個DStream,每條數據類似(word,1)
一些常見的窗口操作如下。所有這些操作都採用上述兩個參數 - windowLength和slideInterval。
操作 | 含義 |
window(windowLength,slideInterval) | 返回基於源DStream的窗口批次計算的新DStream。 |
countByWindow(windowLength,slideInterval) | 返回流中元素的滑動窗口數。 |
reduceByWindow(func,windowLength,slideInterval) | 返回一個新的單元素流,通過使用func在滑動間隔中通過在流中聚合元素創建。 |
reduceByKeyAndWindow(func,windowLength,slideInterval,[ numTasks ]) | 當對(K,V)對的DStream進行調用時,返回(K,V)對的新DStream,其中每個鍵的值 在滑動窗口中使用給定的減少函數func進行聚合。 |
countByValueAndWindow(windowLength, slideInterval,[numTasks ]) | 當調用(K,V)對的DStream時,返回(K,Long)對的新DStream,其中每個鍵的值是其滑動窗口內的頻率。 |
2.4 DStream的輸出操作
輸出操作允許將DStream的數據推送到外部系統,如數據庫或文件系統。由於輸出操作實際上允許外部系統使用變換後的數據,所以它們觸發所有DStream變換的實際執行(類似於RDD的動作)。目前,定義了以下輸出操作:
操作 | 含義 |
print() | 在運行流應用程序的驅動程序節點上的DStream中打印每批數據的前十個元素。 |
saveAsTextFiles(prefix,[ suffix ]) | 將此DStream的內容另存爲文本文件。基於產生在每批間隔的文件名的前綴和後綴:“前綴TIME_IN_MS [.suffix]”。 |
saveAsObjectFiles(prefix,[ suffix ]) | 將DStream的內容保存爲 |
saveAsHadoopFiles(prefix,[ suffix]) | 將此DStream的內容另存爲Hadoop文件。基於產生在每批間隔的文件名的前綴和後綴:“前綴TIME_IN_MS [.suffix]”。 |
foreachRDD(func) | 對從流中生成的每個RDD 應用函數func的最通用的輸出運算符。此功能應將每個RDD中的數據推送到外部系統,例如將RDD保存到文件,或將其通過網絡寫入數據庫。請注意,函數func在運行流應用程序的驅動程序進程中執行,通常會在其中具有RDD動作,從而強制流式傳輸RDD的計算。 |
注意:
- DStreams通過輸出操作進行延遲執行,就像RDD由RDD操作懶惰地執行。具體來說,DStream輸出操作中的RDD動作強制處理接收到的數據。因此,如果您的應用程序沒有任何輸出操作,或者具有輸出操作,比如dstream.foreachRDD()沒有任何RDD操作,那麼任何操作都不會被執行。系統將簡單地接收數據並將其丟棄。
- 默認情況下,輸出操作是一次一個執行的。它們按照它們在應用程序中定義的順序執行。
2.5 DataFrame和SQL操作
可以輕鬆地在流數據上使用DataFrames和SQL操作。您必須使用StreamingContext正在使用的SparkContext創建一個SparkSession。此外,必須這樣做,以便可以在驅動程序故障時重新啓動。
這在下面的示例中顯示。將每個RDD轉換爲DataFrame,註冊爲臨時表,然後使用SQL進行查詢。
- 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()
- }
2.6 MLlib操作
可以通過訓練出個模型,然後將模型作爲廣播變量,在DStream操作中使用該模型預測相關數據。
2.7 緩存/持久性
與RDD類似,DStreams還允許開發人員將流的數據保留在內存中。也就是說,使用persist()
DStream上的方法將自動將該DStream的每個RDD保留在內存中。如果DStream中的數據將被多次計算(例如,相同數據上的多個操作),這是非常有用的。對於基於窗口的操作,像reduceByWindow
和reduceByKeyAndWindow
和基於狀態的操作一樣updateStateByKey
,這是隱含的。因此,基於窗口的操作生成的DStreams將自動保留在內存中,無需開發人員的調用persist()
。
2.8 CheckPoint/檢查點
流式應用程序必須全天候運行,因此必須能夠適應與應用程序邏輯無關的故障(例如,系統故障,JVM崩潰等)。爲了可以這樣做,Spark Streaming需要檢查足夠的信息到容錯存儲系統,以便可以從故障中恢復。
2.8.1 如何使用CheckPoint
啓用 checkpoint,需要設置一個支持容錯的、可靠的文件系統(如 HDFS、s3 等)目錄來保存 checkpoint 數據。通過調用 streamingContext.checkpoint(checkpointDirectory) 來完成。另外,如果你想讓你的application能從 driver 失敗中恢復,你的application 要滿足:
· 若 application爲首次重啓,將創建一個新的 StreamContext 實例
· 如果 application是從失敗中重啓,將會從 checkpoint 目錄導入 checkpoint 數據來重新創建 StreamingContext 實例
通過 StreamingContext.getOrCreate
可以達到目的:
- def functionToCreateContext(): StreamingContext = {
- val ssc = new StreamingContext(...) // new context
- val lines = ssc.socketTextStream(...) // create DStreams
- ...
- ssc.checkpoint(checkpointDirectory) // set checkpoint directory
- ssc
- }
- val context = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext _)
- ```
- context.start()
- context.awaitTermination()
2.9 Spark Streaming程序提交
與spark提交方式一樣
spark-submit
--class
:您的應用程序的入口點(例如org.apache.spark.examples.SparkPi
)
--master
:集羣的主URL(例如spark://10.2.9.114:7077;提交到yarn集羣寫:yarn
)
--deploy-mode
:是否將驅動程序部署在工作節點(cluster
)或本地作爲外部客戶端(client
)(默認值:client
)†
--conf
:任意Spark配置屬性,key = value格式。對於包含空格的值,用引號括起“key = value”(如圖所示)。
application-jar
:包含應用程序和所有依賴關係的捆綁jar的路徑。該URL必須在集羣內全局可見,例如所有節點上存在的hdfs://
路徑或file://
路徑。
application-arguments
:參數傳遞給主類的main方法,如果有的話。
提交到Yarn集羣的特殊參數:
--executor-memory 每個executor內存大小
--num-executors executor數量
--executor-cores executor cpu數量
3 性能調優
3.1 合理設置批處理
1. 通過有效利用集羣資源減少每批數據的處理時間。
2. 設置正確的批量大小,使得批量的數據可以像接收到的那樣快速處理(即數據處理與數據攝取保持一致)。
3.2 數據接收中的並行級別
- val numStreams = 5
- val kafkaStreams = (1 to numStreams).map { i => KafkaUtils.createStream(...) }
- val unifiedStream = streamingContext.union(kafkaStreams)
- unifiedStream.print()
3.3 內存優化
3.3.1 數據序列化
可以通過調優序列化格式來減少數據串行化的開銷
3.3.2 對象類型
例如HashMap
和LinkedList
等一些結構佔用空間較大,可考慮優化使用對象類型。
3.4 廣播大變量
使用廣播功能可以大大減少羣集上啓動作業的成本。
4 FAQ
task大都集中在特定的少數executor上執行,並行度不夠。
原因:
這些點爲receiver所在節點。Receiver會將接收到的數據的第一個副本放在本地,另外的副本隨機分佈在其他節點。黨我們只設置一個副本時(e.g. MEMORY_ONLY_SER),數據會全部集中在receiver所在的幾個節點,task也會被優先分發到這些點上的executor中執行。
4.2 spark Streaming任務失敗
原因:Spark Streaming存在執行一定時間後失敗的問題
解決辦法:定時重啓Spark Streaming任務