Spark優化----開發調優(下)

上次講到避免使用shuffle類算子,接下來繼續

5、使用map-side預聚合的shuffle操作

如果因爲業務需要,一定要使用shuffle操作,無法用map類的算子來替代,那麼儘量使用可以map-side預聚合的算子。

所謂的map-side預聚合,說的是在每個節點本地對相同的key進行一次聚合操作,類似於MapReduce中的本地combiner。 map-side預聚合之後,每個節點本地就只會有一條相同的key,因爲多條相同的key都被聚合起來了。其他節點在拉取所有節點上的相同key時,就 會大大減少需要拉取的數據數量,從而也就減少了磁盤IO以及網絡傳輸開銷。通常來說,在可能的情況下,建議使用reduceByKey或者 aggregateByKey算子來替代掉groupByKey算子。因爲reduceByKey和aggregateByKey算子都會使用用戶自定義 的函數對每個節點本地的相同key進行預聚合。而groupByKey算子是不會進行預聚合的,全量的數據會在集羣的各個節點之間分發和傳輸,性能相對來 說比較差。

比如如下兩幅圖,就是典型的例子,分別基於reduceByKey和groupByKey進行單詞計數。其中第一張圖是groupByKey的原理 圖,可以看到,沒有進行任何本地聚合時,所有數據都會在集羣節點之間傳輸;第二張圖是reduceByKey的原理圖,可以看到,每個節點本地的相同 key數據,都進行了預聚合,然後才傳輸到其他節點上進行全局聚合。



6、使用高性能算子


使用reduceByKey/aggregateByKey替代groupByKey

詳情見上一優化原則

使用mapPartitions替代普通map

mapPartitions類的算子,一次函數調用會處理一個partition所有的數據,而不是一次函數調用處理一條,性能相對來說會高一些。 但是有的時候,使用mapPartitions會出現OOM(內存溢出)的問題。因爲單次函數調用就要處理掉一個partition所有的數據,如果內存 不夠,垃圾回收時是無法回收掉太多對象的,很可能出現OOM異常。所以使用這類操作時要慎重!

使用foreachPartitions替代foreach

原理類似於“使用mapPartitions替代map”,也是一次函數調用處理一個partition的所有數據,而不是一次函數調用處理一條數 據。在實踐中發現,foreachPartitions類的算子,對性能的提升還是很有幫助的。比如在foreach函數中,將RDD中所有數據寫 MySQL,那麼如果是普通的foreach算子,就會一條數據一條數據地寫,每次函數調用可能就會創建一個數據庫連接,此時就勢必會頻繁地創建和銷燬數 據庫連接,性能是非常低下;但是如果用foreachPartitions算子一次性處理一個partition的數據,那麼對於每個 partition,只要創建一個數據庫連接即可,然後執行批量插入操作,此時性能是比較高的。實踐中發現,對於1萬條左右的數據量寫MySQL,性能可 以提升30%以上。

使用filter之後進行coalesce操作

通常對一個RDD執行filter算子過濾掉RDD中較多數據後(比如30%以上的數據),建議使用coalesce算子,手動減少RDD的 partition數量,將RDD中的數據壓縮到更少的partition中去。因爲filter之後,RDD的每個partition中都會有很多數據 被過濾掉,此時如果照常進行後續的計算,其實每個task處理的partition中的數據量並不是很多,有一點資源浪費,而且此時處理的task越多, 可能速度反而越慢。因此用coalesce減少partition數量,將RDD中的數據壓縮到更少的partition之後,只要使用更少的task即 可處理完所有的partition。在某些場景下,對於性能的提升會有一定的幫助。

使用repartitionAndSortWithinPartitions替代repartition與sort類操作

repartitionAndSortWithinPartitions是Spark官網推薦的一個算子,官方建議,如果需要在 repartition重分區之後,還要進行排序,建議直接使用repartitionAndSortWithinPartitions算子。因爲該算子 可以一邊進行重分區的shuffle操作,一邊進行排序。shuffle與sort兩個操作同時進行,比先shuffle再sort來說,性能可能是要高 的。

7、廣播大變量

有時在開發過程中,會遇到需要在算子函數中使用外部變量的場景(尤其是大變量,比如100M以上的大集合),那麼此時就應該使用Spark的廣播(Broadcast)功能來提升性能。

在算子函數中使用到外部變量時,默認情況下,Spark會將該變量複製多個副本,通過網絡傳輸到task中,此時每個task都有一個變量副本。如 果變量本身比較大的話(比如100M,甚至1G),那麼大量的變量副本在網絡中傳輸的性能開銷,以及在各個節點的Executor中佔用過多內存導致的頻 繁GC,都會極大地影響性能。

因此對於上述情況,如果使用的外部變量比較大,建議使用Spark的廣播功能,對該變量進行廣播。廣播後的變量,會保證每個Executor的內存 中,只駐留一份變量副本,而Executor中的task執行時共享該Executor中的那份變量副本。這樣的話,可以大大減少變量副本的數量,從而減 少網絡傳輸的性能開銷,並減少對Executor內存的佔用開銷,降低GC的頻率。


8、使用Kryo優化序列化性能

序列化詳情參考   http://www.jianshu.com/p/e1e19aa51eeb

9、優化數據結構

Java中,有三種類型比較耗費內存:

對象,每個Java對象都有對象頭、引用等額外的信息,因此比較佔用內存空間。

字符串,每個字符串內部都有一個字符數組以及長度等額外信息。

集合類型,比如HashMap、LinkedList等,因爲集合類型內部通常會使用一些內部類來封裝集合元素,比如Map.Entry。

因此Spark官方建議,在Spark編碼實現中,特別是對於算子函數中的代碼,儘量不要使用上述三種數據結構,儘量使用字符串替代對象,使用原始類型(比如Int、Long)替代字符串,使用數組替代集合類型,這樣儘可能地減少內存佔用,從而降低GC頻率,提升性能。

但是在編碼實踐中發現,要做到該原則其實並不容易。因爲我們同時要考慮到代碼的可維護性,如果一個代碼中,完全沒有任何對象抽象,全部是字符 串拼接的方式,那麼對於後續的代碼維護和修改,無疑是一場巨大的災難。同理,如果所有操作都基於數組實現,而不使用HashMap、LinkedList 等集合類型,那麼對於我們的編碼難度以及代碼可維護性,也是一個極大的挑戰。因此建議,在可能以及合適的情況下,使用佔用內存較少的數據結構,但是前 提是要保證代碼的可維護性。

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