京東JDK在大數據平臺的探索與研究
本文旨在概述京東在JDK方向上的嘗試與探索,以及京東JDK項目背景,基本特性以及未來的工作方向。對于JDK特性的技術討論,實現細節及效果,將在后續系列文章中深入討論。
一、HDFS簡介
HDFS是作為底層的分布式存儲服務而存在的,是Hadoop的分布式文件系統組件。HDFS是高容錯的,被設計成在低成本硬件上部署。HDFS為應用數據提供高吞吐量的訪問,適用于具有大規模數據集的應用程序。HDFS采用了基于Master/Slave主從架構的分布式文件系統, 一個HDFS集群包含的Master節點(NameNode)和多個Slave節點(DataNode)服務器,文件以block的形式存儲在DataNode節點。NameNode主要負責響應客戶端請求,進行文件的打開、關閉、重命名文件和目錄,同時決定block到具體Datanode節點的映射。Datanode在Namenode的指揮下進行block的創建、刪除和復制。
二、JVM對HDFS的作用
由于HDFS采用Java開發,并運行于JVM上,因此如何從JVM角度提升HDFS的能力是主要研究的方向之一。 從JVM角度看,NameNode節點的特點是進程生命周期長,對象創建頻繁,資源利用率高,對于內存的資源要求較高,NameNode的性能是HDFS性能的關鍵。DataNode節點的特點是進程生命周期短,多數進程創建后進行對文件塊的操作后即退出。如何對JVM進行優化,才能使其更加適用于HDFS NameNode和DataNode的工作特點是京東JDK研發的主要方向。
三、京東對通用JDK的嘗試
1. 使用Oracle JDK 1.8的經驗
在京東,曾經嘗試使用Oracle的JDK1.8做為HDFS的JDK解決方案。經過不斷的工作與參數調優,已經使HDFS穩定的運行在OracleJDK1.8環境中。但是,隨著京東業務的不斷增長,對于HDFS的要求也在不斷提高,OracleJDK1.8在以下問題上并不能提供更多的幫助:
- 性能優化:雖然OracleJDK1.8 的JVM中具備很多先進的優化功能,比如tiered compiler, 高效的CMS垃圾收集器等,但其主要針對通用Java程序的性能進行優化,缺少針對分布式工作環境的特定優化。由于無法對oracle JDK1.8的源代碼進行修改,通過參數調整并不能從根本上解決問題。
- 不可控的GC: 雖然OracleJDK1.8提供的相當優秀的CMS垃圾收集器,可以有效的提高GC暫停時間帶來的性能損失,但在實際使用過程中,發現GC停頓時間仍然不能滿足要求,比如YoungGC的時間仍在1秒左右,而OldGC消耗在60秒左右,如果一旦發生FullGC,則經常會導致NameNode暫停時間過長從而導致系統假死,結果往往是災難性的。
- 內存利用率低:對于NameNode節點,能夠使用的物理內存在512GB,而為了避免JVM中老年代GC和Full GC時間過長而導致的災難性后果,NameNode節點只能配置Java堆在200GB左右。通常NameNode節點的機器上只運行NameNode進程和一個輕量級的ZKFC進程,所以物理內存不能得到有效利用。另一方面,NameNode的承載能力受到Java堆大小的制約,導致HDFS的總體承載能力受限。
- JDK版本更新:隨著以上問題的不斷顯現,同時JDK1.8將在2019年停止更新,同時需要嘗試新的JDK以及OpenJDK能否幫助解決問題。
2. 嘗試openJDK11
隨著openJDK的不斷演進,為了緩解上面提到的問題,也嘗試了OpenJDK11, 相對于openJDK1.8,發現openJDK11在以下方面可能具備優勢:
G1GC: open JDK11采用G1作為默認的GC算法,相對于CMS,G1具備以下優點:
- 更小的內存碎片:由于CMS老年代采用Mark-sweep算法,并不是每次做OldGC都進行Compact,所以CMS老年代空間常常會引入碎片問題。而G1采用分塊Copy算法,使得內存碎片問題僅僅在G1的分塊中存在,相對于CMS,其內存的利用率更高,發生FullGC和OOM的可能性更低。
- 可控的GC暫停時間:G1算法典型的特點就是它可以讓用戶提供期望的GC暫停時間,在其內部通過統計預測的方法對下一次即將發生的GC算法進行有效的暫停時間的控制,從而優化GC對于性能的損耗。
- 更豐富的性能分析工具:OpenJDK11引入了Java Frame Recorder(JFS),這是原來oracle JDK1.8商業版才具備的特性,JFR可以在不損耗,或輕微損耗性能的情況下,對Java程序進行sampling,從而幫助分析性能、功能瓶頸和指導優化。
- HDFS更高的負載能力:OpenJDK11由于采用G1作為默認的GC算法,其可以更高效的利用堆內存,同時由于G1算法的設計及優化,其發生FullGC的幾率非常低,并且FullGC的暫停時間也得到了優化,所以相對于oracle JDK1.8的CMS,對于HDFS NameNode來說,其負載能力受到堆大小的限制更加寬松。
雖然OpenJDK11能夠幫助緩解一系列問題,但對于京東大數據來說,僅使用原生的OpenJDK11仍然缺少針對性的優化,目前主要存在以下問題:
- 針對大堆的優化:由于openJDK上G1內部的一些限制,其針對大堆,如360GB的堆的性能并沒有達到理想狀態。
- 針對大堆的工具開發:以JMap為例,當堆內存很大的時候,一次JMap操作便利整個堆內存耗時巨大,我們經常遇到JMap導致假死的情況。
- 針對HDFS的定制化工作:另外,目前仍然希望JDK具備一些可利用的特性幫助我們對HDFS在問題分析,危機處理以及線上分析方面的能力進行增強。
四、京東定制化JDK
經過以上嘗試,結合HDFS業務特點及優化需求。最終決定在OpenJDK11的基礎上,對openjdk進行有針對性的開發和優化,打造京東的定制化JDK。
1. 京東JDK特性介紹
除openJDK11具備的特性外,目前京東JDK主要具備以下能力:
(1) JDK8 兼容性支持 javah:
由于JDK8具備Javah工具能供根據Java的類定義文件生成相應JNI實現所需的C/C++頭文件。在大型項目中,如Hadoop,Yarn都會利用Javah進行JNI頭文件的生成。從JDK10開始,javah工具在JDK中被移除,取而代之的是javac –h功能,但由于javac –h在使用上不同于javah,并且在復雜的項目中,要想用javac –h 代替javah, 必須要修改編譯系統,工作量和難度都比較大。為了在京東內部流暢的進行JDK升級,重寫了javah,使其能成功的利用javac –h進行JNI頭文件的生成。
(2) 擴大G1 region size:
由于openJDK的限制,針對G1GC的region大小只能達到32MB, 并且JVM內部推薦的region個數為2048, 即G1GC最為適用的堆大小在64GB (2048*32MB),而業務量要求NameNode堆至少要在180GB,因此JDJDK確定了優化G1GC對于大堆的支持的目標,以期望提高管理結點的性能。
經過調查研究,針對G1GC的region調整,實際上有兩種方向,一種是保持region大小不變,增大region的個數以適應大堆,比如針對180GB的堆,region大小保持在32MB不變,那么就需要創建5760個region。此方案的好處是保持region大小不變,可以將分配的影響降到最小,但同時由于G1算法需要對每個region之間的引用關系做同步,如果堆數量過多,則同步的開銷增大,從而影響GC的效率。
另一種方案是增加region大小,以保持region個數保持在2048或少量增長,其特點是增大region可能會導致應用程序對象分配的行為改變,但對于region間引用關系的同步影響比較小。
為了能夠達到優化性能的目標,對NameNode做了如下分析:通過采集GCdebug的日志信息,可以看到NameNode的對象分配速率非常頻繁,old space allocation rate 達到1MB/s,即有大量的object被頻繁提升到老年代,同時存在大量的TLAB refile以及出現TLAB fill的頻率在每分鐘3萬次左右,TLAB fill 即allocation進入slow path,需要進行TLAB的替換或者在非TLAB中分配。因此對象的分配性能是NameNode 性能的關鍵點之一。
結合以上分析,對JDK的region大小上限進行了優化,同時針對region大小,對G1進行了相應的修改。以下為優化后的實驗得到的數據。
可以看到,TLAB fill次數從每分鐘30000降到了20000,即對象分配在slow path的機率減少了33%。
(3) 針對多線程下鎖的性能優化:
在JDJDK版本升級后, 運維與研發人員在大數據平臺運行過程中,發現G1在運行過程中會出現2s左右的超長YoungGC,而相同規模的YGC大部分只有200ms左右. 如下圖中綠線所示。
經過分析, G1出現2s GC的主要原因在于偏向鎖功能的revoke過于頻繁。利用JFR可以看到如下現象。
綜合以上分析, 在管理節點采用-XX:-UseBiasedLocking后, 2s的GC 消失, 上圖藍色線條所示。
(4) Java堆的動態拓展:
Java程序在啟動時要求程序員為JVM預設堆內存上限,即指定-Xmx的大小(或采用默認JVM參數)。但在實際使用過程中,很難清晰的計算出究竟應該采用多大的Java堆上限,尤其是對于線上系統中的管理進程,很有可能在發生大量的業務請求時出現OOM(Out-Of-Memory)異常而導致管理進程退出,出現災難性后果。另一方面,考慮到系統資源占用,Java程序往往要求JVM不要占用大量的系統內存,即使-Xmx的值小于RAM的大小,所以在程序運行時,經常會出現Java進程因為OOM退出,而系統RAM卻還有很多剩余可以利用。
為了緩解OOM的問題,京東JDK研發了基于G1GC的動態拓展堆大小的功能。 該功能在JVM堆內存使用率正常的情況下,維持java堆在-Xmx之下,而當JVM發現當前進程Java堆被大量占用時,將發出警報,從而運維人員可以根據當前業務情況即系統RAM使用情況,動態的打開Java堆拓展功能,JVM將Java堆進行一定比例的拓展,以保證JVM順利度過業務繁忙的時段。 當業務量降低,并且heap使用率低于一定閾值時,JVM將利用G1GC回收拓展的堆區域,從而保證在正常情況下JVM進程不會給系統內存造成額外的壓力。
(5) 定期、定時觸發GC:
經過調研,發現京東的業務呈現明顯的時間周期性,比如某個集群在某一時段基本處于空閑狀態。而在繁忙狀態時,堆內存以及CPU資源都集中于業務的處理,如果此時發生OldGC或者FullGC,或者YoungGC發生過于頻繁,都會導致系統的業務處理能力下降。
為了降低GC對于業務處理能力的影響,京東JDK基于G1GC開發了周期性GC的功能。運維人員可以在每天系統不繁忙的時間段定時觸發多次YoungGC以及必要的MixedGC/FullGC來清里Java堆中的垃圾,從而降低高峰時段GC觸發的頻率及時間。
(6) JVM及時歸還未使用的內存(Uncommitted Memory)給系統:
JDK12特性,京東JDK目前已經支持。此功能主要為節省物理內存空間。JDK11版本中的G1并不會及時的將空的region交還給OS,只有在FullGC或Old GC的concurrent 階段才會交還已經回收的region給OS。但由于G1的設計目標就是避免FullGC以及盡量少的觸發OldGC,所以實際運行過程中,G1 堆占用的物理內存會遲遲不能釋放給系統,導致JVM進程占用內存遠高于實際使用量。在多進程多任務環境中,會整體導致系統內存資源不能有效分配及使用,同時提高內存硬件的需求量,增加企業的成本投入。
京東JDK在JDK11的基礎上,從JDK12引入了JEP346特性 --“及時回收未使用的Uncommitted Memory給系統“這個特性,其在JVM內部引入了監測機制,當發現系統空閑以及JVMGC觸發不頻繁時,JVM會自動觸發concurrentGC 或FullGC來回收uncommitted region給系統。
(7) 可撤銷的G1 Mixed GC以保證GC停頓時間:
JDK12特性,有效減少及控制G1停頓時間。G1GC的主要設計目標是保證G1的停頓時間在可控的范圍內,用戶可以通過-XX:MaxGCPauseMills參數來指定G1的停頓時間上限,G1GC會盡量嘗試保證每次GC的時間不會超過-XX:MaxGCPauseMills。在JVM內部,G1GC在Concurrent 階段會根據停頓時間上限來選擇需要回收的集合(Collect Set),然后在暫停階段回收這些集合中的對象。
在JDK11版本中,Collection Set一旦確定就無法改變,但由于Collection Set是JVM根據歷史GC信息推斷出的,因此如果推斷與真實情況的誤差過大,會導致MixGC(oldGC)的暫停時間過長,遠超過-XX:MaxGCPauseMills設定的目標。
京東JDK從JDK12引入了JEP344特性—Abortable Mixed Collections for G1,該特性可以將Collection Set分解為“必須回收”和“可選擇回收”的兩部分,在發生MixedGC時,GC在回收完“必須回收”的部分后,會根據目標暫停時間的剩余量循環的從“可選擇回收”部分中選取回收集合進行回收,以保證GC整體暫停時間可控。
(8) 默認的類型信息共享文件(Class Data Sharing - CDS Archive):
Class Data Sharing (CDS)有助于加快Java程序啟動時間,同時允許多JVM實例復用SharedArchive以減少memory footprint.
JDK10對CDS進一步拓展,SharedArchive中保存應用程序數據:Application Class-data sharing (參見JEP 310)
對于CDS,JEP中的介紹如下:
- We can save about 340MB of RAM for a Java EE app server that includes 6 JVM processes consuming a total of 13GB of RAM (~2GB of that is for class meta data).
- We can improve the startup time of the JEdit benchmark by 20-30%.
- We can reduce the RAM usage of the embedded Felix benchmark by 18% across 4 JVM processes.
京東JDK引入了新的JDK12中關于CDS的新特性 - Default CDS Archives。該功能在編譯階段生成默認的Archive,并且無需用戶指定JVM選項-Xshare:auto即可享受到CDS帶來的優點。
(9) 并行的高效JMap Java堆分析工具:
JMap作為Java開發人員常用工具,一般在調查OOM,查看堆對象分布時都能發揮重要作用。但是在日常工作中,發現對于大堆,例如堆內存配置為-Xmx200g時,在線上系統運行JMap histo時間非常長,并且影響整個JVM進程的響應速度,一旦JVM進程被KILL,運行中JMap histo也無法提供有效信息。 經過調研,JMap 工具在掃描Java堆時是單線程工作,并且只有在整個堆掃描完成時才會統計信息并輸出。
針對JMap的問題,京東JDK團隊對JMap進行了拓展,實現了其并行,增量式對掃描方案。對JMap histo在大堆上的掃描并行化,同時在運行中統計中間結果。使得JMap在200GB堆掃描性能提升2倍,同時能夠使JMap在運行過程中不斷輸出中間結果,這樣即使JVM進程退出,JMap仍能提供有效的信息用于分析內存使用情況。
2. 京東JDK優化效果
經過一系列的工作,目前京東JDK已經順利應用于京東大數據平臺HDFS的NameNode節點上,其對于管理結點優化達到50%, 見下圖:
另一方面,JDJDK對于管理結點文件數承載能力從4億上升到10億,承載能力提升1.5倍。緩解了業務方的需求,節省了人力。
針對G1GC 也做了相關優化, 優化后的G1GC 對比之前JDK8的CMS的YoungGC暫停時間如下圖:
GC發生的次數對于如下:
在加/解鎖及線程同步方面,京東JDK團隊也進行了深入的研究及優化,除了上文提到的偏向鎖以外,還利用JVM 的instrumentation等工具,對鎖相關的bytecode進行線上優化,針對不同的HDFS訪問,優化效果如下:
Mkdir:
Delete:
Getfileinfo:
Rename:
五、京東JDK的發展方向
在未來,京東JDK團隊將更加注重于降本增效方面的工作,我們計劃進行更多的嘗試及創新,例如:
- 用于特定使用場景的,獨立的heap區域
- 半自動式GC
- 基于大數據應用場景的GC算法開發及優化
- 基于Graal的AOT功能的開發及優化
同時,京東JDK團隊也將積極參與openJDK社區的開發及研究工作,盡可能將京東JDK的特性貢獻到社區,讓更多人能夠使用到。
作者簡介:臧琳,京東JVM專家,主要負責京東JDK針對京東大數據業務的定制化開發及優化工作。專注于JVM中內存管理,runtime運行時以及JIT編譯器的性能分析及優化等領域。
【本文來自51CTO專欄作者張開濤的微信公眾號(開濤的博客),公眾號id: kaitao-1234567】