intel關於spark gc的優化建議


Apache Spark由於其出色的性能、簡單的接口和豐富的分析和計算庫而獲得了廣泛的行業應用。與大數據生態系統中的許多項目一樣,Spark在Java虛擬機(JVM)上運行。因為Spark可以在內存中存儲大量數據,因此它主要依賴於Java的內存管理和垃圾收集(GC)。但是現在,了解Java的GC選項和參數的用戶可以調優他們的Spark應用程序的最佳性能。本文描述了如何為Spark配置JVM的垃圾收集器,並給出了實際的用例來解釋如何調優GC,以提高Spark的性能。我們在調優GC時考慮關鍵因素,如收集吞吐量和延遲。
 
spark和GC的介紹:
隨着Spark的廣泛應用,Spark應用程序的穩定性和性能調優問題越來越成為人們關注的話題。由於Spark以內存為中心的方法,通常使用100GB或更多內存作為堆空間,這在傳統的Java應用程序中很少見到。在與使用Spark的大公司合作時,我們收到了許多關於GC在執行Spark應用程序時所面臨的各種挑戰的擔憂。例如,垃圾收集需要很長的時間,導致程序經歷長時間的延遲,甚至在嚴重的情況下崩潰。在本文中,我們使用真實的例子,結合具體的問題,討論可以緩解這些問題的Spark應用程序的GC調優方法。
Java應用程序通常使用這兩種GC策略 : 並發標記清理(Concurrent Mark Sweep garbage collection簡稱CMS)垃圾收集和並行垃圾收集(ParallelOld garbage collection)。前者旨在降低延遲,而后者則是針對更高的吞吐量。這兩種策略都有性能瓶頸:CMS GC不執行壓縮,而並行GC只執行全堆壓縮,這會導致相當大的停頓時間。我們建議客戶按照應用的實際需求選擇不同的策略。對於需要實時響應的應用,推薦CMS GC;對於離線分析的應用,推薦使用並行GC。
那么,對於像Spark這樣支持流計算和傳統批處理的計算框架,我們能找到一個最佳的GC收集器嗎?Hotspot JVM1.6 版本引入了垃圾收集的第三個策略:The Garbage-First GC (簡稱G1 GC)。G1收集器由Oracle計划,作為CMS GC的長期替代品。最重要的是,G1收集器的目標是實現高吞吐量和低延遲的雙特點。在詳細介紹使用Spark的G1收集器之前,讓我們先討論一下Java GC基礎的一些背景知識。
 
JVM GC工作流程:
 

 

 

Spark的內存管理介紹
彈性分布數據集(RDD)是Spark的核心抽象。RDD的創建和緩存與內存消耗密切相關。Spark允許用戶將數據進行緩存以便在應用程序中重用,從而避免重復計算造成的開銷。持久化的數據自然是存放在JVM中的。Spark的executor將JVM堆空間划分為兩個部分:一部分用於存放持久化的數據(storeage),另一部分用作RDD轉換期間的內存消耗(shuffle)。我們可以使用spark.storage來調整這兩個部分的比例(1.6版本之前)
 
當發現由GC延遲引起的效率下降時,我們應該首先檢查並確保Spark應用程序是否以有效的方式使用有限的內存空間。內存空間RDD占用的空間越小,程序執行的堆空間就越多,從而可以提高GC效率;相反,RDDs的過度內存消耗導致了老年代大量的緩沖對象導致的性能損失。在這里,我們用一個例子來介紹這一點:
 
例如,有一個Spark應用程序,它執行簡單的迭代計算。反復迭代計算,每步迭代的結果都會被持久化到內存空間中。在程序執行過程中,我們觀察到當迭代次數增加時,進程所使用的內存空間會快速增長,從而導致GC變得更糟。當我們仔細研究時,我們發現它會在內存中緩存每一個RDD,而且不會隨着時間的推移而釋放它們,哪怕它們在下一次迭代之后不會被使用。那么這將導致內存消耗增長,從而引發更多GC嘗試。我們在SPARK-2661中刪除了這種不必要的緩存。在此修改緩存后,RDD大小在三個迭代和緩存空間被有效控制后穩定(如表1所示)。結果,GC效率大大提高,程序的總運行時間縮短了10%~20%。
 

 

 

結論:
當GC時間很長時,它可能表明內存空間沒有被Spark應用程序有效地使用。您可以通過顯式地清理緩存后的RDD,從而提高性能。
 
怎么選擇合適的GC策略
如果我們的應用程序已經可以高效地使用內存,那么下一步就是選擇使用哪種GC策略以及如何進行GC調優了。在這里我們啟用了一個四節點集群,給每個executor分配了88GB的堆內存,並以standalone模式啟動Spark來進行我們的實驗。最初我們使用默認的Spark Parallel GC,發現因為spark應用程序的內存開銷比較大,大多數的對象不能在一個相當短的生命周期被回收,Parallel GC經常出現Full GC。更糟糕的是,Parallel GC提供了非常有限的性能調優選項,因此我們只能使用一些基本參數來調整性能,比如每一代的大小比例,以及在對象被提升到老年代之前的副本數量。但是這些調優策略也只是推遲了Full GC,所以並行GC調優對長時間運行的應用程序沒有幫助。因此,在本文中,我們不進行 Parallel GC調優。表2顯示了Parallel GC的操作,很明顯,當發生FULL GC時,CPU利用率最低。
 
Table 2: Parallel GC Running Status (Before Tuning)
 
CMS GC對於Spark應用中FULL GC的調優選擇同樣少之又少。此外,CMS GC FULL GC暫停時間比Parallel GC還要長,大大降低了應用程序的吞吐量。
 
接下來,我們使用G1 GC策略(默認配置)運行我們的應用程序。令我們驚訝的是,G1 GC也提供了讓人難以接受的FULL GC(請參閱圖3中的“CPU利用率”,顯然作業3暫停了將近100秒),一個長時間的暫停顯著地拖了整個應用程序的運行。如表4所示,雖然總運行時間略長於Parallel GC,但G1 GC的性能略優於CMS GC。
 
Table 3: G1 GC Running Status (Before Tuning)

 

三種GC策略下,spark應用消耗的時間(均使用默認參數,未做任何優化)
Table 4 Comparison of Three Garbage Collectors’ Program Running Time (88GB Heap before tuning)

 

 

基於輸出的日志對G1 GC進行優化
 
首先,我們希望JVM能夠在日志中記錄更多的GC細節。對於Spark,我們可以通過spark.executor.extraJavaOptions設置一些關於JVM的參數。比如:
-XX:+PrintFlagsFinal -XX:+PrintReferenceGC -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy -XX:+UnlockDiagnosticVMOptions -XX:+G1SummarizeConcMark
定義了這些參數以后,我們可以詳細的跟蹤GC的日志記錄(output to $SPARK_HOME/work/$ app_id/$executor_id/stdout at each worker node)接下來,我們可以根據GC日志分析問題的根源,並學習如何提高程序性能。
 
G1 GC日志的結構(具體見原文)
 
從日志中我們可以看到G1 GC日志有一個非常清晰的層次結構。日志列出了暫停發生的時間和原因,以及不同線程的時間消耗、及CPU時間的最大值和平均值。最后,G1 GC在暫停之后列出清理結果,以及總時間消耗。
在當前G1 GC運行日志中,我們發現了這樣一個特殊的塊:(具體見原文)
 

我們可以看到,最大的性能下降是由FULL GC引起的,並且在日志輸出的結果中顯示內存耗盡,內存溢出等等(對於不同的JVM版本日志顯示略有不同)。而這個原因是G1 GC收集器試圖給某些區域進行垃圾回收時,它沒有找到可以復制活動對象的自由區域(在GC時,不被引用的對象會被回收掉,還處於引用的對象會被轉存到另外一個區域),這種情況稱為“疏散失敗”,並且將導致FULL GC發生。顯然,G1 GC中的FULL GC比Parallel GC更糟糕,因此我們必須盡量避免GC,以獲得更好的性能。為了避免G1 GC的FULL GC,有兩種常用的方法:
 
一:減小initiatingheapancypercent選項的值(默認值是45),讓G1 GC可以較早的啟動初始並發標記,這樣可以很大概率避免FULL GC的發生。
二:增加ConcGCThreads選項的值,為並發標記提供更多的線程,這樣我們就可以加速並發標記階段。需要注意的是,這個選項也會占用一些工作線程資源,這取決於你實際有多少CPU以及CPU的利用率。
 
對這兩個選項進行調優,可以盡可能的避免FULL GC的發生。FULL GC被限制后,性能得到顯著提高。但是,我們仍然在GC期間發現了長時間的暫停。在進一步的調查中,我們發現在我們的日志中有以下事件:
280.008: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 62344134656 bytes, allocation request: 46137368 bytes, threshold: 42520176225 bytes (45.00 %), source: concurrent humongous allocation]
 
在這里我們可以看到一些極大的對象(這些對象的大小是標准區域的50%甚至更大)。G1 GC會將這些對象放到相鄰的區域集合中。由於復制這些對象會消耗大量的資源,所以這些對象會被直接分配到老年代,然后將其分類為巨大的區域。在jdk 1.8.0_u40之前,需要一個完整的堆分析來回收這些巨大的區域[JDK-8027959]。如果有許多這樣的對象,堆將很快被填滿,並且回收它們的代價太大。即使后來官方有了修復,但連續區域的分配仍然很昂貴(特別是在遇到嚴重堆碎片時),因此我們希望避免創建這種大小的對象。我們可以增加 g1heapregions 大小的值,以減少創建巨大區域的可能性,但是如果我們使用相對較大的堆,32M已經是它的最大值了。這意味着我們只能分析程序來找到這些對象並減少它們的創建。否則,它可能會導致更多的並發標記階段,在此之后,您需要仔細地調整混合GC相關的knobs(e.g., -XX:G1HeapWastePercent -XX:G1MixedGCLiveThresholdPercent),以避免長期混合GC暫停(由大量巨大的對象引起)。
 
接下來,我們可以分析從單個GC不斷循環的開始一直到混合GC的結束這段時間的間隔。如果時間太長,可以考慮增加ConcGCThreads的值,但是注意這將占用更多的CPU資源。
 
G1 GC也有減少STW暫停長度的方法,以在垃圾收集的並發階段做更多的工作。如上所述,G1 GC為每個區域維護一個Remembered Set(簡稱RSet),以跟蹤外部區域對給定區域的對象引用,G1收集器將在STW階段和並發階段更新RSet。如果您想要減少G1 GC暫停的時間,您可以降低G1RSetUpdatingPauseTimePercent的值,同時增加G1ConcRefinementThreads的值。前者用於指定總STW時間內rset更新時間的期望比率,默認值為10%,而后者用於定義在程序運行期間維護rset的線程數。有了這兩個選項,我們可以將更多的rset的工作負載從STW階段轉移到並發階段。
此外,對於長時間運行的應用程序,我們使用AlwaysPreTouch 選項,因此JVM在啟動時應用了OS所需要的所有內存,並避免了動態應用程序。這提高了運行時性能,以延長啟動時間。
最終,經過幾輪GC參數調整后,我們到達了表5中的結果。與之前的結果相比,我們最終獲得了更令人滿意的運行效率。
 
Table 5 G1 GC Running Status (after Tuning)
 
Configuration Options:
-Xms88g -Xmx88g
-XX:+UseG1GC
-XX:+UnlockDiagnosticVMOptions
-XX:+G1SummarizeConcMark
-XX:InitiatingHeapOccupancyPercent=35
-XX:ConcGCThread=20
 
 
通過GC日志分析可以獲得細粒度的優化。調優后,我們成功地將應用程序的運行時間縮短到4.3分鍾。與調優前的運行時間相比,性能提高了1.7倍;與Parallel GC相比,性能提升了差不多1.5倍。
總結
對於嚴重依賴內存計算的Spark應用程序來說,GC調優尤為重要。當GC出現問題時,不要急於調試GC本身。首先考慮Spark程序的內存管理是否低效,比如持久化和釋放緩存中的RDD。在進行GC調優時,我們首先建議使用G1 GC來運行Spark應用程序。G1 GC可以很好地處理不斷增長的堆大小。對於G1,需要更少的選項來提供更高的吞吐量和更低的延遲。當然,GC調優沒有固定模式。各種應用程序具有不同的特性,為了解決不可預測的情況,必須根據日志和其他取證來掌握GC調優的技術。最后,我們不要忘記通過程序的邏輯和代碼進行優化,比如減少中間對象的創建或復制,控制大型對象的創建,將長時間的對象存儲在堆外,等等。
 
通過使用G1 GC,我們在Spark應用程序中取得了重大的性能改進。Spark未來的工作將把內存管理責任從Java的垃圾收集器轉移到Spark本身。這將大大減少Spark應用程序的調優需求。但是現在來說,GC策略的選擇和調優依舊是提高Sparkd應用性能的關鍵。

原文:

https://databricks.com/blog/2015/05/28/tuning-java-garbage-collection-for-spark-applications.html?from=timeline


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM