版本
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
結果發現一頓操作下來
executor就用起來了一個,寫入task也就只有一個
解析
其實由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)
很好,可以很直觀的看到數據已經劃分爲多個任務,也開始併發執行了。
但是,固定寫死分區數並不是很優雅,這個時候有兩個辦法
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越多越好,對比一下可以發現,劃分合理速度更快
12/4劃分與200/16劃分對比
此外數據庫effectiveTime的月份統計實際不到120條,且存在嚴重的數據傾斜
時間數據傾斜。針對數據傾斜問題,下一章根據repartion源碼解析繼續深入。
動態劃分結果數據傾斜比較嚴重