大數據框架中的Java虛擬機優化
近年以來,大數據應用取得了長足的進展,各種大數據處理框架也應運而生,并得到了業界的高度認可,如Hadoop生態、Spark系列、Flink、Cassandra、Hive等等。這類編程模型通常采用分治的思想,將大的數據處理作業拆分為多個小的計算任務,分配到分布式集群中的不同節點中運行,然后將結果匯聚起來,得出最終結果。
由于使用習慣等關系,業內主流的大數據處理框架都采用Java語言進行編寫。原因在于以下幾點:
1、很多程序開發人員對于Java語言比較熟悉,使用起來輕車熟路;
2、Java提供了便捷的自動內存管理機制,避免了用戶在處理內存過程中可能出現的問題;
3、Java基于JVM運行的特性使得Java程序可以一次編寫,多處運行,擺脫分布式集群中不同操作系統和硬件處理框架帶來的束縛,使得開發者可以更加專注于代碼邏輯的開發;
4、Java語言擁有成熟的社區和豐富的編程資源,可以實現快速開發,出現問題也可以快速尋求幫助。
正是基于這些優點,Java語言成為了目前最主流大數據編程技術。但是,在實際使用的過程中,開發者們發現了一系列JVM相關的性能瓶頸,主要包括以下幾個方面:
1、垃圾回收(GC)占用時間長,在一些大數據應用中,GC時間甚至可以達到總執行時間的50%;
2、GC頻率高,造成任務執行頻繁暫停,應用吞吐率降低,響應延遲升高;
3、GC算法擠占應用線程CPU資源,存在GC線程競爭時,大數據應用執行時間增長可達60%;
4、數據對象在分布式節點間傳輸時需要序列化和反序列化,在某些大數據應用中,用時占比可達30%;
5、JVM冷啟動時需要大量的類加載和代碼即時編譯工作,在應用執行中的用時可達數十秒;
6、JVM運行和維護需要內存消耗,在內存緊張的情況下,可能因為內存耗盡或內存碎片觸發OOM錯誤。
總的來說,這些問題的產生,可以歸納為以下一些原因:
1、內存使用壓力增大
與普通的Java應用不同,大數據應用是“內存密集”型的,應用的內存使用量更大,在大數據處理框架下,JVM的內存使用壓力具體來源于:
(1)大數據應用數據計算和存儲產生的大量內存消耗,大量數據在計算過程中需要同時被讀取到內存中,而一些應用為了更進一步加快處理速度,將中間數據的聚合和可重用數據也緩存在內存當中,這決定了JVM在執行大數據應用時將面對更大的內存使用量;
(2)數據在JVM堆內存當中以對象的形式存儲需要額外的內存占用,對象在JVM當中的數據結構包含了對象頭以及對其它對象的引用,而數據本身在對象中的空間占比往往不超過一半。這些對象的外殼伴隨著數據緩存在內存當中,也需要占用相當數量的空間;
由于JVM垃圾回收機制的原因,會經常觸發全局暫停,而這個問題很難通過簡單的增減內存大小來解決,如果降低內存大小,GC的觸發頻率會增加,對象被掃描和的去的次數增加,應用程序的吞吐量相應降低。可用內存不足還會影響到應用的正常緩存和處理機制,甚至引發內存溢出。而如果提升內存大小,單次GC則需要處理更多的數據對象,平均的暫停時間加長,應用程序的最大延遲相應增加。對于周期性標記掃描的GC算法而言,還會在最終觸發GC之前消耗更多CPU時序進行不必要的標記。
2、內存使用模式變化
大數據應用中數據在內存當中保留的時間周期與普通應用不盡相同。在傳統應用中,堆內存中創建的絕大部分對象在產生之后不久就不再被使用,經典的GC算法正是基于這種內存使用模式,將堆內存進行粗粒度的年代劃分,絕大部分瞬時對象會在針對年輕代的Minor GC當中很快被清理。而大數據應用產生的對象有兩種,一種是由控制大數據處理框架運行邏輯的代碼產生的,即控制路徑對象,它們的內存使用模式一般依舊符合弱世代假設。另一種是輸入數據和計算中間數據在大數據處理框架中封裝生產的,統稱為數據路徑對象。這種對象的內存使用模式要更加復雜,它們可能在內存中長時間累積或緩存,也可能在一個迭代輪次后被清理和輸出。通常來說,數據路徑所創建的對象數量遠超控制路徑。傳統GC算法并不能適應大數據環境下內存使用模式的這種變化,原因在于:
(1)當前GC算法下,長時間存活的數據路徑對象最終都會晉升到老年代中,它們在數次Minor GC當中幸存并最終晉升的過程中,需要在內存中多次移動。而對象移動是GC循環當中最耗時的部分,每一次移動都意味著內存讀寫,而內存位置的改變也需要對相關引用的指針進行更新。考慮到數據路徑對象的數量極為龐大,整個晉升過程會消耗大量CPU時間,觸發多次GC暫停;
(2)數據路徑對象在晉升到老年代之后,在作業執行的時間尺度上,短時間內也不會被回收。傳統的GC算法不會考慮這些對象的存活時間,在涉及到老年代空間的MajorGC或者Mixed GC之前還是會整個堆內存空間進行標記掃描,這些標記掃描過程對于長時間存活的數據對象來說是不必要的。當長時間存活對象占用老年代的比例過高,每次傳出較大代價的Major GC就只能回收有限大小的空間,可能造成GC頻繁觸發,部分緩存數據被迫轉移到磁盤,甚至出現OOM錯誤,浪費大量的CPU時間和全局暫停時間,影響到應用執行效率。
3、JVM與上層框架存在隔閡
大數據處理框架將計算任務分配到各個執行器JVM節點之后,并不會干預JVM的具體執行過程,每個執行器JVM獨立運行,并不感知分布式集群中其它執行器JVM的執行情況,作業的整體進度,以及集群和節點的內存資源使用情況,只是根據自身的運行狀態作出觸發GC,調整堆內存,進行代碼即時編譯等決策,而這些決策從歷史和全局的角度上觀察可能并不是最優的。原因在于:
(1)JVM不清楚任務執行產生的數據對象特征,例如對象數量、內存占用大小、生命周期等,只能根據弱世代假說,對所有對象進行統一的管理。由于大數據應用產生的大量對象長時間存活,JVM的內存管理效率會受到嚴重影響,而這些對象本可以通過大數據框架對用戶代碼和數據流的全局靜態分析進行甄別。
(2)大數據處理框架并不考慮JVM具體的內存管理機制,將所有JVM節點的內存當做連續的全局地址空間,但是實際上JVM在GC算法下對堆內存采取分代管理,存在非連續區域,對象在內存中離散分布,另外大數據處理框架在采用全局地址空間的物理架構下,可能產生大量跨節點對對象引用,給JVM的GC任務帶來了遠程內存訪問的負擔。
(3)大數據處理框架下的JVM之間不清楚彼此的運行情況,如果大數據操作需要在各個JVM之間同步,由于JVM獨立進行GC決策,大數據操作的執行就可能被不同的JVM的GC連續打斷,另外由于互相不感知,處于同一物理節點的JVM之間可能內存資源分配不合理,而大數據框架在相關問題上缺少統籌協調。
因此,開發者們需要針對這些問題產生的原因,進行針對性優化。我們下次文章將會繼續討論這個問題。