2018-11-07 Spark應用程序開發參數調優深入剖析-Spark商業調優實戰

本套系列博客從真實商業環境抽取案例進行總結和分享,並給出Spark商業應用實戰指導,請持續關注本套博客。版權聲明:本套Spark商業應用實戰歸作者(秦凱新)所有,禁止轉載,歡迎學習。

  • Spark商業應用實戰-Spark數據傾斜案例測試及調優準則深入剖析
  • Spark商業應用實戰-Spark資源調度參數調優深入剖析
  • Spark商業應用實戰-Spark應用程序開發參數調優深入剖析
  • Spark商業應用實戰-Spark Shuffle 過程參數優化深入剖析

1 Spark內部資源關係

<figure>[圖片上傳失敗...(image-c5999f-1541596619358)]

<figcaption></figcaption>

</figure>

2 Spark運行資源優化配置

    ./bin/spark-submit \  
    --master yarn-cluster \  
    --num-executors 100 \  
    --executor-memory 6G \ 
    --executor-cores 4 \
    --driver-memory 1G \
    --conf spark.default.parallelism=1000 \
    --conf spark.storage.memoryFraction=0.5 \  
    --conf spark.shuffle.memoryFraction=0.3 \
複製代碼

3 Spark 算子調優建議

  • 程序開發調優 :避免創建重複的RDD val rdd1 = sc.textFile("hdfs://master01:9000/hello.txt") rdd1.map(...) val rdd2 = sc.textFile("hdfs://master01:9000/hello.txt") rdd2.reduce(...) 複製代碼

需要對名爲“hello.txt”的HDFS文件進行一次map操作,再進行一次reduce操作。 也就是說,需要對一份數據執行兩次算子操作。 錯誤的做法:對於同一份數據執行多次算子操作時,創建多個RDD。 這裏執行了兩次textFile方法,針對同一個HDFS文件,創建了兩個RDD出來 ,然後分別對每個RDD都執行了一個算子操作。 這種情況下,Spark需要從HDFS上兩次加載hello.txt文件的內容,並創建兩個單獨的RDD; 第二次加載HDFS文件以及創建RDD的性能開銷,很明顯是白白浪費掉的。


  • 程序開發調優 :儘可能複用同一個RDD
  • 錯誤的做法: 有一個<long , String>格式的RDD,即rdd1。 接着由於業務需要,對rdd1執行了一個map操作,創建了一個rdd2, 而rdd2中的數據僅僅是rdd1中的value值而已,也就是說,rdd2是rdd1的子集。 JavaPairRDD<long , String> rdd1 = ... JavaRDD rdd2 = rdd1.map(...)

分別對rdd1和rdd2執行了不同的算子操作。

    rdd1.reduceByKey(...)
    rdd2.map(...)
複製代碼
  • 正確的做法:

rdd2的數據完全就是rdd1的子集而已,卻創建了兩個rdd,並對兩個rdd都執行了一次算子操作。 此時會因爲對rdd1執行map算子來創建rdd2,而多執行一次算子操作,進而增加性能開銷。 其實在這種情況下完全可以複用同一個RDD。 我們可以使用rdd1,既做reduceByKey操作,也做map操作。

JavaPairRDD<long , String> rdd1 = ...
rdd1.reduceByKey(...)
rdd1.map(tuple._2...)
複製代碼

  • 程序開發調優 :對多次使用的RDD進行持久化 // 正確的做法。 // cache()方法表示:使用非序列化的方式將RDD中的數據全部嘗試持久化到內存中。 // 此時再對rdd1執行兩次算子操作時,只有在第一次執行map算子時,纔會將這個rdd1從源頭處計算一次。 // 第二次執行reduce算子時,就會直接從內存中提取數據進行計算,不會重複計算一個rdd。 val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").cache() rdd1.map(...) rdd1.reduce(...) 正確的做法: // 序列化的方式可以減少持久化的數據對內存/磁盤的佔用量,進而避免內存被持久化數據佔用過多, //從而發生頻繁GC。 val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt") .persist(StorageLevel.MEMORY_AND_DISK_SER) rdd1.map(...) rdd1.reduce(...) 複製代碼

通常不建議使用DISK_ONLY和後綴爲_2的級別:因爲完全基於磁盤文件進行數據的讀寫,會導致性能急劇降低,已經網絡較大開銷


  • 程序開發調優 :儘量避免使用shuffle類算子

如果有可能的話,要儘量避免使用shuffle類算子,最消耗性能的地方就是shuffle過程。

shuffle過程中,各個節點上的相同key都會先寫入本地磁盤文件中,然後其他節點需要通過網絡傳輸拉取各個節點上的磁盤文件中的相同key。而且相同key都拉取到同一個節點進行聚合操作時,還有可能會因爲一個節點上處理的key過多,導致內存不夠存放,進而溢寫到磁盤文件中。因此在shuffle過程中,可能會發生大量的磁盤文件讀寫的IO操作,以及數據的網絡傳輸操作。磁盤IO和網絡數據傳輸也是shuffle性能較差的主要原因。

    儘可能避免使用reduceByKey、join、distinct、repartition等會進行shuffle的算子,儘量使用map類的非shuffle算子。
    // 傳統的join操作會導致shuffle操作。
    // 因爲兩個RDD中,相同的key都需要通過網絡拉取到一個節點上,由一個task進行join操作。
       val rdd3 = rdd1.join(rdd2)

    // Broadcast+map的join操作,不會導致shuffle操作。
    // 使用Broadcast將一個數據量較小的RDD作爲廣播變量。

    // 注意,以上操作,建議僅僅在rdd2的數據量比較少(比如幾百M,或者一兩G)的情況下使用。
    // 因爲每個Executor的內存中,都會駐留一份rdd2的全量數據。
    val rdd2Data = rdd2.collect()
    val rdd2DataBroadcast = sc.broadcast(rdd2Data)
    val rdd3 = rdd1.map(rdd2DataBroadcast...)
複製代碼

  • 程序開發調優 :使用map-side預聚合的shuffle操作

如果因爲業務需要,一定要使用shuffle操作,無法用map類的算子來替代,那麼儘量使用可以map-side預聚合的算子 類似於MapReduce中的本地combiner。map-side預聚合之後,每個節點本地就只會有一條相同的key,因爲多條相同的key都被聚合起來了。其他節點在拉取所有節點上的相同key時,就會大大減少需要拉取的數據數量,從而也就減少了磁盤IO以及網絡傳輸開銷。

建議使用reduceByKey或者aggregateByKey算子來替代掉groupByKey算子

<figure>[圖片上傳失敗...(image-fe3f56-1541596619357)]

<figcaption></figcaption>

</figure>


  • 程序開發調優 :使用高性能的算子
  • 使用reduceByKey/aggregateByKey替代groupByKey : map-side
  • 使用mapPartitions替代普通map : 函數執行頻率
  • 使用foreachPartitions替代foreach : 函數執行頻率
  • 使用filter之後進行coalesce操作 : filter後對分區進行壓縮
  • 使用repartitionAndSortWithinPartitions替代repartition與sort類操作

repartitionAndSortWithinPartitions是Spark官網推薦的一個算子,官方建議,如果需要在repartition重分區之後,還要進行排序,建議直接使用repartitionAndSortWithinPartitions算子


  • 程序開發調優 :廣播大變量

有時在開發過程中,會遇到需要在算子函數中使用外部變量的場景(尤其是大變量,比如100M以上的大集合),那麼此時就應該使用Spark的廣播(Broadcast)功能來提升性能。 默認情況下,Spark會將該變量複製多個副本,通過網絡傳輸到task中,此時每個task都有一個變量副本。如果變量本身比較大的話(比如100M,甚至1G),那麼大量的變量副本在網絡中傳輸的性能開銷,以及在各個節點的Executor中佔用過多內存導致的頻繁GC,都會極大地影響性能。 廣播後的變量,會保證每個Executor的內存中,只駐留一份變量副本,而Executor中的task執行時共享該Executor中的那份變量副本。


  • 程序開發調優 :使用Kryo優化序列化性能
  • 1、在算子函數中使用到外部變量時,該變量會被序列化後進行網絡傳輸。
  • 2、將自定義的類型作爲RDD的泛型類型時(比如JavaRDD,Student是自定義類型),所有自定義類型對象,都會進行序列化。因此這種情況下,也要求自定義的類必須實現Serializable接口。
  • 3、使用可序列化的持久化策略時(比如MEMORY_ONLY_SER),Spark會將RDD中的每個partition都序列化成一個大的字節數組。

Spark默認使用的是Java的序列化機制,你可以使用Kryo作爲序列化類庫,效率要比 Java的序列化機制要高:

        // 創建SparkConf對象。
        val conf = new SparkConf().setMaster(...).setAppName(...)
        // 設置序列化器爲KryoSerializer。
        conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
        // 註冊要序列化的自定義類型。
        conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))
複製代碼

  • 程序開發調優 :優化數據結構

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

  • 1、對象,每個Java對象都有對象頭、引用等額外的信息,因此比較佔用內存空間。
  • 2、字符串,每個字符串內部都有一個字符數組以及長度等額外信息。
  • 3、集合類型,比如HashMap、LinkedList等,因爲集合類型內部通常會使用一些內部類來封裝集合元素,比如Map.Entry Spark官方建議,在Spark編碼實現中,特別是對於算子函數中的代碼,儘量不要使用上述三種數據結構,儘量使用字符串替代對象,使用原始類型(比如Int、Long)替代字符串,使用數組替代集合類型,這樣儘可能地減少內存佔用,從而降低GC頻率,提升性能。

4 總結

因爲開發程序調優相對成熟,所以在此參考大牛的筆記,加上自己的總結,一氣呵成。

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