高吞吐低延遲Java應用的垃圾回收優化
高效能應用構成了現代網路的支柱。LinkedIn有許多內部高吞吐量服務來滿足每秒數千次的使用者請求。要優化使用者體驗,低延遲地響應這些請求非常重要。
比如說,使用者經常用到的一個功能是瞭解動態資訊——不斷更新的專業活動和內容的列表。動態資訊在LinkedIn隨處可見,包括公司頁面,學校頁面以及最重要的主頁。基礎動態資訊資料平臺為我們的經濟圖譜(會員,公司,群組等等)中各種實體的更新建立索引,它必須高吞吐低延遲地實現相關的更新。
圖1 LinkedIn 動態資訊
這些高吞吐低延遲的Java應用轉變為產品,開發人員必須確保應用開發週期的每個階段一致的效能。確定優化垃圾回收(Garbage Collection,GC)的設定對達到這些指標非常關鍵。
本文章通過一系列步驟來明確需求並優化GC,目標讀者是為實現應用的高吞吐低延遲,對使用系統方法優化GC感興趣的開發人員。文章中的方法來自於LinkedIn構建下一代動態資訊資料平臺過程。這些方法包括但不侷限於以下幾點:併發標記清除(Concurrent Mark Sweep,CMS)和G1垃圾回收器的CPU和記憶體開銷,避免長期存活物件引起的持續GC週期,優化GC執行緒任務分配使效能提升,以及GC停頓時間可預測所需的OS設定。
優化GC的正確時機?
GC執行隨著程式碼級的優化和工作負載而發生變化。因此在一個已實施效能優化的接近完成的程式碼庫上調整GC非常重要。但是在端到端的基本原型上進行初步分析也很有必要,該原型系統使用存根程式碼並模擬了可代表產品環境的工作負載。這樣可以捕捉該架構延遲和吞吐量的真實邊界,進而決定是否縱向或橫向擴充套件。
在下一代動態資訊資料平臺的原型階段,幾乎實現了所有端到端的功能,並且模擬了當前產品基礎架構所服務的查詢負載。從中我們獲得了多種用來衡量應用效能的工作負載特徵和足夠長時間執行情況下的GC特徵。
優化GC的步驟
下面是為滿足高吞吐,低延遲需求優化GC的總體步驟。也包括在動態資訊資料平臺原型實施的具體細節。可以看到在ParNew/CMS有最好的效能,但我們也實驗了G1垃圾回收器。
1.理解GC基礎知識
理解GC工作機制非常重要,因為需要調整大量的引數。Oracle的Hotspot JVM 記憶體管理白皮書是開始學習Hotspot JVM GC演算法非常好的資料。瞭解G1垃圾回收器,請檢視該論文。
2. 仔細考量GC需求
為降低應用效能的GC開銷,可以優化GC的一些特徵。吞吐量、延遲等這些GC特徵應該長時間測試執行觀察,確保特徵資料來自於應用程式的處理物件數量發生變化的多個GC週期。
Stop-the-world回收器回收垃圾時會暫停應用執行緒。停頓的時長和頻率不應該對應用遵守SLA產生不利的影響。
併發GC演算法與應用執行緒競爭CPU週期。這個開銷不應該影響應用吞吐量。
不壓縮GC演算法會引起堆碎片化,導致full GC長時間Stop-the-world停頓。
垃圾回收工作需要佔用記憶體。一些GC演算法產生更高的記憶體佔用。如果應用程式需要較大的堆空間,要確保GC的記憶體開銷不能太大。
清晰地瞭解GC日誌和常用的JVM引數對簡單調整GC執行很有必要。GC執行隨著程式碼複雜度增長或者工作特性變化而改變。
我們使用Linux OS的Hotspot Java7u51,32GB堆記憶體,6GB新生代(young generation)和-XX:CMSInitiatingOccupancyFraction值為70(老年代GC觸發時其空間佔用率)開始實驗。設定較大的堆記憶體用來維持長期存活物件的物件快取。一旦這個快取被填充,提升到老年代的物件比例顯著下降。
使用初始的GC配置,每三秒發生一次80ms的新生代GC停頓,超過百分之99.9的應用延遲100ms。這樣的GC很可能適合於SLA不太嚴格要求延遲的許多應用。然而,我們的目標是儘可能降低百分之99.9應用的延遲,為此GC優化是必不可少的。
3.理解GC指標
優化之前要先衡量。瞭解GC日誌的詳細細節(使用這些選項:-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime)可以對該應用的GC特徵有總體的把握。
LinkedIn的內部監控和報表系統,inGraphs和Naarad,生成了各種有用的指標視覺化圖形,比如GC停頓時間百分比,一次停頓最大持續時間,長時間內GC頻率。除了Naarad,有很多開源工具比如gclogviewer可以從GC日誌建立視覺化圖形。
在這個階段,需要確定GC頻率和停頓時長是否影響應用滿足延遲性需求的能力。
4.降低GC頻率
在分代GC演算法中,降低迴收頻率可以通過:(1)降低物件分配/提升率;(2)增加代空間的大小。
在Hotspot JVM中,新生代GC停頓時間取決於一次垃圾回收後物件的數量,而不是新生代自身的大小。增加新生代大小對於應用效能的影響需要仔細評估:
如果更多的資料存活而且被複制到survivor區域,或者每次垃圾回收更多的資料提升到老年代,增加新生代大小可能導致更長的新生代GC停頓。
另一方面,如果每次垃圾回收後存活物件數量不會大幅增加,停頓時間可能不會延長。在這種情況下,減少GC頻率可能使應用總體延遲降低和(或)吞吐量增加。
對於大部分為短期存活物件的應用,僅僅需要控制前面所說的引數。對於建立長期存活物件的應用,就需要注意,被提升的物件可能很長時間都不能被老年代GC週期回收。如果老年代GC觸發閾值(老年代空間佔用率百分比)比較低,應用將陷入不斷的GC週期。設定高的GC觸發閾值可避免這一問題。
由於我們的應用在堆中維持了長期存活物件的較大快取,將老年代GC觸發閾值設定為-XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly。我們也試圖增加新生代大小來減少新生代回收頻率,但是並沒有採用,因為這增加了應用延遲。
5.縮短GC停頓時間
減少新生代大小可以縮短新生代GC停頓時間,因為這樣被複制到survivor區域或者被提升的資料更少。但是,正如前面提到的,我們要觀察減少新生代大小和由此導致的GC頻率增加對於整體應用吞吐量和延遲的影響。新生代GC停頓時間也依賴於tenuring threshold(提升閾值)和空間大小(見第6步)。
使用CMS嘗試最小化堆碎片和與之關聯的老年代垃圾回收full GC停頓時間。通過控制物件提升比例和減小-XX:CMSInitiatingOccupancyFraction的值使老年代GC在低閾值時觸發。所有選項的細節調整和他們相關的權衡,請檢視Web Services的Java 垃圾回收和Java 垃圾回收精粹。
我們觀察到Eden區域的大部分新生代被回收,幾乎沒有物件在survivor區域死亡,所以我們將tenuring threshold從8降低到2(使用選項:-XX:MaxTenuringThreshold=2),為的是縮短新生代垃圾回收消耗在資料複製上的時間。
我們也注意到新生代回收停頓時間隨著老年代空間佔用率上升而延長。這意味著來自老年代的壓力使得物件提升花費更多的時間。為解決這個問題,將總的堆記憶體大小增加到40GB,減小-XX:CMSInitiatingOccupancyFraction的值到80,更快地開始老年代回收。儘管-XX:CMSInitiatingOccupancyFraction的值減小了,增大堆記憶體可以避免不斷的老年代GC。在本階段,我們獲得了70ms新生代回收停頓和百分之99.9延遲80ms。
6.優化GC工作執行緒的任務分配
進一步縮短新生代停頓時間,我們決定研究優化與GC執行緒繫結任務的選項。
-XX:ParGCCardsPerStrideChunk 選項控制GC工作執行緒的任務粒度,可以幫助不使用補丁而獲得最佳效能,這個補丁用來優化新生代垃圾回收的卡表掃描時間。有趣的是新生代GC時間隨著老年代空間的增加而延長。將這個選項值設為32678,新生代回收停頓時間降低到平均50ms。此時百分之99.9應用延遲60ms。
也有其他選項將任務對映到GC執行緒,如果OS允許的話,-XX:+BindGCTaskThreadsToCPUs選項繫結GC執行緒到個別的CPU核。-XX:+UseGCTaskAffinity使用affinity引數將任務分配給GC工作執行緒。然而,我們的應用並沒有從這些選項發現任何益處。實際上,一些調查顯示這些選項在Linux系統不起作用[1,2]。
7.瞭解GC的CPU和記憶體開銷
併發GC通常會增加CPU的使用。我們觀察了執行良好的CMS預設設定,併發GC和G1垃圾回收器共同工作引起的CPU使用增加顯著降低了應用的吞吐量和延遲。與CMS相比,G1可能佔用了應用更多的記憶體開銷。對於低吞吐量的非計算密集型應用,GC的高CPU使用率可能不需要擔心。
圖2 ParNew/CMS和G1的CPU使用百分數%:相對來說CPU使用率變化明顯的節點使用G1
選項-XX:G1RSetUpdatingPauseTimePercent=20
圖3 ParNew/CMS和G1每秒服務的請求數:吞吐量較低的節點使用G1
選項-XX:G1RSetUpdatingPauseTimePercent=20
程式設計師面試社群:236283328
8.為GC優化系統記憶體和I/O管理
通常來說,GC停頓發生在(1)低使用者時間,高系統時間和高時鐘時間和(2)低使用者時間,低系統時間和高時鐘時間。這意味著基礎的程式/OS設定存在問題。情況(1)可能說明Linux從JVM偷頁,情況(2)可能說明清除磁碟快取時Linux啟動GC執行緒,等待I/O時執行緒陷入核心。在這些情況下如何設定引數可以參考該PPT。
為避免執行時效能損失,啟動應用時使用JVM選項-XX:+AlwaysPreTouch訪問和清零頁面。設定vm.swappiness為零,除非在絕對必要時,OS不會交換頁面。
可能你會使用mlock將JVM頁pin在記憶體中,使OS不換出頁面。但是,如果系統用盡了所有的記憶體和交換空間,OS通過kill程式來回收記憶體。通常情況下,Linux核心會選擇高駐留記憶體佔用但還沒有長時間執行的程式(OOM情況下killing程式的工作流)。對我們而言,這個程式很有可能就是我們的應用程式。一個服務具備優雅降級(適度退化)的特點會更好,服務突然故障預示著不太好的可操作性——因此,我們沒有使用mlock而是vm.swappiness避免可能的交換懲罰。
LinkedIn動態資訊資料平臺的GC優化
對於該平臺原型系統,我們使用Hotspot JVM的兩個演算法優化垃圾回收:
新生代垃圾回收使用ParNew,老年代垃圾回收使用CMS。
新生代和老年代使用G1。G1用來解決堆大小為6GB或者更大時存在的低於0.5秒穩定的、可預測停頓時間的問題。在我們用G1實驗過程中,儘管調整了各種引數,但沒有得到像ParNew/CMS一樣的GC效能或停頓時間的可預測值。我們查詢了使用G1發生記憶體洩漏相關的一個bug[3],但還不能確定根本原因。
使用ParNew/CMS,應用每三秒40-60ms的新生代停頓和每小時一個CMS週期。JVM選項如下:
//JVM sizing options
-server -Xms40g -Xmx40g -XX:MaxDirectMemorySize=4096m -XX:PermSize=256m -XX:MaxPermSize=256m
//Young generation options
-XX:NewSize=6g -XX:MaxNewSize=6g -XX:+UseParNewGC -XX:MaxTenuringThreshold=2 -XX:SurvivorRatio=8 -XX:+UnlockDiagnosticVMOptions -XX:ParGCCardsPerStrideChunk=32768
//Old generation options
-XX:+UseConcMarkSweepGC -XX:CMSParallelRemarkEnabled -XX:+ParallelRefProcEnabled -XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly
//Other options
-XX:+AlwaysPreTouch -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:-OmitStackTraceInFastThrow
使用這些選項,對於幾千次讀請求的吞吐量,應用百分之99.9的延遲降低到60ms。
相關文章
- Timestone:Netflix 的高吞吐量、低延遲優先佇列系統佇列
- async-rdma:編寫高吞吐量、低延遲網路應用的Rust庫Rust
- JVM 低延遲垃圾收集器 Shenandoah 和 ZGCJVMNaNGC
- OGG複製程式延遲高,優化方法一(使用索引)優化索引
- Restate:支援JavaScript/Java的Rust低延遲持久工作流RESTJavaScriptRust
- 低延遲音視訊傳輸技術在直播領域的應用
- Spark Streaming高吞吐、高可靠的一些優化Spark優化
- 從雙十一的物流大戰,看全球通訊網路的低延遲優化優化
- Java gc(垃圾回收機制)小結,以及Android優化建議JavaGCAndroid優化
- Python中排隊理論:吞吐量與延遲Python
- Netflix使用ZGC實現低延遲GC
- 效能優化|講的最清楚的垃圾回收演算法優化演算法
- java垃圾回收機制Java
- Java 垃圾回收機制Java
- OGG複製程式延遲高,優化方法二(存在索引),SQL選擇不好的索引優化索引SQL
- 低延遲系統請選擇Java而不是C++ - stackoverflowJavaC++
- 打造低延遲互動音訊: Oboe音訊
- 【JVM系列】低延遲迴收器 ZGCJVMGC
- 垃圾回收(一)【垃圾回收的基礎】
- JAVA垃圾回收機制和Python垃圾回收對比與分析JavaPython
- 網路應用優化——時延與頻寬優化
- 如何降低90%Java垃圾回收時間?以阿里HBase的GC優化實踐為例Java阿里GC優化
- java垃圾回收機制整理Java
- 主從延遲調優思路
- Java的垃圾回收(Garbage Collection)機制Java
- 垃圾回收(三)【垃圾回收通知】
- Android優化(三)_延遲電池續航時間Android優化
- 啟動優化之動態庫延遲載入優化
- 深入理解JVM(③)低延遲的Shenandoah收集器JVMNaN
- 延遲退休來了,如何應對“老齡化”的自己?
- 伺服器延遲高的幾個原因伺服器
- Nginx多程式高併發、低時延、高可靠機制在滴滴快取代理中的應用Nginx快取
- 前端效能優化——延遲載入和非同步載入前端優化非同步
- Java11改進的垃圾回收器Java
- Java 垃圾回收01(基本過程)Java
- 多層代理下解決鏈路低延遲的技巧
- 得物直播低延遲探索 | 得物技術
- 直播分發選低延遲 RTC 還是 CDN?