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