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