Spark每日半小時(38)——Spark Streaming:性能調優

性能調優

從集羣上的Spark Streaming應用程序中獲得最佳性能需要進行一些調整。在高層次上,我們需要考慮兩件事:

  1. 通過有效使用集羣資源減少每批數據的處理時間。
  2. 設置正確的批量大小,以便可以像接收到的那樣快速處理批量數據(即,數據處理與數據提取保持同步)。

減少批處理時間

可以在Spark中進行許多優化,以最大限度地縮短每個批處理的處理時間。

數據接收中的並行度

通過網絡接收數據(如Kafka,Flume,Socket等)需要將數據反序列化並存儲在Spark中。如果數據接收稱爲系統中的瓶頸,則考慮並行化數據接收。請注意,每個輸入DStream都會創建一個接收單個數據流的接收器(在工作機器上運行)。因此,可以通過創建多個輸入DStream可以分成兩個Kafka輸入流,每個輸入流只接收一個主題。這將運行兩個接收器,允許並行接收數據,從而提高整體吞吐量。這些多個DStream可以組合在一起以創建單個DStream。然後,可以在統一流上應用在單個輸入DStream上應用的轉換。如下:

int numStreams = 5;
List<JavaPairDStream<String, String>> kafkaStreams = new ArrayList<>(numStreams);
for (int i = 0; i < numStreams; i++) {
  kafkaStreams.add(KafkaUtils.createStream(...));
}
JavaPairDStream<String, String> unifiedStream = streamingContext.union(kafkaStreams.get(0), kafkaStreams.subList(1, kafkaStreams.size()));
unifiedStream.print();

應考慮的另一個參數是接收器的塊間隔,它由配置參數spark.streaming.blockInterval決定。對於大多數接收器,接收的數據存儲在Spark的內存中之前合併爲數據塊。每批中的塊決定了在類似map的轉換中處理接收數據的任務書。每批每個接收器的任務數量大約是(批處理間隔/塊間隔)。例如,200ms的塊間隔將每2秒批次創建10個任務。如果任務數量太少(即,少於每臺計算機的核心數),那麼效率將會很低,因爲所有可能的核心都不會用於處理數據。要增加給定批處理間隔的任務書,請減少塊間隔。但是,建議的塊間隔最小值約爲50ms,低於該值,任務啓動開銷可能會出現問題。

使用多個輸入流/接收器接收數據的替代方案是顯式地重新分區輸入數據流(使用inputStream.repartition(<number of partitions>))。這會在進一步處理之前將收到的批量數據分佈到集羣中指定數量的機器上。

數據處理中的並行度

如果在計算的任何階段中使用的並行任務的數量不夠高,則可能未充分利用集羣資源。例如,對於像reduceByKey和reduceByKeyAndWindow的分佈式reduce操作,默認的並行任務數由配置屬性spark.default.parallelism控制。我們可以將並行級別作爲參數傳遞。

數據序列化

通過調整序列化格式可以減少數據序列化的開銷。在流式傳輸的情況下,有兩種類型的數據被序列化。

  • 輸入數據:默認情況下,通過Receiver接收的輸入數據通過StorageLevel.MEMORY_AND_DISK_SER_2存儲在執行程序的內存中。也就是說,數據被序列化爲字節以減少GC開銷,並且爲了容忍執行器故障而被複制。此外,數據首先保存在內存中,並且僅在內存不足以保存流式計算所需的所有數據數據時才移除到磁盤。這種序列化顯然有開銷:接收器必須反序列化接收的數據並使用Spark的序列化格式重新序列化。
  • 流式傳輸操作生成的持久RDD:流式計算生成的RDD可以保留在內存中。例如,窗口操作將數據保留在內存中,因爲它們將被多次處理。但是,與StorageLevel.MEMORY_ONLY的Spark Core默認值不同,流式計算生成的持久RDD默認使用StorageLevel.MEMORY_ONLY_SER(即序列化)保留,以最大限度地減少GC開銷。

在這兩種情況下,使用Kryo序列化可以減少CPU和內存開銷。對於Kryo,請考慮註冊自定義類,並禁用對象引用跟蹤。

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

任務啓動開銷

如果每秒啓動地任務數量很高(例如,每秒50或更多),則向從屬設備發送任務地開銷可能很大,並且將難以實現亞秒級延遲。通過以下更改可以減少開銷:

  • 執行模式:在獨立模式或粗粒度Mesos模式下運行Spark可以獲得比細粒度Mesos模式更好地任務啓動時間。

這些更改可以將批處理時間減少100毫秒,從而允許亞秒級批量大小可行。

設置正確的批次間隔

要使集羣上運行的Spark Streaming應用程序保持穩定,系統應該能夠以接收數據的速度處理數據。換句話說,批處理數據應該在生成時儘快處理。通過監視流式Web UI中的處理時間可以找到是否適用於應用程序,其中批處理時間應小於批處理間隔。

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

確定適合我們的ing用程序批量大小的好方法是使用保守的批處理間隔(例如,5-10秒)和低數據速率進行測試。要驗證系統是否能夠跟上數據速率,我們可以檢查每個已處理批處理所遇到的端到端延遲的值(在Spark驅動程序log4j日誌中查找“總延遲”,或使用StreamingListener接口)。如果延遲保持與批量大小相當,則系統穩定。否則,如果延遲不斷增加,則意味着系統無法跟上,因此不穩定。一旦瞭解了穩定的配置,就可以嘗試提高數據速率和/或減少批量。注意,只要延遲減小到低值(即,小於批量大小),由於臨時數據速率增加引起的延遲的瞬時增加可能是正常的。

內存調整

Spark Streaming應用程序所需的集羣內存量在很大程度上取決於所使用的轉換類型。例如,如果要在最後十分重的數據上使用窗口操作,那麼我們的集羣應該有足夠的內存來保存10分鐘的數據。或者,如果我們想使用updateStateByKey大量的鍵,那麼需要的內存將很高。相反,如果我們想做一個簡單的map-filter-store操作,那麼必要的內存就會很低。

通常,由於通過接收器接收的數據與StorageLevel.MEMORY_AND_DISK_SER_2一起存儲,因此不適合內存的數據將移除到磁盤。這可能會降低流應用程序的性能,因此建議我們根據應用程序的需要提供足夠的內存。最好嘗試小規模的查看內存使用情況進行相應估算。

內存調整的另一個方面是垃圾收集。對於需要低延遲的流應用程序,不希望有JVM垃圾收集引起大的暫停。

有一些參數可以幫助我們調整內存使用和GC開銷:

  • DStream的持久性級別:如前面數據序列化部分所述,輸入數據和RDD默認持久化爲序列化字節。與反序列化持久性相比,這減少了內存使用和GC開銷。啓用Kryo序列化可進一步減少序列化大小和內存使用量。通過壓縮(Spark配置spark.rdd.compress)可以實現內存使用的進一步減少,但代價是CPI時間。
  • 清除舊數據:默認情況下,DStream轉換生成的所有輸入數據和持久化RDD都會自動清除。Spark Streaming根據使用的轉換決定何時清除數據。例如,如果我們使用10分鐘的窗口操作,那麼Spark Streaming將保留最後10分鐘的數據,streamingContext.remember。
  • CMS垃圾收集器:強烈建議使用併發標記清除GC,以保持GC相關的暫停始終較低。儘管已知併發GC會降低系統的整體處理吞吐量,但仍建議使用它來實現更一致的批次處理時間。確保在驅動程序(使用:driver-java-options輸入spark-submit)和執行程序(使用Spark配置spark.executor.extraJavaOptions)上設置CMS GC。
  • 其他提示:爲了進一步降低GC開銷,這裏有一些嘗試的建議。
    • 使用OFF_HEAP存儲級別保留RDD
    • 使用具有較小堆大小的更多執行程序。這將降低每個JVM堆中的GC壓力。

要記住的要點:

  • DStream與單個接收器相關聯。爲了獲得讀取並行性,需要創建多個接收器,即多個DStream。接收器在執行器內運行。它佔據一個核心。確保在預訂接收器插槽後又足夠的內核進行處理,即spark.cores.max應考慮接收器插槽。接收器以循環方式分配給執行器。
  • 當從流源接收數據時,接收器創建數據塊。每隔blockInterval毫秒生成一個新的數據塊。在batchInterval期間創建N個數據塊,其中N=batchInterval / blockInterval。這些塊由當前執行程序的BlockManager分發給其他執行程序的塊管理器。之後,將在驅動程序上運行的網絡輸入跟蹤器通知塊位置以進行進一步處理。
  • 在驅動程序上爲batchInterval期間創建的塊創建RDD。batchInterval期間生成的塊是RDD的分區。每個分區都是Spark中的任務。blockInterval == batchInterval意味着創建了一個分區,並且可能在本地處理它。
  • 塊中的映射任務在執行器中處理(一個接受塊,另一個複製塊),具有塊不管塊間隔,除非不是本地調度啓動。具有更大的blockInterval意味着更大的塊。較高的spark.locality.wait值會增加在本地節點上處理快的機會。需要在這兩個參數之間找到平衡,以確保在本地處理更大的塊。
  • 我們可以通過調用inputDstream.repartition(n)來定義分區數,而不是依賴於batchInterval和blockInterval。這會隨機重新調整RDD中的數據以創建n個分區。爲了更大的並行性,雖然以重新清洗爲代價,也是可以接受的。
  • 如果我們由兩個dstream,將形成兩個RDD,並且將創建兩個將一個接一個地安排的執行。爲了避免這種情況,我們可以union這兩個dstream。這將確保爲dstream的兩個RDD形成單個union RDD。然後,此union RDD被視爲單個作業。但是,RDD的分區不會收到影響。
  • 如果批處理時間超過批處理間隔,那麼顯然接收者的內存將開始填滿並最終導致拋出異常(BlockNotFoundException)。目前沒有辦法暫停接收器,使用SparkConf配置spark.streaming.reveiver.maxRate,可以限制接收器的速率。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章