Spark學習——效能調優(一)

Hiway發表於2019-04-01

其他更多java基礎文章:
java基礎學習(目錄)


Spark的效能調優主要有以下幾個方向:

  • 常規效能調優:分配資源、並行度、RDD架構與快取等
  • JVM調優(Java虛擬機器):JVM相關的引數,通常情況下,如果你的硬體配置、基礎的JVM的配置,都ok的話,JVM通常不會造成太嚴重的效能問題;反而更多的是,在troubleshooting中,JVM佔了很重要的地位;JVM造成線上的spark作業的執行報錯,甚至失敗(比如OOM)。
  • shuffle調優(相當重要):spark在執行groupByKey、reduceByKey等操作時的,shuffle環節的調優。這個很重要。shuffle調優,其實對spark作業的效能的影響,是相當之高!!!經驗:在spark作業的執行過程中,只要一牽扯到有shuffle的操作,基本上shuffle操作的效能消耗,要佔到整個spark作業的50%~90%。10%用來執行map等操作,90%耗費在兩個shuffle操作。groupByKey、countByKey。
  • spark操作調優(spark運算元調優,比較重要):groupByKey,countByKey或aggregateByKey來重構實現。有些運算元的效能,是比其他一些運算元的效能要高的。foreachPartition替代foreach。如果一旦遇到合適的情況,效果還是不錯的。

按照優化效果簡單排序:

  1. 分配資源、並行度、RDD架構與快取
  2. shuffle調優
  3. spark運算元調優
  4. JVM調優、廣播大變數、Kryo、fastUtil

本系列主要講解:

  • 效能調優
    • 分配更多資源
    • 調節並行度
    • 重構RDD架構及持久化RDD
    • 廣播大變數
    • Kryo序列化
    • fastUtil優化
    • 調節資料本地化等待時長
  • JVM調優
    • 降低cache操作的記憶體佔比
    • 調節executor堆外記憶體
    • 調節連線等待時長
  • Shuffle調優
    • 合併map端輸出檔案
    • 調節map端記憶體快取和reduce端記憶體佔比
    • SortShuffleManager調優
  • 運算元調優
    • 使用MapPartition提升效能
    • filter過後使用coalesce減少分割槽數量
    • 使用foreachPartition優化
    • 使用repatition解決Spark SQL低並行度
    • 使用reduceByKey本地聚合

分配更多資源

效能調優的王道,就是增加和分配更多的資源,效能和速度上的提升,是顯而易見的;基本上,在一定範圍之內,增加資源與效能的提升,是成正比的;寫完了一個複雜的spark作業之後,進行效能調優的時候,首先第一步,我覺得,就是要來調節最優的資源配置;在這個基礎之上,如果說你的spark作業,能夠分配的資源達到了你的能力範圍的頂端之後,無法再分配更多的資源了,公司資源有限;那麼才是考慮去做後面的這些效能調優的點。

分配哪些資源?

  • executor數量
  • 每個executor的cpu core數量
  • 每個executor的記憶體大小
  • driver記憶體大小

在哪裡分配這些資源?

在我們在生產環境中,提交spark作業時,用的spark-submit shell指令碼,裡面調整對應的引數

/usr/local/spark/bin/spark-submit \
--class cn.spark.sparktest.core.WordCountCluster \
--num-executors 3 \  配置executor的數量
--driver-memory 100m \  配置driver的記憶體(影響不大)
--executor-memory 100m \  配置每個executor的記憶體大小
--executor-cores 3 \  配置每個executor的cpu core數量
/usr/local/SparkTest-0.0.1-SNAPSHOT-jar-with-dependencies.jar \
複製程式碼

為什麼多分配了這些資源以後,效能會得到提升?

Spark學習——效能調優(一)
如上圖所示,Driver中的SparkContext,DAGScheduler,TaskScheduler,會將我們的運算元,切割成大量的task,提交到Application的executor上面去執行。假設我們最後切割出了100個task任務。

增加executor

如果executor數量比較少,那麼,能夠並行執行的task數量就比較少,就意味著,我們的Application的並行執行的能力就很弱。
比如有3個executor,每個executor有2個cpu core,那麼同時能夠並行執行的task,就是6個。6個執行完以後,再換下一批6個task。 增加了executor數量以後,那麼,就意味著,能夠並行執行的task數量,也就變多了。比如原先是6個,現在可能可以並行執行10個,甚至20個,100個。那麼並行能力就比之前提升了數倍,數十倍。 相應的,效能(執行的速度),也能提升數倍~數十倍。

增加每個executor的cpu core

增加每個executor的cpu core,也是增加了執行的並行能力。原本20個executor,每個才2個cpu core。能夠並行執行的task數量,就是40個task。
現在每個executor的cpu core,增加到了5個。能夠並行執行的task數量,就是100個task。 執行的速度,提升了2.5倍。

增加每個executor的記憶體量。

增加了記憶體量以後,對效能的提升,有三點:

  1. 如果需要對RDD進行cache,那麼更多的記憶體,就可以快取更多的資料,將更少的資料寫入磁碟,甚至不寫入磁碟。減少了磁碟IO。
  2. 對於shuffle操作,reduce端,會需要記憶體來存放拉取的資料並進行聚合。如果記憶體不夠,也會寫入磁碟。如果給executor分配更多記憶體以後,就有更少的資料,需要寫入磁碟,甚至不需要寫入磁碟。減少了磁碟IO,提升了效能。
  3. 對於task的執行,可能會建立很多物件。如果記憶體比較小,可能會頻繁導致JVM堆記憶體滿了,然後頻繁GC,垃圾回收,minor GC和full GC。(速度很慢)。記憶體加大以後,帶來更少的GC,垃圾回收,避免了速度變慢,速度變快了。

調節並行度

並行度:其實就是指的是,Spark作業中,各個stage的task數量,也就代表了Spark作業的在各個階段(stage)的並行度。

如果並行度過低,會怎麼樣?

假設,現在已經在spark-submit指令碼里面,給我們的spark作業分配了足夠多的資源,比如50個executor,每個executor有10G記憶體,每個executor有3個cpu core。基本已經達到了叢集或者yarn佇列的資源上限。

task沒有設定,或者設定的很少,比如就設定了,100個task。50個executor,每個executor有3個cpu core,也就是說,你的Application任何一個stage執行的時候,都有總數在150個cpu core,可以並行執行。但是你現在,只有100個task,平均分配一下,每個executor分配到2個task,ok,那麼同時在執行的task,只有100個,每個executor只會並行執行2個task。每個executor剩下的一個cpu core,就浪費掉了。

你的資源雖然分配足夠了,但是問題是,並行度沒有與資源相匹配,導致你分配下去的資源都浪費掉了。

合理的並行度的設定

應該是要設定的足夠大,大到可以完全合理的利用你的叢集資源;比如上面的例子,總共叢集有150個cpu core,可以並行執行150個task。那麼就應該將你的Application的並行度,至少設定成150,才能完全有效的利用你的叢集資源,讓150個task,並行執行;而且task增加到150個以後,即可以同時並行執行,還可以讓每個task要處理的資料量變少;比如總共150G的資料要處理,如果是100個task,每個task計算1.5G的資料;現在增加到150個task,可以並行執行,而且每個task主要處理1G的資料就可以。

很簡單的道理,只要合理設定並行度,就可以完全充分利用你的叢集計算資源,並且減少每個task要處理的資料量,最終,就是提升你的整個Spark作業的效能和執行速度。

  1. task數量,至少設定成與Spark application的總cpu core數量相同(最理想情況,比如總共150個cpu core,分配了150個task,一起執行,差不多同一時間執行完畢)
  2. 官方是推薦,task數量,設定成spark application總cpu core數量的2~3倍,比如150個cpu core,基本要設定task數量為300~500;

實際情況,與理想情況不同的,有些task會執行的快一點,比如50s就完了,有些task,可能會慢一點,要1分半才執行完,所以如果你的task數量,剛好設定的跟cpu core數量相同,可能還是會導致資源的浪費,因為,比如150個task,10個先執行完了,剩餘140個還在執行,但是這個時候,有10個cpu core就空閒出來了,就導致了浪費。那如果task數量設定成cpu core總數的2~3倍,那麼一個task執行完了以後,另一個task馬上可以補上來,就儘量讓cpu core不要空閒,同時也是儘量提升spark作業執行的效率和速度,提升效能。

如何設定一個Spark Application的並行度?

spark.default.parallelism 
SparkConf conf = new SparkConf()
  .set("spark.default.parallelism", "500")
複製程式碼

重構RDD架構和持久化RDD

Spark學習——效能調優(一)

  • 如上圖第一條DAG,預設情況下,多次對一個RDD執行運算元,去獲取不同的RDD;都會對這個RDD以及之前的父RDD,全部重新計算一次;所以在計算RDD3和RDD4的時候,前面的讀取HDFS檔案,然後對RDD1執行運算元,獲取 到RDD2會計算兩遍。
    這種情況,是絕對絕對,一定要避免的,一旦出現一個RDD重複計算的情況,就會導致效能急劇降低。比如,HDFS->RDD1-RDD2的時間是15分鐘,那麼此時就要走兩遍,變成30分鐘。

  • 另外一種情況,在上圖第二條DAG中國,從一個RDD到幾個不同的RDD,運算元和計算邏輯其實是完全一樣的,結果因為人為的疏忽,計算了多次,獲取到了多個RDD。這個也是儘量要避免的。

如何重構RDD架構

  • 1. RDD架構重構與優化
    儘量去複用RDD,差不多的RDD,可以抽取稱為一個共同的RDD,供後面的RDD計算時,反覆使用。

  • 2. 公共RDD一定要實現持久化
    對於要多次計算和使用的公共RDD,一定要進行持久化。
    持久化,也就是說,將RDD的資料快取到記憶體中/磁碟中,(BlockManager),以後無論對這個RDD做多少次計算,那麼都是直接取這個RDD的持久化的資料,比如從記憶體中或者磁碟中,直接提取一份資料。

Spark學習——效能調優(一)

  • 3. 持久化,是可以進行序列化的
    如果正常將資料持久化在記憶體中,那麼可能會導致記憶體的佔用過大,這樣的話,也許,會導致OOM記憶體溢位。
    當純記憶體無法支撐公共RDD資料完全存放的時候,就優先考慮,使用序列化的方式在純記憶體中儲存。將RDD的每個partition的資料,序列化成一個大的位元組陣列,就一個物件;序列化後,大大減少記憶體的空間佔用。
    序列化的方式,唯一的缺點就是,在獲取資料的時候,需要反序列化。

  • 4. 為了資料的高可靠性,而且記憶體充足,可以使用雙副本機制,進行持久化
    持久化的雙副本機制,持久化後的一個副本,因為機器當機了,副本丟了,就還是得重新計算一次;持久化的每個資料單元,儲存一份副本,放在其他節點上面;從而進行容錯;一個副本丟了,不用重新計算,還可以使用另外一份副本。
    這種方式,僅僅針對你的記憶體資源極度充足

廣播大變數

關於廣播可以閱讀 Spark學習(二)——RDD基礎 中的共享變數。

為什麼要使用廣播大變數

task執行的運算元中,使用了外部的變數,然後driver會把變數以task的形式傳送到excutor端,每個task都會獲取一份變數的副本。如果有很多個task,就會有很多給excutor端攜帶很多個變數,如果這個變數非常大的時候,就可能會造成記憶體溢位。

比如,外部變數map是1M。總共,你前面調優都調的特好,資源給的到位,配合著資源,並行度調節的絕對到位,1000個task。大量task的確都在並行執行。
這些task裡面都用到了佔用1M記憶體的map,那麼首先,map會拷貝1000份副本,通過網路傳輸到各個task中去,給task使用。總計有1G的資料,會通過網路傳輸。網路傳輸的開銷,不容樂觀啊!!!網路傳輸,也許就會消耗掉你的spark作業執行的總時間的一小部分。
map副本,傳輸到了各個task上之後,是要佔用記憶體的。1個map的確不大,1M;1000個map分佈在你的叢集中,一下子就耗費掉1G的記憶體。對效能會有什麼影響呢?
不必要的記憶體的消耗和佔用,就導致了,你在進行RDD持久化到記憶體,也許就沒法完全在記憶體中放下;就只能寫入磁碟,最後導致後續的操作在磁碟IO上消耗效能;
你的task在建立物件的時候,也許會發現堆記憶體放不下所有物件,也許就會導致頻繁的垃圾回收器的回收,GC。GC的時候,一定是會導致工作執行緒停止,也就是導致Spark暫停工作那麼一點時間。頻繁GC的話,對Spark作業的執行的速度會有相當可觀的影響。

廣播大變數原理

Spark學習——效能調優(一)

  • 廣播變數,初始的時候,就在Drvier上有一份副本。
  • task在執行的時候,想要使用廣播變數中的資料,此時首先會在自己本地的Executor對應的BlockManager中,嘗試獲取變數副本;如果本地沒有,那麼就從Driver遠端拉取變數副本,並儲存在本地的BlockManager中;此後這個executor上的task,都會直接使用本地的BlockManager中的副本。
  • executor的BlockManager除了從driver上拉取,也可能從其他節點的BlockManager上拉取變數副本,距離越近越好。

使用Kryo序列化

  • 預設情況下,Spark內部是使用Java的序列化機制,ObjectOutputStream / ObjectInputStream,物件輸入輸出流機制,來進行序列化。這種預設序列化機制的好處在於,處理起來比較方便;也不需要我們手動去做什麼事情,只是,你在運算元裡面使用的變數,必須是實現Serializable介面的,可序列化即可。
  • 但是缺點在於,預設的序列化機制的效率不高,序列化的速度比較慢;序列化以後的資料,佔用的記憶體空間相對還是比較大。 可以手動進行序列化格式的優化,Spark支援使用Kryo序列化機制。Kryo序列化機制,比預設的Java序列化機制,速度要快,序列化後的資料要更小,大概是Java序列化機制的1/10。所以Kryo序列化優化以後,可以讓網路傳輸的資料變少;在叢集中耗費的記憶體資源大大減少。

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

  1. 運算元函式中使用到的外部變數
    運算元函式中使用到的外部變數,使用Kryo以後:優化網路傳輸的效能,可以優化叢集中記憶體的佔用和消耗
  2. 持久化RDD時進行序列化,StorageLevel.MEMORY_ONLY_SER等
    持久化RDD,優化記憶體的佔用和消耗;持久化RDD佔用的記憶體越少,task執行的時候,建立的物件,就不至於頻繁的佔滿記憶體,頻繁發生GC。
  3. shuffle
    可以優化網路傳輸的效能

如何使用Kryo

  • 首先第一步,在SparkConf中設定一個屬性,spark.serializer,org.apache.spark.serializer.KryoSerializer類;
SparkConf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
複製程式碼

Kryo之所以沒有被作為預設的序列化類庫的原因,就要出現了:主要是因為Kryo要求,如果要達到它的最佳效能的話,那麼就一定要註冊你自定義的類(比如,你的運算元函式中使用到了外部自定義型別的物件變數,這時,就要求必須註冊你的類,否則Kryo達不到最佳效能)。

  • 第二步,註冊你使用到的,需要通過Kryo序列化的一些自定義
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.registerKryoClasses(new Class[]{CategorySortKey.class})
複製程式碼

使用fastUtil優化資料格式

fastutil是擴充套件了Java標準集合框架(Map、List、Set;HashMap、ArrayList、HashSet)的類庫,提供了特殊型別的map、set、list和queue;
fastutil能夠提供更小的記憶體佔用,更快的存取速度;我們使用fastutil提供的集合類,來替代自己平時使用的JDK的原生的Map、List、Set,好處在於,fastutil集合類,可以減小記憶體的佔用,並且在進行集合的遍歷、根據索引(或者key)獲取元素的值和設定元素的值的時候,提供更快的存取速度;
fastutil也提供了64位的array、set和list,以及高效能快速的,以及實用的IO類,來處理二進位制和文字型別的檔案; fastutil最新版本要求Java 7以及以上版本;

使用場景

Spark中應用fastutil的場景:

  1. 如果運算元函式使用了外部變數;那麼第一,你可以使用Broadcast廣播變數優化;第二,可以使用Kryo序列化類庫,提升序列化效能和效率;第三,如果外部變數是某種比較大的集合,那麼可以考慮使用fastutil改寫外部變數,首先從源頭上就減少記憶體的佔用,通過廣播變數進一步減少記憶體佔用,再通過Kryo序列化類庫進一步減少記憶體佔用。
  2. 在你的運算元函式裡,也就是task要執行的計算邏輯裡面,如果有邏輯中,出現,要建立比較大的Map、List等集合,可能會佔用較大的記憶體空間,而且可能涉及到消耗效能的遍歷、存取等集合操作;那麼此時,可以考慮將這些集合型別使用fastutil類庫重寫,使用了fastutil集合類以後,就可以在一定程度上,減少task建立出來的集合型別的記憶體佔用。避免executor記憶體頻繁佔滿,頻繁喚起GC,導致效能下降。

fastutil的使用

第一步:在pom.xml中引用fastutil的包

<dependency>
    <groupId>fastutil</groupId>
    <artifactId>fastutil</artifactId>
    <version>5.0.9</version>
</dependency>
複製程式碼

第二步:List => IntList

IntList fastutilExtractList = new IntArrayList();
複製程式碼

調節資料本地化時長

Spark學習——效能調優(一)
Spark在Driver上,對Application的每一個stage的task,進行分配之前,都會計算出每個task要計算的是哪個分片資料,RDD的某個partition;Spark的task分配演算法,優先,會希望每個task正好分配到它要計算的資料所在的節點,這樣的話,就不用在網路間傳輸資料;

但是呢,通常來說,有時,事與願違,可能task沒有機會分配到它的資料所在的節點,為什麼呢,可能那個節點的計算資源和計算能力都滿了;所以呢,這種時候,通常來說,Spark會等待一段時間,預設情況下是3s鍾(不是絕對的,還有很多種情況,對不同的本地化級別,都會去等待),到最後,實在是等待不了了,就會選擇一個比較差的本地化級別,比如說,將task分配到靠它要計算的資料所在節點,比較近的一個節點,然後進行計算。

但是對於第二種情況,通常來說,肯定是要發生資料傳輸,task會通過其所在節點的BlockManager來獲取資料,BlockManager發現自己本地沒有資料,會通過一個getRemote()方法,通過TransferService(網路資料傳輸元件)從資料所在節點的BlockManager中,獲取資料,通過網路傳輸回task所在節點。

對於我們來說,當然不希望是類似於第二種情況的了。最好的,當然是task和資料在一個節點上,直接從本地executor的BlockManager中獲取資料,純記憶體,或者帶一點磁碟IO;如果要通過網路傳輸資料的話,那麼實在是,效能肯定會下降的,大量網路傳輸,以及磁碟IO,都是效能的殺手。

什麼時候要調節這個引數?

觀察日誌,spark作業的執行日誌,推薦大家在測試的時候,先用client模式,在本地就直接可以看到比較全的日誌。 日誌裡面會顯示,starting task。。。,PROCESS LOCAL、NODE LOCAL 觀察大部分task的資料本地化級別

如果大多都是PROCESS_LOCAL,那就不用調節了 如果是發現,好多的級別都是NODE_LOCAL、ANY,那麼最好就去調節一下資料本地化的等待時長 調節完,應該是要反覆調節,每次調節完以後,再來執行,觀察日誌 看看大部分的task的本地化級別有沒有提升;看看,整個spark作業的執行時間有沒有縮短

你別本末倒置,本地化級別倒是提升了,但是因為大量的等待時長,spark作業的執行時間反而增加了,那就還是不要調節了

怎麼調節?

new SparkConf()
  .set("spark.locality.wait", "10")
複製程式碼
  • spark.locality.wait,預設是3s;6s,10s

預設情況下,下面3個的等待時長,都是跟上面那個是一樣的,都是3s

  • spark.locality.wait.process
  • spark.locality.wait.node
  • spark.locality.wait.rack

相關文章