Spark性能調優

1.分配更多的資源 -- 性能調優的王道

真實項目里的腳本:

? ? bin/spark-submit \

? ? --class com.xx.xx \

? ? --num-executors 80 \

? ? --driver-memory 6g \

? ? --executor-cores 3 \

? ? --master yarn-cluster \

? ? --queue root.default \

? ? --conf spark.yarn.executor.memoryOverhead=2048 \

? ? --conf spark.core.connection.ack.waite.timeout=300 \

? ? /usr/xx/xx.jar \

? ? args

分配資源之前,要先看機器能夠給我們提供多少資源,并且最大化的利用這些資源才是最好的;

1.standalone模式:

? ? 根據實際情況分配spark作業資源,比如每臺機器4G,2個cpu,20臺機器

2.spark-on-yarn模式:

? ? 要看spark作業要提交到的資源隊列,大概有多少資源?

SparkContext、DAGScheduler、taskScheduler,會將我們的算子切割成大量的task提交到Application的executor上去執行,比如分配給其中一個的executor100個task,他的cpu只有2個,那么并行只能執行2個task,如果cpu為5個,那么久先執行5個再執行5個

a.增加executor的數量:

? ? 如果executor的數量比較少,那么意味著可以并行執行的task的數量就比較少,

? ? 就意味著Application的并行執行能力比較弱;

? ? ? ? 比如:

? ? ? ? ? ? 有3個executor,每個executor有2個cup core,

? ? ? ? ? ? 那么能夠并行執行的task就是6個,6個執行完換下一批6個

? ? 增加executor的個數后,并行執行的task就會變多,性能得到提升

b.增加每個executor的cpu core

? ? ? ? 比如:

? ? ? ? ? ? 之前:3個exexutor,每個executor的cpu core為2個,那么并執行的task是6個

? ? ? ? ? ? ? ? 把cpu core增加到5個,那么并行執行的task就是15個,提高了性能

c.增加每個executor的內存量:

? ? 1.如果需要對RDD進行cache,那么更多的內存就能緩存更多的數據,

? ? ? 將更少的數據寫入磁盤,甚至不寫入磁盤,減少了磁盤IO。

? ? 2.對于shuffle操作,reduce端會需要內存來存儲拉取過來的數據進行聚合,如果內存不夠,

? ? ? 也會寫入磁盤,增加executor內存,就會有更少的數據寫入磁盤,較少磁盤IO,提高性能。

? ? 3.對于task的執行,需要創建很多對象,如果內存比較小,可能導致JVM堆內存滿了,

? ? ? 然后頻繁的GC,垃圾回收,minorGC和fullGC,速度會很慢,如果加大內存,

? ? ? 帶來更少的GC,速度提升。

2.調節并行度

并行度:spark作業中,各個stage的task個數,也就代表了saprk作業中各個階段[stage]的并行度。

[spark作業,Application,jobs,action會觸發一個job,每個job會拆成多個stage,發生shuffle的時候,會拆出一個stage]

如果不調節并行度,導致并行度過低,會怎么樣???

比如:

? ? 1.我們通過上面的分配好資源后,有50個executor,每個executor10G內存,每個executor有3個cpu core

? ? ? 基本已經達到了集群或者yarn的資源上限

? ? 2. 50個executor * 3個cpu = 150個task,即可以并行執行150個task;

? ? ? 而我們設置的時候只設置了100個并行度的task,這時候每個executor分配到2個task,同時運行task的數量只有100個,導致每個executor剩下的1個cpu core在那空轉,浪費資源。

? ? 資源雖然夠了,但是并行度沒有和資源相匹配,導致分配下去的資源浪費掉了!!!

? ? **合理的并行度設置,應該要設置的足夠大,大到完全合理的利用集群資源!而且減少了每個task要處理的數據量[比如150g的數據分別分發給100個task處理就是每個task處理1.5G,但是如果是150個task的話,每個task就處理1G]**

總結:

? ? a. task數量,至少設置成與Spark application的總cpu數相同(理想狀態下,比如150個cpu core,分配150個task,差不多同時運行完畢)

? ? b.官方推薦做法:task的數量設置成 Spark application的cpu core的個數的3~5倍!!

? ? ? 比如總共150個cpu core,設置成300~500個task

? ? ? 為什么這么設置呢???

? ? ? ? 實際情況下和理想狀態下是不一樣的,因為有些task運行的快,有些運行的慢,

? ? ? ? 比如有些運行需要50s,有些需要幾分鐘運行完畢,如果剛好設置task數量和cpu core的數量相同,可能會導致資源的浪費;

? ? ? ? 比如150個task,10個運行完了,還有140個在運行,那么勢必會導致10個cpu core的閑置,

? ? ? ? 所以如果設置task的數量為cpu數量的2~3倍,一旦有task運行完,另一個task就會立刻補上來,

? ? ? ? 盡量讓cpu core不要空閑,提升了spark作業運行效率,提升性能。

? ? c.如何設置一個 Spark application的并行度???

? ? ? ? SparkConf sc = new SparkConf()

? ? ? ? ? ? ? ? ? ? ? .set("spark.default.parallelism","500");

3.重構RDD架構及RDD持久化:

默認情況下 RDD出現的問題:? ? ? ? ? ?

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? RDD4

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? /

? ? ? ? ? hdfs --> RDD1 --> RDD2 -->RDD3

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? RDD5

? ? 以上情況: 執行RDD4和RDD5的時候都會從RDD1到RDD2然后到RDD3,執行期間的算子操作,

? ? 而不會說到RDD3的算子操作后的結果給緩存起來,所以這樣很麻煩,

? ? 出現了RDD重復計算的情況,導致性能急劇下降!

結論:

? ? 對于要多次計算和使用的公共RDD,一定要進行持久化!

? ? 持久化也就是:BlockManager將RDD的數據緩存到內存或者磁盤上,后續無論對這個RDD進行多少次計算,都只需要到緩存中取就ok了。

? ? 持久化策略:

? ? ? ? rdd.persist(StorageLevel.xx()) 或者 cache

? ? ? ? 1.優先會把數據緩存到內存中 -- StorageLevel.MEMORY_ONLY

? ? ? ? 2.如果純內存空間無法支撐公共RDD的數據時,就會優先考慮使用序列化的機制在純內存中存儲,

? ? ? ? 將RDD中的每個partition中的數據序列化成一個大的字節數組,也就是一個對象,

? ? ? ? 序列化后,大大減少了內存空間的占用。-- StorageLevel.MEMORY_ONLY_SER

? ? ? ? ? ? 序列化唯一的缺點:在獲取數據的時候需要反序列化

? ? ? ? 3.如果序列化純內存的方式還是導致OOM,內存溢出的話,那就要考慮磁盤的方式。

? ? ? ? ? 內存+磁盤的普通方式(無序列化) -- StorageLevel.MEMORY_AND_DISK

? ? ? ? 4.如果上面的還是無法存下的話,就用 內存+磁盤+序列化 -- StorageLevel.MEMORY_AND_DISK_SER

另:在機器內存**極度充足**的情況下,可以使用雙副本機制,來持久化,保證數據的高可靠性

? ? 如果機器宕機了,那么還有一份副本數據,就不用再次進行算子計算了。[錦上添花--一般不要這么做] -- StorageLevel.MEMORY_ONLY_SER_2

4.廣播大變量 [sc.broadcast(rdd.collect)]

問題情景:

? ? 當我們在寫程序用到外部的維度表的數據進行使用的時候,程序默認會給每個task都發送相同的這個數據,

? ? 如果這個數據為100M,如果我們有1000個task,100G的數據,通過網絡傳輸到task,

? ? 集群瞬間因為這個原因消耗掉100G的內存,對spark作業運行速度造成極大的影響,性能想想都很可怕!!!

解決方案:

? ? sc.broadcast(rdd.collect)

? ? 分析原理:

? ? ? ? [BlockManager:負責管理某個executor上的內存和磁盤上的數據]

? ? ? ? 廣播變量會在Driver上有一份副本,當一個task使用到廣播變量的數據時,會在自己本地的executor的BlockManager去取數據,

? ? ? ? 發現沒有,BlockManager就會到Driver上遠程去取數據,并保存在本地,

? ? ? ? 然后第二個task需要的時候來找BlockManager直接就可以找到該數據,

? ? ? ? executor的Blockmanager除了可以到Driver遠程的取數據,

? ? ? ? 還可能到鄰近節點的BlockManager上去拉取數據,越近越好!

? ? 舉例說明:

? ? ? ? 50個executor,1000個task,外部數據10M,

? ? ? ? 默認情況下,1000個task,1000個副本,10G數據,網絡傳輸,集群中10G的內存消耗

? ? ? ? 如果使用廣播,50個executor,500M的數據,網絡傳輸速率大大增加,

? ? ? ? 10G=10000M 和 500M的對比 20倍啊。。。

**之前的一個測試[真實]:

? ? ? ? 沒有經過任何調優的spark作業,運行下來16個小時,

? ? ? ? 合理分配資源+合理分配并行度+RDD持久化,作業下來5個小時,

? ? ? ? 非常重要的一個調優Shuffle優化之后,2~3個小時,

? ? ? ? 應用了其他的性能調優之后,JVM調參+廣播等等,30分鐘

? ? ? ? 16個小時 和 30分鐘對比,太可怕了!!!性能調優真的真的很重要!!!

5.Kryo序列化機制:

默認情況下,Spark內部使用java的序列化機制

ObjectOutPutStream/ObjectInputStream,對象輸入輸出流機制來進行序列化

這種序列化機制的好處:

? ? 處理方便,只是需要在算子里使用的變量是實現Serializable接口即可

缺點在于:

? ? 默認序列化機制效率不高,序列化的速度比較慢,序列化后的數據占用內存空間還比較大

解決:手動進行序列化格式的優化:Kryo [spark支持的]

Kryo序列化機制比默認的java序列化機制速度快,

序列化后的數據更小,是java序列化后數據的 1/10 。

所以Kryo序列化后,會讓網絡傳輸的數據更少,在集群中耗費的資源大大減少。

Kryo序列化機制:[一旦啟用,會生效的幾個地方]

? ? a.算子函數中使用到的外部變量[比如廣播的外部維度表數據]

? ? ? ? 優化網絡傳輸性能,較少集群的內存占用和消耗

? ? b.持久化RDD時進行序列化,StorageLevel.MEMORY_ONLY_SER

? ? ? ? 將每個RDD partition序列化成一個大的字節數組時,就會使用Kryo進一步優化序列化的效率和性能。

? ? ? ? 持久化RDD占用的內存越少,task執行的時候,創建的對象,不至于頻繁的占滿內存,頻繁的GC

? ? c.shuffle

? ? ? ? 在stage間的task的shuffle操作時,節點與節點之間的task會通過網絡拉取和傳輸數據,

? ? ? ? 此時這些數據也是可能需要序列化的,就會使用Kryo

實現Kryo:

step1. 在SparkConf里設置 new SparkConf()

? ? ? ? ? ? ? ? ? ? ? .set("spark.serializer","org.apache.spark.serializer.KyroSerializer")

? ? ? ? ? ? ? ? ? ? ? .registerKryoClasses(new Class[]{MyCategory.class})

? ? [Kryo之所以沒有沒有作為默認的序列化類庫,就是因為Kryo要求,如果要達到它的最佳效果的話]

? ? [一定要注冊我們自定義的類,不如:算子函數中使用到了外部自定義的對象變量,這時要求必須注冊這個類,否則Kyro就達不到最佳性能]

step2. 注冊使用到的,需要Kryo序列化的一些自定義類

6.使用FastUtil優化數據格式:

FastUtil是什么??

fastutil是擴展了Java標準集合框架(Map、List、Set;HashMap、ArrayList、HashSet)的類庫,提供了特殊類型的map、set、list和queue;

fastutil能夠提供更小的內存占用,更快的存取速度;我們使用fastutil提供的集合類,來替代自己平時使用的JDK的原生的Map、List、Set,

fastutil的每一種集合類型,都實現了對應的Java中的標準接口(比如fastutil的map,實現了Java的Map接口),因此可以直接放入已有系統的任何代碼中。

fastutil還提供了一些JDK標準類庫中沒有的額外功能(比如雙向迭代器)。

fastutil除了對象和原始類型為元素的集合,fastutil也提供引用類型的支持,但是對引用類型是使用等于號(=)進行比較的,而不是equals()方法。

fastutil盡量提供了在任何場景下都是速度最快的集合類庫。

Spark中FastUtil運用的場景??

1.如果算子函數使用了外部變量,

? ? 第一可以使用broadcast廣播變量優化;

? ? 第二可以使用Kryo序列化類庫,提升序列化性能和效率;

? ? 第三如果外部變量是某種比較大的集合(Map、List等),可以考慮fastutil來改寫外部變量,

? ? ? ? 首先從源頭上就減少了內存的占用,通過廣播變量進一步減少內存占用,

? ? ? ? 再通過Kryo類庫進一步減少內存占用

? ? 避免了executor內存頻繁占滿,頻繁喚起GC,導致性能下降的現象

使用步驟:

step1:導入pom依賴

? ? <dependency>

? ? ? ? <groupId>fastutil</groupId>

? ? ? ? ? ? <artifactId>fastutil</artifactId>

? ? ? ? <version>5.0.9</version>

? ? </dependency>

step2:

? ? List<Integer> => IntList

? ? 基本都是類似于IntList的格式,前綴就是集合的元素類型,

? ? 特殊的就是Map,Int2IntMap,代表了Key-Value映射的元素類型

7.調節數據本地化等待時長:

問題發生的場景:

spark在Driver上,對Application的每一個stage的task分配之前,

都會計算出每個task要計算的是哪個分片數據,RDD的某個partition;

spark的分配算法:

? ? a.優先把每一個task正好分配到他要計算的數據所在的節點,這樣的話不用在網絡間傳輸數據

? ? b.但是,task沒有機會分配到數據所在的節點上,為什么呢???

? ? ? ? 因為那個節點上的計算資源和計算能力都滿了,這個時候 spark會等待一段時間,

? ? ? ? 默認情況下是3s鐘,到最后,實在等不了了,就會選擇一個較差的本地化級別,

? ? ? ? 比如說會把task分配到靠他要計算的數據的節點最近的節點,然后進行計算

? ? c.對于b來說肯定要發生網絡傳輸,task會通過其所在節點的executor的BlockManager來獲取數據,

? ? BlockManager發現自己本地沒有,就會用getRemote()的方法,通過TransferService(網絡數據傳輸組件)

? ? 從數據所在節點的BlockManager中獲取數據,通過網絡傳輸給task所在的節點

總結:

? 我們肯定是希望看到 task和數據都在同一個節點上,直接從本地的executor中的BlockManager中去獲取數據,

? 純內存或者帶點IO,如果通過網絡傳輸,那么大量的網絡傳輸和磁盤IO都是性能的殺手

本地化的級別類型:

1.PROCESS_LOCAL: 進程本地化,代碼和數據都在同一個進程中,也就是在同一個executor進程中,

? task由executor來執行,數據在executor的BlockManager中,性能最好

2.NODE_LOCAL: 節點本地化,比如說一個節點上有兩個executor,其中一個task需要第一個executor的數據,

? 但是他被分配給了第二個executor,他會找第二個executor的BlockManager去取數據,但是沒有,

? BlockManager會去第一個的executor的BlockManager去取數據,這是發生在進程中的

3.NOPREF: 數據從哪里獲取都一樣,沒有好壞之分

4.RACK_LOCAL: 數據在同一個機架上的不同節點上,需要進行網絡間的數據傳輸

5.ANY: 數據可能在集群中的任何地方,而且不在同一個機架,這種性能最差!!

開發時的流程:

觀察spark作業時的日志,先測試,先用client模式,在本地就可以看到比較全的日志。

日志里面會顯示:starting task...,PROCESS_LOCAL,或者是NODE_LOCAL,觀察大部分數據本地化的級別

如果發現大部分都是PROCESS_LOCAL的級別,那就不用調了,如果大部分都是NODE_LOCAL或者ANY,那就要調節一下等待時長了

要反復調節,反復觀察本地化級別是否提升,查看spark作業運行的時間有沒有縮短

不要本末倒置,如果是 本地化級別提升了,但是因為大量的等待時間,spark作業的運行時常變大了,這就不要調節了

spark.locality.waite

spark.locality.waite.process

spark.locality.waite.node

spark.locality.waite.rack

默認等待時長都是3s

設置方法:

? ? new SparkConf().set("spark.locality.waite","10")//不要帶s

8.JVM調優:1個executor對應1個JVM進程

A. 降低cache操作的內存占比

JVM模塊:

? ? 每一次存放對象的時候都會放入eden區域,其中有一個survivor區域,另一個survivor區域是空閑的[新生代],

? ? 當eden區域和一個survivor區域放滿了以后(spark運行產生的對象太多了),

? ? 就會觸發minor gc,小型垃圾回收,把不再使用的對象從內存中清空,給后面新創建的對象騰出空間

? ? 清理掉了不在使用的對象后,還有一部分存活的對象(還要繼續使用的對象),

? ? 將存活的對象放入空閑的那個survivor區域里,這里默認eden:survivor1: survivor2 = 8:1:1,

? ? 假如對象占了1.5放不下survivor區域了,那么就會放到[老年代]里;

? ? 假如JVM的內存不夠大的話,可能導致頻繁的新生代內存滿溢,頻繁的進行minor gc,

? ? 頻繁的minor gc會導致短時間內,有些存活的對象,多次垃圾回收都沒有回收掉,

? ? 會導致這種短生命周期的對象(其實是不一定要長期使用的對象)年齡過大,

? ? 垃圾回收次數太多,還沒有回收到,就已經跑到了老年代;

? ? 老年代中可能會因為內存不足,囤積一大堆短生命周期的對象(本來應該在年輕代中的),

? ? 可能馬上就要回收掉的對象,此時可能造成老年代內存滿溢,造成頻繁的full gc(全局/全面垃圾回收),full gc就會去老年代中回收對象;

? ? 由于full gc算法的設計,是針對老年代中的對象,數量很少,滿溢進行full gc的頻率應該很少,

? ? 因此采取了不太復雜的但是耗費性能和時間的垃圾回收算法。full gc 很慢很慢;

? ? full gc 和 minor gc,無論是快還是慢,都會導致JVM的工作線程停止工作,即 stop the world,

? ? 簡言之:gc的時候,spark停止工作,等待垃圾回收結束;

在spark中,堆內存被分為了兩塊:

? ? 一塊是專門用來給RDD cache和persist操作進行RDD數據緩存用的;

? ? 一塊是給spark算子函數的運行使用的,存放函數中自己創建的對象;

默認情況下,給RDD cache的內存占比是60%,但是在某些情況下,比如RDD cache不那么緊張,

而task算子函數中創建的對象過多,內存不太大,導致頻繁的minor gc,甚至頻繁的full gc,

導致spark頻繁的暫停工作,性能影響會非常大,

解決辦法:

? ? 集群是spark-onyarn的話就可以通過spark ui來查看,spark的作業情況,

? ? 可以看到每個stage的運行情況,包括每個task的運行時間,gc時間等等,

? ? 如果發現gc太頻繁,時間太長,此時可以適當調節這個比例;

總結:

? ? 降低cache的內存占比,大不了用persist操作,選擇將一部分的RDD數據存入磁盤,

? ? 或者序列化方式Kryo,來減少RDD緩存的內存占比;

? ? 對應的RDD算子函數的內存占比就增多了,就可以減少minor gc的頻率,同時減少full gc的頻率,提高性能

具體實現:0.6->0.5->0.4->0.2

? ? new SparkConf().set("spark.storage.memoryFraction","0.5")

B. executor堆外內存與連接時常

1. executor堆外內存[off-heap memory]:

? 場景:

? ? ? ? 比如兩個stage,第二個stage的executor的task需要第一個executor的數據,

? ? ? ? 雖然可以通過Driver的MapOutputTracker可以找到自己數據的地址[也就是第一個executor的BlockManager],

? ? ? ? 但是第一個executor已經掛掉了,關聯的BlockManager也沒了,就沒辦法獲取到數據;

? ? 有時候,如果你的spark作業處理的數據量特別大,幾億數據量;

? ? spark作業一運行,是不是報錯諸如:shuffle file cannot find,executor task lost,out of memory,

? ? 這時候可能是executor的堆外內存不夠用了,導致executor在運行的時候出現了內存溢出;

? ? 導致后續的stage的task在運行的時候,可能從一些executor中拉取shuffle map output 文件,

? ? 但是executor已經掛掉了,關聯的BlockManager也沒有了,所以可能會報shuffle output file not found,resubmitting task,executor lost,spark作業徹底失敗;

? 這個時候就可以考慮調節executor的堆外內存,堆外內存調節的比較大的話,也會提升性能;

? ? 怎么調價堆外內存的大小??

? ? ? ? 在spark-submit 的腳本中添加

? ? ? ? ? ? ? ? ? ? --conf spark.yarn.executor.memoryOverhead=2048

? ? ? ? 注意:這個設置是在spark-submit腳本中,不是在 new SparkConf()里設置的!!!

? ? ? ? 這個是在spark-onyarn的集群中設置的,企業也是這么設置的!

? ? ? ? 默認情況下,堆外內存是300多M,我們在項目中通常都會出現問題,導致spark作業反復崩潰,

? ? ? ? 我們就可以調節這個參數 ,一般來說至少1G(1024M),有時候也會2G、4G,

? ? ? ? 來避免JVM oom的異常問題,提高整體spark作業的性能

2. 連接時常的等待:

? ? ? ? 知識回顧:如果JVM處于垃圾回收過程,所有的工作線程將會停止,相當于一旦進行垃圾回收,

? ? ? ? spark/executor就會停止工作,無法提供響應

? 場景:

? ? ? ? 通常executor優先會從自己關聯的BlockManager去取數據,如果本地沒有,

? ? ? ? 會通過TransferService,去遠程連接其他節點上的executor的BlockManager去取;

? ? ? ? 如果這個遠程的executor正好創建的對象特別大,特別多,頻繁的讓JVM的內存滿溢,進行垃圾回收,

? ? ? ? 此時就沒有反應,無法建立網絡連接,會有卡住的現象。spark默認的網絡連接超時時間是60s,

? ? ? ? 如果卡住60秒都無法建立網絡連接的話,就宣布失敗;

? ? ? ? 出現的現象:偶爾會出現,一串fileId諸如:hg3y4h5g4j5h5g5h3 not found,file lost,

? ? ? ? 報錯幾次,幾次都拉取不到數據的話,可能導致spark作業的崩潰!

? ? ? ? 也可能會導致DAGScheduler多次提交stage,TaskScheduler反復提交多次task,

? ? ? ? 大大延長了spark作業的運行時間

? 解決辦法:[注意是在shell腳本上不是在SparkConf上set!!]

? ? ? ? spark-submit

? ? ? ? ? ? ? ? ? ? --conf spark.core.connection.ack.waite.timeout=300

9.shuffle調優

shuffle的概念以及場景

? ? 什么情況下會發生shuffle??

? ? ? ? 在spark中,主要是這幾個算子:groupByKey、reduceByKey、countByKey、join等

? ? 什么是shuffle?

? ? ? ? a) groupByKey:把分布在集群中各個節點上的數據中同一個key,對應的values都集中到一塊,

? ? ? ? 集中到集群中的同一個節點上,更嚴密的說就是集中到一個節點上的一個executor的task中。

? ? ? ? 集中一個key對應的values后才能交給我們處理,<key,iterable<value>>

? ? ? ? b) reduceByKey:算子函數對values集中進行reduce操作,最后變成一個value

? ? ? ? c) join? RDD<key,value>? ? RDD<key,value>,只要兩個RDD中key相同的value都會到一個節點的executor的task中,供我們處理

? ? 以reduceByKey為例:

9.1. shuffle調優之 map端合并輸出文件

默認的shuffle對性能有什么影響??

? ? 實際生產環境的條件:

? ? ? ? 100個節點,每個節點一個executor:100個executor,每個executor2個cpu core,

? ? ? ? 總格1000個task,平均到每個executor是10個task;按照第二個stage的task個數和第一個stage的相同,

? ? ? ? 那么每個節點map端輸出的文件個數就是:10 * 1000 = 10000 個

? ? ? ? 總共100個節點,總共map端輸出的文件數:10000 * 100 = 100W 個

? ? ? ? 100萬個。。。太嚇人了!!!

? ? shuffle中的寫磁盤操作,基本上是shuffle中性能消耗最嚴重的部分,

? ? 通過上面的分析可知,一個普通的生產環境的spark job的shuffle環節,會寫入磁盤100萬個文件,

? ? 磁盤IO性能和對spark作業執行速度的影響,是極其驚人的!!

? ? 基本上,spark作業的性能,都消耗在了shuffle中了,雖然不只是shuffle的map端輸出文件這一部分,但是這也是非常大的一個性能消耗點。

怎么解決?

? ? 開啟map端輸出文件合并機制:

? ? ? ? new SparkConf().set('spark.shuffle.consolidateFiles','true')

? ? 實際開發中,開啟了map端輸出文件合并機制后,有什么變化?

? ? ? ? 100個節點,100個executor,

? ? ? ? 每個節點2個cpu core,

? ? ? ? 總共1000個task,每個executor10個task,

? ? ? ? 每個節點的輸出文件個數:

? ? ? ? ? ? 2*1000 = 2000 個文件

? ? ? ? 總共輸出文件個數:

? ? ? ? ? ? 100 * 2000 = 20萬 個文件

? ? ? ? 相比開啟合并之前的100萬個,相差了5倍!!

合并map端輸出文件,對spark的性能有哪些影響呢?

? ? 1. map task寫入磁盤文件的IO,減少:100萬 -> 20萬個文件

? ? 2. 第二個stage,原本要拉取第一個stage的task數量文件,1000個task,第二個stage的每個task都會拉取1000份文件,走網絡傳輸;合并以后,100個節點,每個節點2個cpu,第二個stage的每個task只需要拉取 100 * 2 = 200 個文件,網絡傳輸的性能大大增強

? ? 實際生產中,使用了spark.shuffle.consolidateFiles后,實際的調優效果:

? ? ? ? 對于上述的生產環境的配置,性能的提升還是相當可觀的,從之前的5個小時 降到了 2~3個小時

總結:

? ? 不要小看這個map端輸出文件合并機制,實際上在數據量比較大的情況下,本身做了前面的優化,

? ? executor上去了 -> cpu core 上去了 -> 并行度(task的數量)上去了,但是shuffle沒調優,

? ? 這時候就很糟糕了,大量的map端輸出文件的產生,會對性能有比較惡劣的影響

9.2. map端內存緩沖與reduce端內存占比

spark.shuffle.file.buffer,默認32k

spark.shuffle.memoryFraction,占比默認0.2

調優的分量:

? ? map端內存緩沖和reduce端內存占比,網上對他倆說的是shuffle調優的不二之選,其實這是不對的,

? ? 因為以實際的生產經驗來說,這兩個參數沒那么重要,但是還是有一點效果的,

? ? 就像是很多小的細節綜合起來效果就很明顯了,

原理:

map:

? ? 默認情況下,shuffle的map task輸出到磁盤文件的時候,統一都會先寫入每個task自己關聯的一個內存緩沖區中,

? ? 這個緩沖區默認大小是32k,每一次,當內存緩沖區滿溢后,才會進行spill操作,溢寫到磁盤文件中

reduce:

? ? reduce端task,在拉取數據之后,會用hashmap的數據格式來對每個key對應的values進行匯聚,

? ? 針對每個key對應的value,執行我們自定義的聚合函數的代碼,比如_+_,(把所有values相加)

? ? reduce task,在進行匯聚、聚合等操作的時候,實際上,使用的就是自己對應的executor的內存,

? ? executor(jvm進程,堆),默認executor內存中劃分給reduce task進行聚合的比例是20%。

? ? 問題來了,內存占比是20%,所以很有可能會出現,拉取過來的數據很多,那么在內存中,

? ? 放不下,這個時候就會發生spill(溢寫)到磁盤文件中取.

如果不調優會出現什么問題??

默認map端內存緩沖是32k,

默認reduce端聚合內存占比是20%

如果map端處理的數據比較大,而內存緩沖是固定的,會出現什么問題呢?

? ? 每個task處理320k,32k的內存緩沖,總共向磁盤溢寫10次,

? ? 每個task處理32000k,32k的內存緩沖,總共向磁盤溢寫1000次,

? ? 這樣就造成了多次的map端往磁盤文件的spill溢寫操作,發生大量的磁盤IO,降低性能

map數據量比較大,reduce端拉取過來的數據很多,就會頻繁的發生reduce端聚合內存不夠用,

頻繁發生spill操作,溢寫到磁盤上去,這樣一來,磁盤上溢寫的數據量越大,

后面進行聚合操作的時候,很可能會多次讀取磁盤中的數據進行聚合

默認情況下,在數據量比較大的時候,可能頻繁的發生reduce端磁盤文件的讀寫;

這兩點是很像的,而且有關聯的,數據量變大,map端肯定出現問題,reduce也出現問題,

出的問題都是一樣的,都是磁盤IO頻繁,變多,影響性能

調優解決:

? ? 我們要看spark UI,

? ? ? ? 1. 如果公司用的是standalone模式,那么很簡單,把spark跑起來,會顯示sparkUI的地址,

? ? ? ? 4040端口號,進去看,依次點擊可以看到,每個stage的詳情,有哪些executor,有哪些task,

? ? ? ? 每個task的shuffle write 和 shuffle read的量,shuffle的磁盤和內存,讀寫的數據量

? ? ? ? 2. 如果是yarn模式提交,從yarn的界面進去,點擊對應的application,進入spark ui,查看詳情

? ? 如果發現磁盤的read和write很大,就意味著要調節一下shuffle的參數,進行調優,

? ? 首先當然要考慮map端輸出文件合并機制

? ? 調節上面兩個的參數,原則是:

? ? ? ? spark.shuffle.buffer,每次擴大一倍,然后看看效果,64k,128k

? ? ? ? spark.shuffle.memoryFraction,每次提高0.1,看看效果

? ? 不能調節的過大,因為你這邊調節的很大,相對應的其他的就會變得很小,其他環節就會出問題

? ? 調節后的效果:

? ? ? ? map task內存緩沖變大了,減少了spill到磁盤文件的次數;

? ? ? ? reduce端聚合內存變大了,減少了spill到磁盤的次數,而且減少了后面聚合時讀取磁盤的數量

? ? ? ? new SparkConf()

? ? ? ? .set("spark.shuffle.file.buffer","64")

? ? ? ? .set("spark.shuffle.file.memoryFraction","0.3")

10.算子調優

1.算子調優之MapPartitons提升map的操作性能

在spark中最近本的原則:每個task處理RDD中的每一個partition

優缺點對比:

? ? 普通Map:

? ? ? ? 優點:比如處理了一千條數據,內存不夠了,那么就可以將已經處理的一千條數據從內存里面垃圾回收掉,

? ? ? ? 或者用其他辦法騰出空間;通常普通的map操作不會導致內存OOM異常;

? ? ? ? 缺點:比如一個partition中有10000條數據,那么function會執行和計算一萬次

? ? MapPartitions:

? ? ? ? 優點:一個task僅僅會執行一次function,一次function接收partition中的所有數據

? ? ? ? 只要執行一次就可以了,性能比較高

? ? ? ? 缺點:對于大數據量來說,比如一個partition100萬條數據,一次傳入一個function后,

? ? ? ? 可能一下子內存就不夠了,但是又沒辦法騰出空間來,可能就OOM,內存溢出

那么什么時候使用MapPartitions呢?

? ? 當數據量不太大的時候,都可以使用MapPartitions來操作,性能還是很不錯的,

? ? 不過也有經驗表明用了MapPartitions后,內存直接溢出,

? ? 所以在項目中自己先估算一下RDD的數據量,以每個partition的量,還有分配給executor的內存大小,

? ? 可以試一下,如果直接OOM了,那就放棄吧,如果能夠跑通,那就可以使用。

2.算子調優之filter之后 filter 之后 用 coalesce來減少partition的數量

默認情況下,RDD經過filter之后,RDD中每個partition的數據量會不太一樣,(原本partition里的數據量可能是差不多的)

問題:

? ? 1.每一個partition的數據量變少了,但是在后面進行處理的時候,

? ? 還是要和partition的數量一樣的task數量去處理,有點浪費task計算資源

? ? 2.每個partition的數據量不一樣,后面會導致每個處理partition的task要處理的數據量不一樣,

? ? 這時候很容易出現**數據傾斜**

? ? 比如說,有一個partition的數據量是100,而另一個partition的數據量是900,

? ? 在task處理邏輯一樣的情況下,不同task要處理的數據量可能差別就到了9倍,甚至10倍以上,

? ? 同樣導致速度差別在9倍或者10倍以上

? ? 這樣就是導致了有的task運行的速度很快,有的運行的很慢,這就是數據傾斜。

解決:

? ? 針對以上問題,我們希望把partition壓縮,因為數據量變小了,partition完全可以對應的變少,

? ? 比如原來4個partition,現在可以變成2個partition,那么就只要用后面的2個task來處理,

? ? 不會造成task資源的浪費(不必要針對只有一點點數據的partition來啟動一個task進行計算)

? ? 避免了數據傾斜的問題

3.算子調優之使用foreachPartition優化寫入數據庫性能

默認的foreach有哪些缺點?

? ? 首先和map一樣,對于每條數據都要去調一次function,task為每個數據,都要去執行一次task;

? ? 如果一個partition有100萬條數據,就要調用100萬次,性能極差!

? ? 如果每條數據都要創建一個數據庫連接,那么就要創建100萬個數據庫連接,

? ? 但是數據庫連接的創建和銷毀都是非常耗性能的,雖然我們用了數據庫連接池,只要創建固定數量的連接,

? ? 還是得多次通過數據庫連接,往數據庫里(mysql)發送一條sql語句,mysql需要去執行這條sql語句,

? ? 有100萬條數據,那么就是要發送100萬次sql語句;

用了foreachPartition以后,有哪些好處?

? ? 1.對于我們寫的函數就調用一次就行了,一次傳入一個partition的所有數據

? ? 2.主要創建或者獲取一個數據庫連接就可以了

? ? 3.只要向數據里發送一條sql語句和一組參數就可以了

在實際開發中,我們都是清一色使用foreachPartition算子操作,

但是有個問題,跟mapPartitions操作一樣,如果partition的數據量非常大,

比如真的是100萬條,那幾本就不行了!一下子進來可能會發生OOM,內存溢出的問題

一組數據的對比:

? ? 生產環境中:

? ? ? ? 一個partition中有1000條數據,用foreach,跟用foreachPartition,

? ? ? ? 性能提高了2~3分;

數據庫里是:

? ? for循環里preparestatement.addBatch

? ? 外面是preparestatement.executeBatch

4.算子調優之repartition解決SparkSQL低并行度的問題

并行度: 我們是可以自己設置的

? ? 1.spark.default.parallelism

? ? 2.sc.textFile(),第二個參數傳入指定的數量(這個方法用的非常少)

在生產環境中,我們是要自己手動設置一下并行度的,官網推薦就是在spark-submit腳本中,

指定你的application總共要啟動多少個executor,100個,每個executor多少個cpu core,

2~3個,假設application的總cpu core有200個;

官方推薦設置并行度要是總共cpu core個數的2~3倍,一般最大值,所以是 600;

設置的這個并行度,在哪些情況下生效?哪些情況下不生效?

? ? 1.如果沒有使用SparkSQL(DataFrame)的話,那么整個spark應用的并行度就是我們設置的那個并行度

? ? 2.如果第一個stage使用了SparkSQL從Hive表中查詢了一些數據,然后做了一些transformatin的操作,

? ? 接著做了一個shuffle操作(groupByKey);下一個stage,在shuffle之后,做了一些transformation的操作

? ? 如果Hive表對應了20個block,而我們自己設置的并行度是100,

? ? 那么第一個stage的并行度是不受我們控制的,就只有20個task,第二個stage的才是我們設置的并行度100個

問題出在哪里了?

? ? SparkSQL 默認情況下,我們是沒辦法手動設置并行度的,所以可能造成問題,也可能不造成問題,

? ? SparkSQL后面的transformation算子操作,可能是很復雜的業務邏輯,甚至是很復雜的算法,

? ? 如果SparkSQL默認的并行度設置的很少,20個,然后每個task要處理為數不少的數據量,

? ? 還要執行很復雜的算法,這就導致第一個stage特別慢,第二個stage 1000個task,特別快!

解決辦法:

? ? repartition:

? ? ? ? 使用SparkSQL這一步的并行度和task的數量肯定是沒辦法改變了,但是可以將SparkSQL查出來的RDD,

? ? ? ? 使用repartition算子進行重新分區,比如分多個partition,20 -> 100個;

? ? ? ? 然后從repartition以后的RDD,并行度和task數量,就會按照我們預期的來了,

? ? ? ? 就可以避免在跟SparkSQL綁定在一起的stage中的算子,只能使用少量的task去處理大量數據以及復雜的算法邏輯

5.算子操作reduceByKey:

reduceByKey相較于普通的shuffle操作(不如groupByKey),他的一個特點就是會進行map端的本地聚合;

對map端給下個stage每個task創建的輸出文件中,寫數據之前,就會進行本地的combiner操作,也就是多每個key的value,都會執行算子函數(_+_),減少了磁盤IO,較少了磁盤空間的占用,在reduce端的緩存也變少了

11.troubleshooting之控制reduce端緩沖大小以避免內存溢出(OOM)

new SparkConf().set("spark.reducer.maxSizeInFlight","24") //默認是48M

Map端的task是不斷地輸出數據的,數據量可能是很大的,

? ? 但是其reduce端的task,并不是等到Map端task將屬于自己的那個分數據全部寫入磁盤后,再去拉取的

? ? Map端寫一點數據,reduce端task就會去拉取一小部分數據,立刻進行后面的聚合,算子函數的應用;

? ? 每次reduce能夠拉取多少數據,是由reduce端buffer來定,因為拉取過來的數據都是放入buffer中的,

? ? 然后采用后面的executor分配的堆內存占比(0.2),去進行后續的聚合,函數操作

reduce端buffer 可能會出現什么問題?

? ? reduce端buffer默認是48M,也許大多時候,還沒有拉取滿48M,也許是10M,就計算掉了,

? ? 但是有時候,Map端的數據量特別大,寫出的速度特別快,reduce端拉取的時候,全部到達了自己緩沖的最大極限48M,全部填滿,

? ? 這個時候,再加上reduce端執行的聚合函數代碼,可能會創建大量的對象,也許一下子內存就撐不住了,就會造成OOM,reduce端的內存就會造成內存泄漏

如何解決?

? ? 這個時候,我們應該減少reduce端task緩沖的大小,我們寧愿多拉取幾次,但是每次同時能拉取到reduce端每個task的數據量比較少,就不容易發生OOM,比如調成12M;

? ? 在實際生產中,這種問題是很常見的,這是典型的以性能換執行的原理,

? ? reduce的緩沖小了,不容易造成OOM了,但是性能一定是有所下降的,你要拉取的次數多了,

? ? 就會走更多的網絡IO流,這時候只能走犧牲性能的方式了;

曾經一個經驗:

? ? 曾經寫了一個特別復雜的spark作業,寫完代碼后,半個月就是跑步起來,里面各種各樣的問題,

? ? 需要進行troubleshooting,調節了十幾個參數,其中里面就有reduce端緩沖的大小,最后,

? ? 總算跑起來了!

12. troubleshooting之解決JVM GC導致的shuffle拉取文件失敗:

過程:

? ? 第一個stage的task輸出文件的同時 ,會像Driver上記錄這些數據信息,然后下一個stage的task想要得到上個stage的數據,

? ? 就得像Driver所要元數據信息,然后去像上一個的stage的task生成的文件中拉取數據。

問題場景:

? ? 在spark作業中,有時候經常出現一種情況,就是log日志報出:shuffle file not found..,

? ? 有時候他會偶爾出現一次,有的時候出現一次后重新提交stage、task,重新執行一遍 就好了。

分析問題:

? ? executor在JVM進程中,可能內存不太夠用,那么此時就很可能執行GC,minor gc 或者 full gc,

? ? 總之一旦發生gc后,就會導致所有工作線程全部停止,比如BlockManager,基于netty的網絡通信。

? ? 第二個stage的task去拉取數據的時候,上一個executor正好在進行gc,就導致拉取了半天也沒拉取到數據,

? ? 那為什么第二次提交stage的時候,就又可以了呢?

? ? ? ? 因為第二次提交的時候,上一個executor已經完成了gc。

解決:

? ? spark.shuffle.io.maxRetries 3[默認3次]

? ? ? ? shuffle 文件拉取時,如果沒有拉取到,最多或者重試幾次,默認3次

? ? spark.shuffle.io.retryWait 5s [默認5s]

? ? ? ? 每一次重新拉取文件的時間間隔,默認5s

? ? 默認情況下,第一個stage的executor正在漫長的full gc,第二個stage的executor嘗試去拉取數據,

? ? 結果沒拉取到,這樣會反復重試拉取3次,中間間隔時間5s,也就是總共15s,拉取不成功,就報 shuffle file not found

? ? ? ? 我們可以增大上面兩個參數的值:

? ? ? ? ? ? spark.shuffle.io.maxRetries 60次

? ? ? ? ? ? spark.shuffle.io.retryWait 60s

? ? ? ? ? ? 最多可以忍受一個小時沒有拉取到shuffle file,這只是一個設置最大的可能值,

? ? ? ? ? ? full gc 也不可能一個小時都沒結束把,

? ? ? ? ? ? 這樣就解決了因為gc 而無法拉取到數據的問題

13. troubleshooting之解決yarn-cluster模式的JVM棧內存溢出問題

yarn-cluster運行流程:

? ? 1.本地機器執行spark-submit腳本[yarn-cluster模式],提交spark application給resourceManager

? ? 2. resourceManager找到一個節點[nodeManager]啟動applicationMaster[Driver進程]

? ? 3. applicationMaster找resourceManager申請executor

? ? 4. resourceManager分配container(內存+cpu)

? ? 5. applicationMaster找到對應nodeManager申請啟動executor

? ? 6. nodeManager啟動executor

? ? 7. executor找applicationMaster進行反向注冊

? ? 到這里為止,applicationMaster(Driver)就知道自己有哪些資源可以用(executor),

? ? 然后就會去執行job,拆分stage,提交stage的task,進行task調度,

? ? 分配到各個executor上面去執行。

yarn-client 和 yarn-cluster的區別:

? ? yarn-client模式Driver運行在本地機器上;yarn-cluster模式Driver是運行在yarn集群上的某個nodeManager節點上的;

? ? yarn-client模式會導致本地機器負責spark作業的調用,所以網卡流量會激增,yarn-cluster沒有這個問題;

? ? yarnclient的Driver運行在本地,通常來說本地機器和yarn集群都不會在一個機房,所以性能不是特別好;

? ? yarn-cluster模式下,Driver是跟yarn集群運行在一個機房內,性能上也會好很好;

實踐經驗碰到的yarn-cluster的問題:

? ? 有時候運行了包含spark sql的spark作業,可能會遇到 在yarn-client上運行好好地,在yarn-cluster模式下,

? ? 可能無法提交運行,會報出JVM的PermGen(永久代)的內存溢出-OOM;

? ? Yarn-client模式下,Driver是運行在本地機器的,spark使用的JVM的PerGen的配置,是本地的spark-class文件,

? ? (spark客戶端是默認有配置的),JVM的永久代大小默認是128M,這個是沒問題的;

? ? 但是在Yarn-cluster模式下,Driver是運行在yarn集群的某個節點上的,使用的是沒有經過配置的默認設置82M(PerGen永久代大小)

? ? spark sql內部會進行很負責的sl語義解析、語法樹的轉換,特別復雜,在這種情況下,如果sql特別復雜,

? ? 很可能會導致性能的消耗,內存的消耗,可能對PermGen永久代的內存占比就很大

? ? 所以此時,如果對PermGen的內存占比需求多與82M,但是又小于128M,就會出現類似上面的情況,

? ? yarn-client可以正常運行因為他的默認permgen大小是128M,但是yarn-cluster的默認是82M,就會出現PermGen OOM -- PermGen out of memory

解決:

? ? spark-submit腳本中加入參數:

? ? ? ? --conf spark.driver.extraJavaOptions='-XX:PermSize=128M -XX:MxPermSize=256M'

? ? ? ? 這樣就設置了永久代的大小默認128M,最大256M,那么這樣的話,就可以保證spark作業不會出現上面的PermG

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1、 性能調優 1.1、 分配更多資源 1.1.1、分配哪些資源? Executor的數量 每個Executor所...
    Frank_8942閱讀 4,618評論 2 36
  • 前言 繼基礎篇講解了每個Spark開發人員都必須熟知的開發調優與資源調優之后,本文作為《Spark性能優化指南》的...
    Alukar閱讀 907評論 0 2
  • 1.1、 分配更多資源 1.1.1、分配哪些資源? Executor的數量 每個Executor所能分配的CPU數...
    miss幸運閱讀 3,215評論 3 15
  • 你愛我,所以我很珍惜你,我也許再也找不到第二個更愛我的人。 我愛你,所以我很珍惜,因為我愛你,我愿意為你付出不求回...
    滄浪之水_528f閱讀 915評論 0 1
  • 從吉隆口岸出境,10個小時的山路顛蕀,到加德滿都。伴著和印度歌舞音樂雷同的音樂節奏,車在路上跳,人在車里跳,心在肚...
    蓉醬說閱讀 834評論 3 10