1. 程式人生 > >Spark調優之JVM調優

Spark調優之JVM調優

今天給大家分享Spark調優相關的JVM調優,這個調優方法在開發中也很常見,他主要分為兩種,一種是降低cache操作的記憶體佔比,一種是調節executor堆外記憶體和降低連線等待時長。
在此之前,我們先來了解一下JVM的堆記憶體

堆記憶體存放我們建立的一些物件,有老年代和年輕代。理想情況下,老年代都是放一些生命週期很長的物件,數量應該是很少的,比如資料庫連線池。我們在spark task執行運算元函式(我們自己寫的),可能會建立很多物件,這些物件都是要放入JVM年輕代中的。
每一次放物件的時候,都是放入eden區域,和其中一個survivor區域。另外一個survivor區域是空閒的。
當eden區域和一個survivor區域放滿了以後(spark執行過程中,產生的物件實在太多了),就會觸發minor gc,小型垃圾回收。把不再使用的物件,從記憶體中清空,給後面新建立的物件騰出來點兒地方。
清理掉了不再使用的物件之後,那麼也會將存活下來的物件(還要繼續使用的),放入之前空閒的那一個survivor區域中。這裡可能會出現一個問題。預設eden、survior1和survivor2的記憶體佔比是8:1:1。問題是,如果存活下來的物件是1.5,一個survivor區域放不下。此時就可能通過JVM的擔保機制(不同JVM版本可能對應的行為),將多餘的物件,直接放入老年代了。
如果你的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停止工作了。等著垃圾回收結束。
記憶體不充足的時候,出現的問題:
1、頻繁minor gc,也會導致頻繁spark停止工作
2、老年代囤積大量活躍物件(短生命週期的物件),導致頻繁full gc,full gc時間很長,短則數十秒,長則數分鐘,甚至數小時。可能導致spark長時間停止工作。
3、嚴重影響咱們的spark的效能和執行的速度。

一、降低cache操作的記憶體佔比

spark中,堆記憶體又被劃分成了兩塊,一塊是專門用來給RDD的cache、persist操作進行RDD資料快取用的。另外一塊用來給spark運算元函式的執行使用的,存放函式中自己建立的物件。
預設情況下,給RDD cache操作的記憶體佔比,是0.6,60%的記憶體都給了cache操作了。但是問題是,如果某些情況下cache不是那麼的緊張,問題在於task運算元函式中建立的物件過多,然後記憶體又不太大,導致了頻繁的minor gc,甚至頻繁full gc,導致spark頻繁的停止工作。效能影響會很大。
針對上述這種情況,可以在任務執行介面,去檢視你的spark作業的執行統計,可以看到每個stage的執行情況,包括每個task的執行時間、gc時間等等。如果發現gc太頻繁,時間太長。此時就可以適當調價這個比例。
降低cache操作的記憶體佔比,大不了用persist操作,選擇將一部分快取的RDD資料寫入磁碟,或者序列化方式,配合Kryo序列化類,減少RDD快取的記憶體佔用。降低cache操作記憶體佔比,對應的,運算元函式的記憶體佔比就提升了。這個時候,可能就可以減少minor gc的頻率,同時減少full gc的頻率。對效能的提升是有一定的幫助的。
一句話,讓task執行運算元函式時,有更多的記憶體可以使用。
spark.storage.memoryFraction,0.6 -> 0.5 -> 0.4 -> 0.2

二、調節executor堆外記憶體與連線等待時長

調節executor堆外記憶體
在此之前我們先了解下什麼是堆外記憶體
堆記憶體完全由 JVM 負責分配和釋放,如果程式存在缺陷,有可能導致記憶體洩漏而溢位,丟擲 OOM 異常: java.lang.OutOfMemoryError。
除了堆記憶體,Java 還可以使用堆外記憶體,也稱直接記憶體(Direct Memory)。顧名思義,堆外記憶體是在 JVM Heap 之外分配的記憶體塊,並不是 JVM 規範中定義的記憶體區域。
堆外記憶體可直接分配和釋放,減少 GC 暫停時間,提高效率;可擴充套件,支援程序間共享,節省堆記憶體到堆外記憶體的拷貝等特點。如果程式存在缺陷,同樣有可能導致堆外記憶體洩漏而溢位:OutOfDirectMemoryError。

有時候,如果你的spark作業處理的資料量特別大,幾億資料量。然後spark作業一執行,時不時的報錯,shuffle file cannot find,executor、task lost,out of memory(記憶體溢位)。
可能是executor的堆外記憶體不太夠用,導致executor在執行的過程中,可能會記憶體溢位,可能導致後續的stage的task在執行的時候,要從一些executor中去拉取shuffle map output檔案,但是executor可能已經掛掉了,關聯的block manager也沒有了。所以會報shuffle output file not found,resubmitting task,executor lost。spark作業徹底崩潰。
上述情況下,就可以去考慮調節一下executor的堆外記憶體。也許就可以避免報錯。此外,有時堆外記憶體調節的比較大的時候,對於效能來說,也會帶來一定的提升。
可以調節堆外記憶體的上限:
–conf spark.yarn.executor.memoryOverhead=2048
spark-submit腳本里面,去用–conf的方式,去新增配置。用new SparkConf().set()這種方式去設定是沒有用的!一定要在spark-submit指令碼中去設定。
spark.yarn.executor.memoryOverhead(看名字,顧名思義,針對的是基於yarn的提交模式)
預設情況下,這個堆外記憶體上限大概是300M。通常在專案中,真正處理大資料的時候,這裡都會出現問題,導致spark作業反覆崩潰,無法執行。此時就會去調節這個引數,到至少1G(1024M),甚至說2G、4G。
通常這個引數調節上去以後,就會避免掉某些JVM OOM的異常問題,同時呢,會讓整體spark作業的效能,得到較大的提升。

調節連線等待時長

我們知道,executor會優先從自己本地關聯的BlockManager中獲取某份資料。如果本地block manager沒有的話,那麼會通過TransferService,去遠端連線其他節點上executor的block manager去獲取。
而此時上面executor去遠端連線的那個executor,因為task建立的物件特別大,特別多,
頻繁的讓JVM堆記憶體滿溢,正在進行垃圾回收。而處於垃圾回收過程中,所有的工作執行緒全部停止,相當於只要一旦進行垃圾回收,spark / executor停止工作,無法提供響應。
此時呢,就會沒有響應,無法建立網路連線,會卡住。spark預設的網路連線的超時時長,是60s,如果卡住60s都無法建立連線的話,那麼就宣告失敗了。
報錯幾次,幾次都拉取不到資料的話,可能會導致spark作業的崩潰。也可能會導致DAGScheduler,反覆提交幾次stage。TaskScheduler反覆提交幾次task。大大延長我們的spark作業的執行時間。
可以考慮調節連線的超時時長:
–conf spark.core.connection.ack.wait.timeout=300
spark-submit指令碼,切記,不是在new SparkConf().set()這種方式來設定的。
spark.core.connection.ack.wait.timeout(spark core,connection,連線,ack,wait timeout,建立不上連線的時候,超時等待時長)
調節這個值比較大以後,通常來說,可以避免部分的偶爾出現的某某檔案拉取失敗,某某檔案lost掉了。