1. 程式人生 > >Spark效能優化:JVM引數調優

Spark效能優化:JVM引數調優

關於JVM垃圾回收種類

Minor GC
從年輕代空間(包括 Eden 和 Survivor 區域)回收記憶體被稱為 Minor GC。這一定義既清晰又易於理解。但是,當發生Minor GC事件的時候,有一些有趣的地方需要注意到:
當 JVM 無法為一個新的物件分配空間時會觸發 Minor GC,比如當 Eden 區滿了。所以分配率越高,越頻繁執行 Minor GC。
記憶體池被填滿的時候,其中的內容全部會被複制,指標會從0開始跟蹤空閒記憶體。Eden 和 Survivor 區進行了標記和複製操作,取代了經典的標記、掃描、壓縮、清理操作。所以 Eden 和 Survivor 區不存在記憶體碎片。寫指標總是停留在所使用記憶體池的頂部。
執行 Minor GC 操作時,不會影響到永久代。從永久代到年輕代的引用被當成 GC roots,從年輕代到永久代的引用在標記階段被直接忽略掉。
質疑常規的認知,所有的 Minor GC 都會觸發“全世界的暫停(stop-the-world)”,停止應用程式的執行緒。對於大部分應用程式,停頓導致的延遲都是可以忽略不計的。其中的真相就 是,大部分 Eden 區中的物件都能被認為是垃圾,永遠也不會被複制到 Survivor 區或者老年代空間。如果正好相反,Eden 區大部分新生物件不符合 GC 條件,Minor GC 執行時暫停的時間將會長很多。
所以 Minor GC 的情況就相當清楚了——每次 Minor GC 會清理年輕代的記憶體。


Major GC vs Full GC


大家應該注意到,目前,這些術語無論是在 JVM 規範還是在垃圾收集研究論文中都沒有正式的定義。但是我們一看就知道這些在我們已經知道的基礎之上做出的定義是正確的,Minor GC 清理年輕帶記憶體應該被設計得簡單:


Major GC 是清理永久代。
Full GC 是清理整個堆空間—包括年輕代和永久代。

很不幸,實際上它還有點複雜且令人困惑。首先,許多 Major GC 是由 Minor GC 觸發的,所以很多情況下將這兩種 GC 分離是不太可能的。另一方面,許多現代垃圾收集機制會清理部分永久代空間,所以使用“cleaning”一詞只是部分正確。

這使得我們不用去關心到底是叫 Major GC 還是 Full GC,大家應該關注當前的 GC 是否停止了所有應用程式的執行緒,還是能夠併發的處理而不用停掉應用程式的執行緒。

這種混亂甚至內建到 JVM 標準工具。下面一個例子很好的解釋了我的意思。讓我們比較兩個不同的工具 Concurrent Mark 和 Sweep collector (-XX:+UseConcMarkSweepGC)在 JVM 中執行時輸出的跟蹤記錄。

依據物件的存活週期進行分類,短命物件歸為新生代,長命物件歸為老年代。
根據不同代的特點,選取合適的收集演算法
新生代:少量物件存活,適合複製演算法
老年代:大量物件存活,適合標記清理或者標記壓縮


Young:主要是用來存放新生的物件。
Old:主要存放應用程式中生命週期長的記憶體物件。
Permanent:是指記憶體的永久儲存區域,主要存放Class和Meta的資訊,Class在被 Load的時候被放入PermGen space區域. 它和和存放Instance的Heap區域不同,GC(Garbage Collection)不會在主程式執行期對PermGen space進行清理,所以如果你的APP會LOAD很多CLASS的話,就很可能出現PermGen space錯誤


上圖演示新生代GC過程用到的複製演算法,黃色表示死物件,綠色表示剩餘空間,紅色表示倖存物件





上圖演示老年代GC時用到的標記-壓縮演算法

總結一下,物件一般出生在Eden區,年輕代GC過程中,物件在2個倖存區之間移動,如果物件存活到適當的年齡,會被移動到老年代。當物件在老年代死亡時,就需要更高級別的GC,更重量級的GC演算法(複製演算法不適用於老年代,因為沒有多餘的空間用於複製)

Spark中Java虛擬機器垃圾回收調優的背景 如果在持久化RDD的時候,持久化了大量的資料,那麼Java虛擬機器的垃圾回收就可能成為一個性能瓶頸。因為Java虛擬機器會定期進行垃圾回收,此時就會追蹤所有的java物件,並且在垃圾回收時,找到那些已經不在使用的物件,然後清理舊的物件,來給新的物件騰出記憶體空間。 垃圾回收的效能開銷,是跟記憶體中的物件的數量,成正比的。所以,對於垃圾回收的效能問題,首先要做的就是,使用更高效的資料結構,比如array和string;其次就是在持久化rdd時,使用序列化的持久化級別,而且用Kryo序列化類庫,這樣,每個partition就只是一個物件——一個位元組陣列。 監測垃圾回收 我們可以對垃圾回收進行監測,包括多久進行一次垃圾回收,以及每次垃圾回收耗費的時間。只要在spark-submit指令碼中,增加一個配置即可, --conf "spark.executor.extraJavaOptions=-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps"  但是要記住,這裡雖然會打印出Java虛擬機器的垃圾回收的相關資訊,但是是輸出到了worker上的日誌中,而不是driver的日誌中。 但是這種方式也只是一種,其實也完全可以通過SparkUI(4040埠)來觀察每個stage的垃圾回收的情況。 優化executor記憶體比例 對於垃圾回收來說,最重要的就是調節RDD快取佔用的記憶體空間,與運算元執行時建立的物件佔用的記憶體空間的比例。預設情況下,Spark使用每個executor 60%的記憶體空間來快取RDD,那麼在task執行期間建立的物件,只有40%的記憶體空間來存放。 在這種情況下,很有可能因為你的記憶體空間的不足,task建立的物件過大,那麼一旦發現40%的記憶體空間不夠用了,就會觸發Java虛擬機器的垃圾回收操作。因此在極端情況下,垃圾回收操作可能會被頻繁觸發。 在上述情況下,如果發現垃圾回收頻繁發生。那麼就需要對那個比例進行調優,使用new SparkConf().set("spark.storage.memoryFraction", "0.5")即可,可以將RDD快取佔用空間的比例降低,從而給更多的空間讓task建立的物件進行使用。 因此,對於RDD持久化,完全可以使用Kryo序列化,加上降低其executor記憶體佔比的方式,來減少其記憶體消耗。給task提供更多的記憶體,從而避免task的執行頻繁觸發垃圾回收。 高階垃圾回收調優(一) Java堆空間被劃分成了兩塊空間,一個是年輕代,一個是老年代。年輕代放的是短時間存活的物件,老年代放的是長時間存活的物件。年輕代又被劃分了三塊空間,Eden、Survivor1、Survivor2。 首先,Eden區域和Survivor1區域用於存放物件,Survivor2區域備用。建立的物件,首先放入Eden區域和Survivor1區域,如果Eden區域滿了,那麼就會觸發一次Minor GC,進行年輕代的垃圾回收。Eden和Survivor1區域中存活的物件,會被移動到Survivor2區域中。然後Survivor1和Survivor2的角色調換,Survivor1變成了備用。 如果一個物件,在年輕代中,撐過了多次垃圾回收,都沒有被回收掉,那麼會被認為是長時間存活的,此時就會被移入老年代。此外,如果在將Eden和Survivor1中的存活物件,嘗試放入Survivor2中時,發現Survivor2放滿了,那麼會直接放入老年代。此時就出現了,短時間存活的物件,進入老年代的問題。 如果老年代的空間滿了,那麼就會觸發Full GC,進行老年代的垃圾回收操作 高階垃圾回收調優(二) Spark中,垃圾回收調優的目標就是,只有真正長時間存活的物件,才能進入老年代,短時間存活的物件,只能呆在年輕代。不能因為某個Survivor區域空間不夠,在Minor GC時,就進入了老年代。從而造成短時間存活的物件,長期呆在老年代中佔據了空間,而且Full GC時要回收大量的短時間存活的物件,導致Full GC速度緩慢。 如果發現,在task執行期間,大量full gc發生了,那麼說明,年輕代的Eden區域,給的空間不夠大。此時可以執行一些操作來優化垃圾回收行為: 1、包括降低spark.storage.memoryFraction的比例,給年輕代更多的空間,來存放短時間存活的物件; 2、給Eden區域分配更大的空間,使用-Xmn即可,通常建議給Eden區域,預計大小的4/3; 3、如果使用的是HDFS檔案,那麼很好估計Eden區域大小,如果每個executor有4個task,然後每個hdfs壓縮塊解壓縮後大小是3倍,此外每個hdfs塊的大小是64M,那麼Eden區域的預計大小就是:4 * 3 * 64MB,然後呢,再通過-Xmn引數,將Eden區域大小設定為4 * 3 * 64 * 4/3。 最後一點總結 對於垃圾回收的調優,儘量就是說,調節executor記憶體的比例就可以了。因為jvm的調優是非常複雜和敏感的。真的到了萬不得已的地方,並且對jvm相關的技術很瞭解,那麼再進行eden區域的調節,調優。 一些高階的引數 -XX:SurvivorRatio=4:如果值為4,那麼就是兩個Survivor跟Eden的比例是2:4,也就是說每個Survivor佔據的年輕代的比例是1/6,所以,你其實也可以嘗試調大Survivor區域的大小。 -XX:NewRatio=4:調節新生代和老年代的比例, 表示年輕代與年老代所佔比值為1:4,年輕代佔整個堆疊的1/5 Xms=Xmx 並且設定了Xmn的情況下,該引數不需要進行設定。

Shell提交指令碼例項:

#!/bin/bash  
source /etc/profile  
  
nohup /opt/modules/spark/bin/spark-submit \  
--master spark://10.130.2.20:7077 \  
--conf "spark.executor.extraJavaOptions=-XX:PermSize=8m -XX:SurvivorRatio=4 -XX:NewRatio=4 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps" \  
--driver-memory 1g \  
--executor-memory 1g \  
--total-executor-cores 48 \  
--conf "spark.ui.port=8088"  \  
--jars /opt/bin/sparkJars/kafka_2.10-0.8.2.1.jar,/opt/bin/sparkJars/spark-streaming-kafka_2.10-1.4.1.jar,/opt/bin/sparkJars/metrics-core-2.2.0.jar,/opt/bin/sparkJars  
/mysql-connector-java-5.1.26-bin.jar,/opt/bin/sparkJars/spark-streaming-kafka_2.10-1.4.1.jar \  
--class com.spark.streaming.Top3HotProduct \  
SparkApp.jar \  
> sparkApp.log 2>&1 & \