spark 優(yōu)化 分析方向 (性能調(diào)優(yōu))

第1章 Spark 性能調(diào)優(yōu)

1.1 常規(guī)性能調(diào)優(yōu)

1.1.1 常規(guī)性能調(diào)優(yōu)一:最優(yōu)資源配置

     Spark性能調(diào)優(yōu)的第一步,就是為任務(wù)分配更多的資源,在一定范圍內(nèi),增加資源的分配與性能的提升是成正比的,實(shí)現(xiàn)了最優(yōu)的資源配置后,在此基礎(chǔ)上再考慮進(jìn)行后面論述的性能調(diào)優(yōu)策略。資源的分配在使用腳本提交Spark任務(wù)時(shí)進(jìn)行指定,標(biāo)準(zhǔn)的Spark任務(wù)提交腳本如下所示:
bin/spark-submit \
--class com.test.spark.Analysis \
--master yarn
--deploy-mode cluster
--num-executors 80 \
--driver-memory 6g \
--executor-memory 6g \
--executor-cores 3 \
/usr/opt/modules/spark/jar/spark.jar \

可以進(jìn)行分配的資源如表所示:

名稱 說明
--num-executors 配置Executor的數(shù)量
--driver-memory 配置Driver內(nèi)存(影響不大)
--executor-memory 配置每個(gè)Executor的內(nèi)存大小
--executor-cores 配置每個(gè)Executor的CPU core數(shù)量

調(diào)節(jié)原則:盡量將任務(wù)分配的資源調(diào)節(jié)到可以使用的資源的最大限度。
對(duì)于具體資源的分配,我們分別討論Spark的兩種Cluster運(yùn)行模式:

  • 第一種是Spark Standalone模式,你在提交任務(wù)前,一定知道或者可以從運(yùn)維部門獲取到你可以使用的資源情況,在編寫submit腳本的時(shí)候,就根據(jù)可用的資源情況進(jìn)行資源的分配,比如說集群有15臺(tái)機(jī)器,每臺(tái)機(jī)器為8G內(nèi)存,2個(gè)CPU core,那么就指定15個(gè)Executor,每個(gè)Executor分配8G內(nèi)存,2個(gè)CPU core。
  • 第二種是Spark Yarn模式,由于Yarn使用資源隊(duì)列進(jìn)行資源的分配和調(diào)度,在編寫submit腳本的時(shí)候,就根據(jù)Spark作業(yè)要提交到的資源隊(duì)列,進(jìn)行資源的分配,比如資源隊(duì)列有400G內(nèi)存,100個(gè)CPU core,那么指定50個(gè)Executor,每個(gè)Executor分配8G內(nèi)存,2個(gè)CPU core。

對(duì)各項(xiàng)資源進(jìn)行了調(diào)節(jié)后,得到的性能提升會(huì)有如下表現(xiàn):

增加Executor個(gè)數(shù) 在資源允許的情況下,增加Executor的個(gè)數(shù)可以提高執(zhí)行task的并行度。比如有4個(gè)Executor,每個(gè)Executor有2個(gè)CPU core,那么可以并行執(zhí)行8個(gè)task,如果將Executor的個(gè)數(shù)增加到8個(gè)(資源允許的情況下),那么可以并行執(zhí)行16個(gè)task,此時(shí)的并行能力提升了一倍。

增加每個(gè)Executor的CPU core個(gè)數(shù) 在資源允許的情況下,增加每個(gè)Executor的Cpu core個(gè)數(shù),可以提高執(zhí)行task的并行度。比如有4個(gè)Executor,每個(gè)Executor有2個(gè)CPU core,那么可以并行執(zhí)行8個(gè)task,如果將每個(gè)Executor的CPU core個(gè)數(shù)增加到4個(gè)(資源允許的情況下),那么可以并行執(zhí)行16個(gè)task,此時(shí)的并行能力提升了一倍。

增加每個(gè)Executor的內(nèi)存量 在資源允許的情況下,增加每個(gè)Executor的內(nèi)存量以后,對(duì)性能的提升有三點(diǎn):

  1. 可以緩存更多的數(shù)據(jù)(即對(duì)RDD進(jìn)行cache),寫入磁盤的數(shù)據(jù)相應(yīng)減少,甚至可以不寫入磁盤,減少了可能的磁盤IO;
  2. 可以為shuffle操作提供更多內(nèi)存,即有更多空間來存放reduce端拉取的數(shù)據(jù),寫入磁盤的數(shù)據(jù)相應(yīng)減少,甚至可以不寫入磁盤,減少了可能的磁盤IO;
  3. 可以為task的執(zhí)行提供更多內(nèi)存,在task的執(zhí)行過程中可能創(chuàng)建很多對(duì)象,內(nèi)存較小時(shí)會(huì)引發(fā)頻繁的GC,增加內(nèi)存后,可以避免頻繁的GC,提升整體性能。
補(bǔ)充:生產(chǎn)環(huán)境Spark submit腳本配置
bin/spark-submit \
--class com.test.spark.WordCount \
--master yarn\
--deploy-mode cluster\
--num-executors 80 \
--driver-memory 6g \
--executor-memory 6g \
--executor-cores 3 \
--queue root.default \
--conf spark.yarn.executor.memoryOverhead=2048 \
--conf spark.core.connection.ack.wait.timeout=300 \
/usr/local/spark/spark.jar

參數(shù)配置參考值:
    --num-executors:50~100
    --driver-memory:1G~5G
    --executor-memory:6G~10G
    --executor-cores:3
    --master:實(shí)際生產(chǎn)環(huán)境一定使用yarn

1.1.2 常規(guī)性能調(diào)優(yōu)二:RDD優(yōu)化

1 RDD復(fù)用
在對(duì)RDD進(jìn)行算子時(shí),要避免相同的算子和計(jì)算邏輯之下對(duì)RDD進(jìn)行重復(fù)的計(jì)算

image.png

對(duì)上圖中的RDD計(jì)算架構(gòu)進(jìn)行修改,得到如下圖所示的優(yōu)化結(jié)果:

image.png

2 RDD持久化
在Spark中,當(dāng)多次對(duì)同一個(gè)RDD執(zhí)行算子操作時(shí),每一次都會(huì)對(duì)這個(gè)RDD以之前的父RDD重新計(jì)算一次,這種情況是必須要避免的,對(duì)同一個(gè)RDD的重復(fù)計(jì)算是對(duì)資源的極大浪費(fèi),因此,必須對(duì)多次使用的RDD進(jìn)行持久化,通過持久化將公共RDD的數(shù)據(jù)緩存到內(nèi)存/磁盤中,之后對(duì)于公共RDD的計(jì)算都會(huì)從內(nèi)存/磁盤中直接獲取RDD數(shù)據(jù)。
對(duì)于RDD的持久化,有兩點(diǎn)需要說明:

  1. RDD的持久化是可以進(jìn)行序列化的,當(dāng)內(nèi)存無法將RDD的數(shù)據(jù)完整的進(jìn)行存放的時(shí)候,可以考慮使用序列化的方式減小數(shù)據(jù)體積,將數(shù)據(jù)完整存儲(chǔ)在內(nèi)存中

2.如果對(duì)于數(shù)據(jù)的可靠性要求很高并且內(nèi)存充足可以使用副本機(jī)制,對(duì)RDD數(shù)據(jù)進(jìn)行持久化。當(dāng)持久化啟用了復(fù)本機(jī)制時(shí),對(duì)于持久化的每個(gè)數(shù)據(jù)單元都存儲(chǔ)一個(gè)副本,放在其他節(jié)點(diǎn)上面,由此實(shí)現(xiàn)數(shù)據(jù)的容錯(cuò),一旦一個(gè)副本數(shù)據(jù)丟失,不需要重新計(jì)算,還可以使用另外一個(gè)副本

3 RDD盡可能早的filter操作
獲取到初始RDD后,應(yīng)該考慮盡早地過濾掉不需要的數(shù)據(jù),進(jìn)而減少對(duì)內(nèi)存的占用,從而提升Spark作業(yè)的運(yùn)行效率

1.1.3 常規(guī)性能調(diào)優(yōu)三:并行度調(diào)節(jié)

Spark作業(yè)中的并行度指各個(gè)stage的task的數(shù)量。
如果并行度設(shè)置不合理而導(dǎo)致并行度過低,會(huì)導(dǎo)致資源的極大浪費(fèi),例如,20個(gè)Executor,每個(gè)Executor分配3個(gè)CPU core,而Spark作業(yè)有40個(gè)task,這樣每個(gè)Executor分配到的task個(gè)數(shù)是2個(gè),這就使得每個(gè)Executor有一個(gè)CPU core空閑,導(dǎo)致資源的浪費(fèi)。
理想的并行度設(shè)置,應(yīng)該是讓并行度與資源相匹配,簡單來說就是在資源允許的前提下,并行度要設(shè)置的盡可能大,達(dá)到可以充分利用集群資源。合理的設(shè)置并行度,可以提升整個(gè)Spark作業(yè)的性能和運(yùn)行速度。
Spark官方推薦,task數(shù)量應(yīng)該設(shè)置為Spark作業(yè)總CPU core數(shù)量的2~3倍。之所以沒有推薦task數(shù)量與CPU core總數(shù)相等,是因?yàn)閠ask的執(zhí)行時(shí)間不同,有的task執(zhí)行速度快而有的task執(zhí)行速度慢,如果task數(shù)量與CPU core總數(shù)相等,那么執(zhí)行快的task執(zhí)行完成后,會(huì)出現(xiàn)CPU core空閑的情況。如果task數(shù)量設(shè)置為CPU core總數(shù)的2~3倍,那么一個(gè)task執(zhí)行完畢后,CPU core會(huì)立刻執(zhí)行下一個(gè)task,降低了資源的浪費(fèi),同時(shí)提升了Spark作業(yè)運(yùn)行的效率。

Spark作業(yè)并行度的設(shè)置如下所示:
val conf = new SparkConf().set("spark.default.parallelism", "500")

1.1.4 常規(guī)性能調(diào)優(yōu)四:廣播大變量

默認(rèn)情況下,task中的算子中如果使用了外部的變量,每個(gè)task都會(huì)獲取一份變量的復(fù)本,這就造成了內(nèi)存的極大消耗。一方面,如果后續(xù)對(duì)RDD進(jìn)行持久化,可能就無法將RDD數(shù)據(jù)存入內(nèi)存,只能寫入磁盤,磁盤IO將會(huì)嚴(yán)重消耗性能;另一方面,task在創(chuàng)建對(duì)象的時(shí)候,也許會(huì)發(fā)現(xiàn)堆內(nèi)存無法存放新創(chuàng)建的對(duì)象,這就會(huì)導(dǎo)致頻繁的GC,GC會(huì)導(dǎo)致工作線程停止,進(jìn)而導(dǎo)致Spark暫停工作一段時(shí)間,嚴(yán)重影響Spark性能。
假設(shè)當(dāng)前任務(wù)配置了20個(gè)Executor,指定500個(gè)task,有一個(gè)20M的變量被所有task共用,此時(shí)會(huì)在500個(gè)task中產(chǎn)生500個(gè)副本,耗費(fèi)集群10G的內(nèi)存,如果使用了廣播變量, 那么每個(gè)Executor保存一個(gè)副本,一共消耗400M內(nèi)存,內(nèi)存消耗減少了5倍。
廣播變量在每個(gè)Executor保存一個(gè)副本,此Executor的所有task共用此廣播變量,這讓變量產(chǎn)生的副本數(shù)量大大減少。
在初始階段,廣播變量只在Driver中有一份副本。task在運(yùn)行的時(shí)候,想要使用廣播變量中的數(shù)據(jù),此時(shí)首先會(huì)在自己本地的Executor對(duì)應(yīng)的BlockManager中嘗試獲取變量,如果本地沒有,BlockManager就會(huì)從Driver或者其他節(jié)點(diǎn)的BlockManager上遠(yuǎn)程拉取變量的復(fù)本,并由本地的BlockManager進(jìn)行管理;之后此Executor的所有task都會(huì)直接從本地的BlockManager中獲取變量。

1.1.5 常規(guī)性能調(diào)優(yōu)五:Kryo序列化

默認(rèn)情況下,Spark使用Java的序列化機(jī)制。Java的序列化機(jī)制使用方便,不需要額外的配置,在算子中使用的變量實(shí)現(xiàn)Serializable接口即可,但是,Java序列化機(jī)制的效率不高,序列化速度慢并且序列化后的數(shù)據(jù)所占用的空間依然較大。
Kryo序列化機(jī)制比Java序列化機(jī)制性能提高10倍左右,Spark之所以沒有默認(rèn)使用Kryo作為序列化類庫,是因?yàn)樗恢С炙袑?duì)象的序列化,同時(shí)Kryo需要用戶在使用前注冊(cè)需要序列化的類型,不夠方便,但從Spark 2.0.0版本開始,簡單類型、簡單類型數(shù)組、字符串類型的Shuffling RDDs 已經(jīng)默認(rèn)使用Kryo序列化方式了。

public class MyKryoRegistrator implements KryoRegistrator
{
  @Override
  public void registerClasses(Kryo kryo)
  {
    kryo.register(StartupReportLogs.class);
  }
}

配置Kryo序列化方式的實(shí)例代碼:

//創(chuàng)建SparkConf對(duì)象
val conf = new SparkConf().setMaster(…).setAppName(…)
//使用Kryo序列化庫,如果要使用Java序列化庫,需要把該行屏蔽掉
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer");  
//在Kryo序列化庫中注冊(cè)自定義的類集合,如果要使用Java序列化庫,需要把該行屏蔽掉
conf.set("spark.kryo.registrator", "atguigu.com.MyKryoRegistrator");

1.1.6 常規(guī)性能調(diào)優(yōu)六:調(diào)節(jié)本地化等待時(shí)長

Spark作業(yè)運(yùn)行過程中,Driver會(huì)對(duì)每一個(gè)stage的task進(jìn)行分配。根據(jù)Spark的task分配算法,Spark希望task能夠運(yùn)行在它要計(jì)算的數(shù)據(jù)算在的節(jié)點(diǎn)(數(shù)據(jù)本地化思想),這樣就可以避免數(shù)據(jù)的網(wǎng)絡(luò)傳輸。通常來說,task可能不會(huì)被分配到它處理的數(shù)據(jù)所在的節(jié)點(diǎn),因?yàn)檫@些節(jié)點(diǎn)可用的資源可能已經(jīng)用盡,此時(shí),Spark會(huì)等待一段時(shí)間,默認(rèn)3s,如果等待指定時(shí)間后仍然無法在指定節(jié)點(diǎn)運(yùn)行,那么會(huì)自動(dòng)降級(jí),嘗試將task分配到比較差的本地化級(jí)別所對(duì)應(yīng)的節(jié)點(diǎn)上,比如將task分配到離它要計(jì)算的數(shù)據(jù)比較近的一個(gè)節(jié)點(diǎn),然后進(jìn)行計(jì)算,如果當(dāng)前級(jí)別仍然不行,那么繼續(xù)降級(jí)。
當(dāng)task要處理的數(shù)據(jù)不在task所在節(jié)點(diǎn)上時(shí),會(huì)發(fā)生數(shù)據(jù)的傳輸。task會(huì)通過所在節(jié)點(diǎn)的BlockManager獲取數(shù)據(jù),BlockManager發(fā)現(xiàn)數(shù)據(jù)不在本地時(shí),戶通過網(wǎng)絡(luò)傳輸組件從數(shù)據(jù)所在節(jié)點(diǎn)的BlockManager處獲取數(shù)據(jù)。
網(wǎng)絡(luò)傳輸數(shù)據(jù)的情況是我們不愿意看到的,大量的網(wǎng)絡(luò)傳輸會(huì)嚴(yán)重影響性能,因此,我們希望通過調(diào)節(jié)本地化等待時(shí)長,如果在等待時(shí)長這段時(shí)間內(nèi),目標(biāo)節(jié)點(diǎn)處理完成了一部分task,那么當(dāng)前的task將有機(jī)會(huì)得到執(zhí)行,這樣就能夠改善Spark作業(yè)的整體性能。
Spark的本地化等級(jí)如表所示:

名稱                          解析
PROCESS_LOCAL   進(jìn)程本地化,task和數(shù)據(jù)在同一個(gè)Executor中,性能最好。
NODE_LOCAL          節(jié)點(diǎn)本地化,task和數(shù)據(jù)在同一個(gè)節(jié)點(diǎn)中,但是task和數(shù)據(jù)不在同一個(gè)Executor中,數(shù)據(jù)需要在進(jìn)程間進(jìn)行傳輸。
RACK_LOCAL         機(jī)架本地化,task和數(shù)據(jù)在同一個(gè)機(jī)架的兩個(gè)節(jié)點(diǎn)上,數(shù)據(jù)需要通過網(wǎng)絡(luò)在節(jié)點(diǎn)之間進(jìn)行傳輸。
NO_PREF                對(duì)于task來說,從哪里獲取都一樣,沒有好壞之分。
ANY                                task和數(shù)據(jù)可以在集群的任何地方,而且不在一個(gè)機(jī)架中,性能最差。

在Spark項(xiàng)目開發(fā)階段,可以使用client模式對(duì)程序進(jìn)行測試,此時(shí),可以在本地看到比較全的日志信息,日志信息中有明確的task數(shù)據(jù)本地化的級(jí)別,如果大部分都是PROCESS_LOCAL,那么就無需進(jìn)行調(diào)節(jié),但是如果發(fā)現(xiàn)很多的級(jí)別都是NODE_LOCAL、ANY,那么需要對(duì)本地化的等待時(shí)長進(jìn)行調(diào)節(jié),通過延長本地化等待時(shí)長,看看task的本地化級(jí)別有沒有提升,并觀察Spark作業(yè)的運(yùn)行時(shí)間有沒有縮短。
注意,過猶不及,不要將本地化等待時(shí)長延長地過長,導(dǎo)致因?yàn)榇罅康牡却龝r(shí)長,使得Spark作業(yè)的運(yùn)行時(shí)間反而增加了

spark本地化等待時(shí)長的設(shè)置如代碼所示:
val conf = new SparkConf() .set("spark.locality.wait", "6")

1.2 算子調(diào)優(yōu)

1.2.1 算子調(diào)優(yōu)一:mapPartitions

普通的map算子對(duì)RDD中的每一個(gè)元素進(jìn)行操作,而mapPartitions算子對(duì)RDD中每一個(gè)分區(qū)進(jìn)行操作。如果是普通的map算子,假設(shè)一個(gè)partition有1萬條數(shù)據(jù),那么map算子中的function要執(zhí)行1萬次,也就是對(duì)每個(gè)元素進(jìn)行操作。


image.png

如果是mapPartition算子,由于一個(gè)task處理一個(gè)RDD的partition,那么一個(gè)task只會(huì)執(zhí)行一次function,function一次接收所有的partition數(shù)據(jù),效率比較高。

image.png

比如,當(dāng)要把RDD中的所有數(shù)據(jù)通過JDBC寫入數(shù)據(jù),如果使用map算子,那么需要對(duì)RDD中的每一個(gè)元素都創(chuàng)建一個(gè)數(shù)據(jù)庫連接,這樣對(duì)資源的消耗很大,如果使用mapPartitions算子,那么針對(duì)一個(gè)分區(qū)的數(shù)據(jù),只需要建立一個(gè)數(shù)據(jù)庫連接。
mapPartitions算子也存在一些缺點(diǎn):對(duì)于普通的map操作,一次處理一條數(shù)據(jù),如果在處理了2000條數(shù)據(jù)后內(nèi)存不足,那么可以將已經(jīng)處理完的2000條數(shù)據(jù)從內(nèi)存中垃圾回收掉;但是如果使用mapPartitions算子,但數(shù)據(jù)量非常大時(shí),function一次處理一個(gè)分區(qū)的數(shù)據(jù),如果一旦內(nèi)存不足,此時(shí)無法回收內(nèi)存,就可能會(huì)OOM,即內(nèi)存溢出。
因此,mapPartitions算子適用于數(shù)據(jù)量不是特別大的時(shí)候,此時(shí)使用mapPartitions算子對(duì)性能的提升效果還是不錯(cuò)的。(當(dāng)數(shù)據(jù)量很大的時(shí)候,一旦使用mapPartitions算子,就會(huì)直接OOM)
在項(xiàng)目中,應(yīng)該首先估算一下RDD的數(shù)據(jù)量、每個(gè)partition的數(shù)據(jù)量,以及分配給每個(gè)Executor的內(nèi)存資源,如果資源允許,可以考慮使用mapPartitions算子代替map。

1.2.2 算子調(diào)優(yōu)二:foreachPartition優(yōu)化數(shù)據(jù)庫操作

在生產(chǎn)環(huán)境中,通常使用foreachPartition算子來完成數(shù)據(jù)庫的寫入,通過foreachPartition算子的特性,可以優(yōu)化寫數(shù)據(jù)庫的性能。
如果使用foreach算子完成數(shù)據(jù)庫的操作,由于foreach算子是遍歷RDD的每條數(shù)據(jù),因此,每條數(shù)據(jù)都會(huì)建立一個(gè)數(shù)據(jù)庫連接,這是對(duì)資源的極大浪費(fèi),因此,對(duì)于寫數(shù)據(jù)庫操作,我們應(yīng)當(dāng)使用foreachPartition算子。
與mapPartitions算子非常相似,foreachPartition是將RDD的每個(gè)分區(qū)作為遍歷對(duì)象,一次處理一個(gè)分區(qū)的數(shù)據(jù),也就是說,如果涉及數(shù)據(jù)庫的相關(guān)操作,一個(gè)分區(qū)的數(shù)據(jù)只需要?jiǎng)?chuàng)建一次數(shù)據(jù)庫連接,如圖所示:

image.png

使用了foreachPartition算子后,可以獲得以下的性能提升:

  1. 對(duì)于我們寫的function函數(shù),一次處理一整個(gè)分區(qū)的數(shù)據(jù);
  2. 對(duì)于一個(gè)分區(qū)內(nèi)的數(shù)據(jù),創(chuàng)建唯一的數(shù)據(jù)庫連接;
  3. 只需要向數(shù)據(jù)庫發(fā)送一次SQL語句和多組參數(shù);
    在生產(chǎn)環(huán)境中,全部都會(huì)使用foreachPartition算子完成數(shù)據(jù)庫操作。foreachPartition算子存在一個(gè)問題,與mapPartitions算子類似,如果一個(gè)分區(qū)的數(shù)據(jù)量特別大,可能會(huì)造成OOM,即內(nèi)存溢出。

1.2.3 算子調(diào)優(yōu)三:filter與coalesce的配合使用

在Spark任務(wù)中我們經(jīng)常會(huì)使用filter算子完成RDD中數(shù)據(jù)的過濾,在任務(wù)初始階段,從各個(gè)分區(qū)中加載到的數(shù)據(jù)量是相近的,但是一旦進(jìn)過filter過濾后,每個(gè)分區(qū)的數(shù)據(jù)量有可能會(huì)存在較大差異,如圖所示:

image.png

根據(jù)圖中信息我們可以發(fā)現(xiàn)兩個(gè)問題:

  1. 每個(gè)partition的數(shù)據(jù)量變小了,如果還按照之前與partition相等的task個(gè)數(shù)去處理當(dāng)前數(shù)據(jù),有點(diǎn)浪費(fèi)task的計(jì)算資源;
  2. 每個(gè)partition的數(shù)據(jù)量不一樣,會(huì)導(dǎo)致后面的每個(gè)task處理每個(gè)partition數(shù)據(jù)的時(shí)候,每個(gè)task要處理的數(shù)據(jù)量不同,這很有可能導(dǎo)致數(shù)據(jù)傾斜問題。如上圖所示,第二個(gè)分區(qū)的數(shù)據(jù)過濾后只剩100條,而第三個(gè)分區(qū)的數(shù)據(jù)過濾后剩下800條,在相同的處理邏輯下,第二個(gè)分區(qū)對(duì)應(yīng)的task處理的數(shù)據(jù)量與第三個(gè)分區(qū)對(duì)應(yīng)的task處理的數(shù)據(jù)量差距達(dá)到了8倍,這也會(huì)導(dǎo)致運(yùn)行速度可能存在數(shù)倍的差距,這也就是數(shù)據(jù)傾斜問題。

針對(duì)上述的兩個(gè)問題,我們分別進(jìn)行分析:

  1. 針對(duì)第一個(gè)問題,既然分區(qū)的數(shù)據(jù)量變小了,我們希望可以對(duì)分區(qū)數(shù)據(jù)進(jìn)行重新分配,比如將原來4個(gè)分區(qū)的數(shù)據(jù)轉(zhuǎn)化到2個(gè)分區(qū)中,這樣只需要用后面的兩個(gè)task進(jìn)行處理即可,避免了資源的浪費(fèi)。
  2. 針對(duì)第二個(gè)問題,解決方法和第一個(gè)問題的解決方法非常相似,對(duì)分區(qū)數(shù)據(jù)重新分配,讓每個(gè)partition中的數(shù)據(jù)量差不多,這就避免了數(shù)據(jù)傾斜問題。那么具體應(yīng)該如何實(shí)現(xiàn)上面的解決思路?我們需要coalesce算子。repartition與coalesce都可以用來進(jìn)行重分區(qū),其中repartition只是coalesce接口中shuffle為true的簡易實(shí)現(xiàn),coalesce默認(rèn)情況下不進(jìn)行shuffle,但是可以通過參數(shù)進(jìn)行設(shè)置。假設(shè)我們希望將原本的分區(qū)個(gè)數(shù)A通過重新分區(qū)變?yōu)锽,那么有以下幾種情況:
  • A > B(多數(shù)分區(qū)合并為少數(shù)分區(qū))
  1. A與B相差值不大
    此時(shí)使用coalesce即可,無需shuffle過程。
  2. A與B相差值很大
    此時(shí)可以使用coalesce并且不啟用shuffle過程,但是會(huì)導(dǎo)致合并過程性能低下,所以推薦設(shè)置coalesce的第二個(gè)參數(shù)為true,即啟動(dòng)shuffle過程
  • A < B(少數(shù)分區(qū)分解為多數(shù)分區(qū))

此時(shí)使用repartition即可,如果使用coalesce需要將shuffle設(shè)置為true,否則coalesce無效。
我們可以在filter操作之后,使用coalesce算子針對(duì)每個(gè)partition的數(shù)據(jù)量各不相同的情況,壓縮partition的數(shù)量,而且讓每個(gè)partition的數(shù)據(jù)量盡量均勻緊湊,以便于后面的task進(jìn)行計(jì)算操作,在某種程度上能夠在一定程度上提升性能。
注意:local模式是進(jìn)程內(nèi)模擬集群運(yùn)行,已經(jīng)對(duì)并行度和分區(qū)數(shù)量有了一定的內(nèi)部優(yōu)化,因此不用去設(shè)置并行度和分區(qū)數(shù)量

1.2.4 算子調(diào)優(yōu)四:repartition解決SparkSQL低并行度問題

在第一節(jié)的常規(guī)性能調(diào)優(yōu)中我們講解了并行度的調(diào)節(jié)策略,但是,并行度的設(shè)置對(duì)于Spark SQL是不生效的,用戶設(shè)置的并行度只對(duì)于Spark SQL以外的所有Spark的stage生效
Spark SQL的并行度不允許用戶自己指定,Spark SQL自己會(huì)默認(rèn)根據(jù)hive表對(duì)應(yīng)的HDFS文件的split個(gè)數(shù)自動(dòng)設(shè)置Spark SQL所在的那個(gè)stage的并行度,用戶自己通spark.default.parallelism參數(shù)指定的并行度,只會(huì)在沒Spark SQL的stage中生效。
由于Spark SQL所在stage的并行度無法手動(dòng)設(shè)置,如果數(shù)據(jù)量較大,并且此stage中后續(xù)的transformation操作有著復(fù)雜的業(yè)務(wù)邏輯,而Spark SQL自動(dòng)設(shè)置的task數(shù)量很少,這就意味著每個(gè)task要處理為數(shù)不少的數(shù)據(jù)量,然后還要執(zhí)行非常復(fù)雜的處理邏輯,這就可能表現(xiàn)為第一個(gè)有Spark SQL的stage速度很慢,而后續(xù)的沒有Spark SQL的stage運(yùn)行速度非常快。為了解決Spark SQL無法設(shè)置并行度和task數(shù)量的問題,我們可以使用repartition算子。

image.png

1.2.5 算子調(diào)優(yōu)五:reduceByKey預(yù)聚合

reduceByKey相較于普通的shuffle操作一個(gè)顯著的特點(diǎn)就是會(huì)進(jìn)行map端的本地聚合,map端會(huì)先對(duì)本地的數(shù)據(jù)進(jìn)行combine操作,然后將數(shù)據(jù)寫入給下個(gè)stage的每個(gè)task創(chuàng)建的文件中,也就是在map端,對(duì)每一個(gè)key對(duì)應(yīng)的value,執(zhí)行reduceByKey算子函數(shù)。reduceByKey算子的執(zhí)行過程如圖所示:

image.png

使用reduceByKey對(duì)性能的提升如下:

  1. 本地聚合后,在map端的數(shù)據(jù)量變少,減少了磁盤IO,也減少了對(duì)磁盤空間的占用;
  2. 本地聚合后,下一個(gè)stage拉取的數(shù)據(jù)量變少,減少了網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)量;
  3. 本地聚合后,在reduce端進(jìn)行數(shù)據(jù)緩存的內(nèi)存占用減少;
  4. 本地聚合后,在reduce端進(jìn)行聚合的數(shù)據(jù)量減少。

基于reduceByKey的本地聚合特征,我們應(yīng)該考慮使用reduceByKey代替其他的shuffle算子,例如groupByKey。reduceByKey與groupByKey的運(yùn)行原理如圖所示:

image.png

groupByKey原理

image.png

reduceByKey原理

根據(jù)上圖可知,groupByKey不會(huì)進(jìn)行map端的聚合,而是將所有map端的數(shù)據(jù)shuffle到reduce端,然后在reduce端進(jìn)行數(shù)據(jù)的聚合操作。由于reduceByKey有map端聚合的特性,使得網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)量減小,因此效率要明顯高于groupByKey。

1.3 Shuffle調(diào)優(yōu)

1.3.1 Shuffle調(diào)優(yōu)一:調(diào)節(jié)map端緩沖區(qū)大小

在Spark任務(wù)運(yùn)行過程中,如果shuffle的map端處理的數(shù)據(jù)量比較大,但是map端緩沖的大小是固定的,可能會(huì)出現(xiàn)map端緩沖數(shù)據(jù)頻繁spill溢寫到磁盤文件中的情況,使得性能非常低下,通過調(diào)節(jié)map端緩沖的大小,可以避免頻繁的磁盤IO操作,進(jìn)而提升Spark任務(wù)的整體性能。
map端緩沖的默認(rèn)配置是32KB,如果每個(gè)task處理640KB的數(shù)據(jù),那么會(huì)發(fā)生640/32 = 20次溢寫,如果每個(gè)task處理64000KB的數(shù)據(jù),機(jī)會(huì)發(fā)生64000/32=2000此溢寫,這對(duì)于性能的影響是非常嚴(yán)重的。

map端緩沖的配置方法如代碼清單所示:
val conf = new SparkConf() .set("spark.shuffle.file.buffer", "64")

1.3.2 Shuffle調(diào)優(yōu)二:調(diào)節(jié)reduce端拉取數(shù)據(jù)緩沖區(qū)大小

Spark Shuffle過程中,shuffle reduce task的buffer緩沖區(qū)大小決定了reduce task每次能夠緩沖的數(shù)據(jù)量,也就是每次能夠拉取的數(shù)據(jù)量,如果內(nèi)存資源較為充足,適當(dāng)增加拉取數(shù)據(jù)緩沖區(qū)的大小,可以減少拉取數(shù)據(jù)的次數(shù),也就可以減少網(wǎng)絡(luò)傳輸?shù)拇螖?shù),進(jìn)而提升性能。
reduce端數(shù)據(jù)拉取緩沖區(qū)的大小可以通過spark.reducer.maxSizeInFlight參數(shù)進(jìn)行設(shè)置,默認(rèn)為48MB,

該參數(shù)的設(shè)置方法如代碼清單所示:
val conf = new SparkConf().set("spark.reducer.maxSizeInFlight", "96")

1.3.3 Shuffle調(diào)優(yōu)三:調(diào)節(jié)reduce端拉取數(shù)據(jù)重試次數(shù)

Spark Shuffle過程中,reduce task拉取屬于自己的數(shù)據(jù)時(shí),如果因?yàn)榫W(wǎng)絡(luò)異常等原因?qū)е率?huì)自動(dòng)進(jìn)行重試。對(duì)于那些包含了特別耗時(shí)的shuffle操作的作業(yè),建議增加重試最;敗。在實(shí)踐中發(fā)現(xiàn),對(duì)于針對(duì)超大數(shù)據(jù)量(數(shù)十億~上百億)的shuffle過程,調(diào)節(jié)該參數(shù)可以大幅度提升穩(wěn)定性。
reduce端拉取數(shù)據(jù)重試次數(shù)可以通過spark.shuffle.io.maxRetries參數(shù)進(jìn)行設(shè)置,該參數(shù)就代表了可以重試的最大次數(shù)。如果在指定次數(shù)之內(nèi)拉取還是沒有成功,就可能會(huì)導(dǎo)致作業(yè)執(zhí)行失敗,默認(rèn)為3,

該參數(shù)的設(shè)置方法如代碼清單所示:
val conf = new SparkConf().set("spark.shuffle.io.maxRetries", "6")

1.3.4 Shuffle調(diào)優(yōu)四:調(diào)節(jié)reduce端拉取數(shù)據(jù)等待間隔

Spark Shuffle過程中,reduce task拉取屬于自己的數(shù)據(jù)時(shí),如果因?yàn)榫W(wǎng)絡(luò)異常等原因?qū)е率?huì)自動(dòng)進(jìn)行重試,在一次失敗后,會(huì)等待一定的時(shí)間間隔再進(jìn)行重試,可以通過加大間隔時(shí)長(比如60s),以增加shuffle操作的穩(wěn)定性。
reduce端拉取數(shù)據(jù)等待間隔可以通過spark.shuffle.io.retryWait參數(shù)進(jìn)行設(shè)置,默認(rèn)值為5s

該參數(shù)的設(shè)置方法如代碼清單所示:
val conf = new SparkConf().set("spark.shuffle.io.retryWait", "60s")

1.3.5 Shuffle調(diào)優(yōu)五:調(diào)節(jié)SortShuffle排序操作閾值

對(duì)于SortShuffleManager,如果shuffle reduce task的數(shù)量小于某一閾值則shuffle write過程中不會(huì)進(jìn)行排序操作,而是直接按照未經(jīng)優(yōu)化的HashShuffleManager的方式去寫數(shù)據(jù),但是最后會(huì)將每個(gè)task產(chǎn)生的所有臨時(shí)磁盤文件都合并成一個(gè)文件,并會(huì)創(chuàng)建單獨(dú)的索引文件。
當(dāng)你使用SortShuffleManager時(shí),如果的確不需要排序操作,那么建議將這個(gè)參數(shù)調(diào)大一些,大于shuffle read task的數(shù)量,那么此時(shí)map-side就不會(huì)進(jìn)行排序了,減少了排序的性能開銷,但是這種方式下,依然會(huì)產(chǎn)生大量的磁盤文件,因此shuffle write性能有待提高。
SortShuffleManager排序操作閾值的設(shè)置可以通過spark.shuffle.sort. bypassMergeThreshold這一參數(shù)進(jìn)行設(shè)置,默認(rèn)值為200

該參數(shù)的設(shè)置方法如代碼清單所示:
val conf = new SparkConf().set("spark.shuffle.sort.bypassMergeThreshold", "400")

1.4 JVM調(diào)優(yōu)

對(duì)于JVM調(diào)優(yōu),首先應(yīng)該明確,full gc/minor gc,都會(huì)導(dǎo)致JVM的工作線程停止工作,即stop the world。

1.4.1 JVM調(diào)優(yōu)一:降低cache操作的內(nèi)存占比

  1. 靜態(tài)內(nèi)存管理機(jī)制
    根據(jù)Spark靜態(tài)內(nèi)存管理機(jī)制,堆內(nèi)存被劃分為了兩塊,Storage和Execution。Storage主要用于緩存RDD數(shù)據(jù)和broadcast數(shù)據(jù),Execution主要用于緩存在shuffle過程中產(chǎn)生的中間數(shù)據(jù),Storage占系統(tǒng)內(nèi)存的60%,Execution占系統(tǒng)內(nèi)存的20%,并且兩者完全獨(dú)立。
    在一般情況下,Storage的內(nèi)存都提供給了cache操作,但是如果在某些情況下cache操作內(nèi)存不是很緊張,而task的算子中創(chuàng)建的對(duì)象很多,Execution內(nèi)存又相對(duì)較小,這回導(dǎo)致頻繁的minor gc,甚至于頻繁的full gc,進(jìn)而導(dǎo)致Spark頻繁的停止工作,性能影響會(huì)很大。
    在Spark UI中可以查看每個(gè)stage的運(yùn)行情況,包括每個(gè)task的運(yùn)行時(shí)間、gc時(shí)間等等,如果發(fā)現(xiàn)gc太頻繁,時(shí)間太長,就可以考慮調(diào)節(jié)Storage的內(nèi)存占比,讓task執(zhí)行算子函數(shù)式,有更多的內(nèi)存可以使用。
    Storage內(nèi)存區(qū)域可以通過spark.storage.memoryFraction參數(shù)進(jìn)行指定,默認(rèn)為0.6,即60%,可以逐級(jí)向下遞減,
如代碼清單所示:
val conf = new SparkConf().set("spark.storage.memoryFraction", "0.4")

  1. 統(tǒng)一內(nèi)存管理機(jī)制
    根據(jù)Spark統(tǒng)一內(nèi)存管理機(jī)制,堆內(nèi)存被劃分為了兩塊,Storage和Execution。Storage主要用于緩存數(shù)據(jù),Execution主要用于緩存在shuffle過程中產(chǎn)生的中間數(shù)據(jù),兩者所組成的內(nèi)存部分稱為統(tǒng)一內(nèi)存,Storage和Execution各占統(tǒng)一內(nèi)存的50%,由于動(dòng)態(tài)占用機(jī)制的實(shí)現(xiàn),shuffle過程需要的內(nèi)存過大時(shí),會(huì)自動(dòng)占用Storage的內(nèi)存區(qū)域,因此無需手動(dòng)進(jìn)行調(diào)節(jié)。

1.4.2 JVM調(diào)優(yōu)二:調(diào)節(jié)Executor堆外內(nèi)存

Executor的堆外內(nèi)存主要用于程序的共享庫、Perm Space、 線程Stack和一些Memory mapping等, 或者類C方式allocate object。
有時(shí),如果你的Spark作業(yè)處理的數(shù)據(jù)量非常大,達(dá)到幾億的數(shù)據(jù)量,此時(shí)運(yùn)行Spark作業(yè)會(huì)時(shí)不時(shí)地報(bào)錯(cuò),例如shuffle output file cannot find,executor lost,task lost,out of memory等,這可能是Executor的堆外內(nèi)存不太夠用,導(dǎo)致Executor在運(yùn)行的過程中內(nèi)存溢出。
stage的task在運(yùn)行的時(shí)候,可能要從一些Executor中去拉取shuffle map output文件,但是Executor可能已經(jīng)由于內(nèi)存溢出掛掉了,其關(guān)聯(lián)的BlockManager也沒有了,這就可能會(huì)報(bào)出shuffle output file cannot find,executor lost,task lost,out of memory等錯(cuò)誤,此時(shí),就可以考慮調(diào)節(jié)一下Executor的堆外內(nèi)存,也就可以避免報(bào)錯(cuò),與此同時(shí),堆外內(nèi)存調(diào)節(jié)的比較大的時(shí)候,對(duì)于性能來講,也會(huì)帶來一定的提升。
默認(rèn)情況下,Executor堆外內(nèi)存上限大概為300多MB,在實(shí)際的生產(chǎn)環(huán)境下,對(duì)海量數(shù)據(jù)進(jìn)行處理的時(shí)候,這里都會(huì)出現(xiàn)問題,導(dǎo)致Spark作業(yè)反復(fù)崩潰,無法運(yùn)行,此時(shí)就會(huì)去調(diào)節(jié)這個(gè)參數(shù),到至少1G,甚至于2G、4G。

Executor堆外內(nèi)存的配置需要在spark-submit腳本里配置,如代碼清單所示:
--conf spark.yarn.executor.memoryOverhead=2048

以上參數(shù)配置完成后,會(huì)避免掉某些JVM OOM的異常問題,同時(shí),可以提升整體Spark作業(yè)的性能

1.4.3 JVM調(diào)優(yōu)三:調(diào)節(jié)連接等待時(shí)長

在Spark作業(yè)運(yùn)行過程中,Executor優(yōu)先從自己本地關(guān)聯(lián)的BlockManager中獲取某份數(shù)據(jù),如果本地BlockManager沒有的話,會(huì)通過TransferService遠(yuǎn)程連接其他節(jié)點(diǎn)上Executor的BlockManager來獲取數(shù)據(jù)。
如果task在運(yùn)行過程中創(chuàng)建大量對(duì)象或者創(chuàng)建的對(duì)象較大,會(huì)占用大量的內(nèi)存,這回導(dǎo)致頻繁的垃圾回收,但是垃圾回收會(huì)導(dǎo)致工作現(xiàn)場全部停止,也就是說,垃圾回收一旦執(zhí)行,Spark的Executor進(jìn)程就會(huì)停止工作,無法提供相應(yīng),此時(shí),由于沒有響應(yīng),無法建立網(wǎng)絡(luò)連接,會(huì)導(dǎo)致網(wǎng)絡(luò)連接超時(shí)。
在生產(chǎn)環(huán)境下,有時(shí)會(huì)遇到file not found、file lost這類錯(cuò)誤,在這種情況下,很有可能是Executor的BlockManager在拉取數(shù)據(jù)的時(shí)候,無法建立連接,然后超過默認(rèn)的連接等待時(shí)長60s后,宣告數(shù)據(jù)拉取失敗,如果反復(fù)嘗試都拉取不到數(shù)據(jù),可能會(huì)導(dǎo)致Spark作業(yè)的崩潰。這種情況也可能會(huì)導(dǎo)致DAGScheduler反復(fù)提交幾次stage,TaskScheduler返回提交幾次task,大大延長了我們的Spark作業(yè)的運(yùn)行時(shí)間。

此時(shí),可以考慮調(diào)節(jié)連接的超時(shí)時(shí)長,連接等待時(shí)長需要在spark-submit腳本中進(jìn)行設(shè)置,設(shè)置方式如代碼清單所示:
--conf spark.core.connection.ack.wait.timeout=300

調(diào)節(jié)連接等待時(shí)長后,通常可以避免部分的XX文件拉取失敗、XX文件lost等報(bào)錯(cuò)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,530評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評(píng)論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,759評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,204評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,415評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,955評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,675評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,967評(píng)論 2 374