sparksql運算調優紀事(二)——task併發任務數設置

版本

spark 2.1.0

前言

利用spark-submit提交作業的時候,根據各種天花亂墜的教程我們會指定一大堆參數,藉以提升併發和執行性能,比如

–executor-cores 4
–num-executors 4
–executor-memory 8g
–conf spark.default.parallelism=50
–conf spark.sql.shuffle.partitions=20

結果發現一頓操作下來
任務數只有1
任務數1
executor就用起來了一個,寫入task也就只有一個
???

解析

其實由spark執行原理我們知道
spark

  • 一個executor可以執行多個task
  • 多個executor執行批量task則形成了併發任務集羣

然而現在的情況是總共就兩個task,而其中最耗時的map以及寫入全部集中於一個task中,這樣executor-cores,num-executors完全就沒用嘛!
這個時候就應該從代碼層面進行處理。
首先代碼使用的是sparksql,讀取數據並校驗寫入hbase,操作爲map。
再來看spark.default.parallelism,spark.sql.shuffle.partition這兩個參數幹嘛用的?官網文檔如下

參數 默認值 用途
spark.default.parallelism For distributed shuffle operations like reduceByKeyand join, the largest number of partitions in a parent RDD. For operations like parallelizewith no parent RDDs, it depends on the cluster manager:Local mode: number of cores on the local machine Mesos fine grained mode: 8Others: total number of cores on all executor nodes or 2, whichever is larger Default number of partitions in RDDs returned by transformations like join, reduceByKey, and parallelize when not set by user.
spark.sql.shuffle.partitions 200 Configures the number of partitions to use when shuffling data for joins or aggregations.

由表格以及描述我們可以直到,一個是用於rdd處理的,而sparksql dataframe使用的spark.sql.shuffle.partitions也僅針對於join和aggregations,那這樣確實不起作用,這個時候需要在代碼裏手動指定repartition。

優化

在spark2.1.0裏,repartition有三個方法,分別是

def repartition(numPartitions: Int): Dataset[T] = withTypedPlan {
    Repartition(numPartitions, shuffle = true, logicalPlan)
  }

def repartition(numPartitions: Int, partitionExprs: Column*): Dataset[T] = withTypedPlan {
    RepartitionByExpression(partitionExprs.map(_.expr), logicalPlan, Some(numPartitions))
  }

def repartition(partitionExprs: Column*): Dataset[T] = withTypedPlan {
    RepartitionByExpression(partitionExprs.map(_.expr), logicalPlan, numPartitions = None)
  }

可以看到該方法可以接收int直接指定分區數,也可以通過字段動態/固定分區。我們先用def repartition(numPartitions: Int)這個方法試一下效果,既然服務器運算節點是4個,num-executors也設定爲4,這個時候併發的任務數也設定爲4的倍數,讀取的dataframe先設置12,寫入hbase時也設置爲4。

var repItems = items.repartition(12);
itemsOut.repartition(4).saveAsNewAPIHadoopDataset(itemJob.getConfiguration)

map結果1
寫入結果2
很好,可以很直觀的看到數據已經劃分爲多個任務,也開始併發執行了。
但是,固定寫死分區數並不是很優雅,這個時候有兩個辦法
1、我們可以通過環境變量或是腳本參數動態指定拉取分區數;
2、通過repartition(partitionExprs: Column*)方法,根據字段動態分配。

接下來採用動態分區試一下,我使用時間字段effectiveTime來根據月份進行劃分,同時還需要自己編寫udf進行規則處理

val timeRepartition = (effectiveTime:Date) => {
      var timeflag = "";
      if(effectiveTime==null){
        timeflag = "2020-01"
      }else{
        timeflag= effectiveTime.getYear+""+"-"+effectiveTime.getMonth+"";
      }
      timeflag
    }
    val timeRep = udf(timeRepartition)
var repItems = items.repartition(timeRep(items("effective_time")));

執行結果如下,可以看出劃分出了默認的200個task,且實際上不是task越多越好,對比一下可以發現,劃分合理速度更快
200task
12/4劃分與200/16劃分對比
12/4劃分
200/16動態劃分
此外數據庫effectiveTime的月份統計實際不到120條,且存在嚴重的數據傾斜
時間數據傾斜。針對數據傾斜問題,下一章根據repartion源碼解析繼續深入。

動態劃分結果數據傾斜比較嚴重
數據傾斜

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