高吞吐低延遲Java應用的垃圾回收優化

 

題目:高吞吐低延遲Java應用的垃圾回收優化html

 

1.    概述

 

高性能應用構成了現代網絡的支柱。LinkedIn有許多內部高吞吐量服務來知足每秒數千次的用戶請求。要優化用戶體驗,低延遲地響應這些請求很是重要。java

 

好比說,用戶常常用到的一個功能是瞭解動態信息——不斷更新的專業活動和內容的列表。動態信息在LinkedIn隨處可見,包括公司頁面,學校頁面以及最重要的主頁。基礎動態信息數據平臺爲咱們的經濟圖譜(會員,公司,羣組等等)中各類實體的更新創建索引,它必須高吞吐低延遲地實現相關的更新。linux

 

高吞吐低延遲Java應用的垃圾回收優化

圖1 LinkedIn 動態信息git

 

這些高吞吐低延遲的Java應用轉變爲產品,開發人員必須確保應用開發週期的每一個階段一致的性能。肯定優化垃圾回收(Garbage Collection,GC)的設置對達到這些指標很是關鍵。github

 

本文章經過一系列步驟來明確需求並優化GC,目標讀者是爲實現應用的高吞吐低延遲,對使用系統方法優化GC感興趣的開發人員。文章中的方法來自於LinkedIn構建下一代動態信息數據平臺過程。這些方法包括但不侷限於如下幾點:併發標記清除(Concurrent Mark Sweep,CMS)和G1垃圾回收器的CPU和內存開銷,避免長期存活對象引發的持續GC週期,優化GC線程任務分配使性能提高,以及GC停頓時間可預測所需的OS設置。web

 

2.    優化GC的正確時機?

 

GC運行隨着代碼級的優化和工做負載而發生變化。所以在一個已實施性能優化的接近完成的代碼庫上調整GC很是重要。可是在端到端的基本原型上進行初步分析也頗有必要,該原型系統使用存根代碼並模擬了可表明產品環境的工做負載。這樣能夠捕捉該架構延遲和吞吐量的真實邊界,進而決定是否縱向或橫向擴展。算法

 

在下一代動態信息數據平臺的原型階段,幾乎實現了全部端到端的功能,而且模擬了當前產品基礎架構所服務的查詢負載。從中咱們得到了多種用來衡量應用性能的工做負載特徵和足夠長時間運行狀況下的GC特徵。緩存

 

3.    優化GC的步驟

 

下面是爲知足高吞吐,低延遲需求優化GC的整體步驟。也包括在動態信息數據平臺原型實施的具體細節。能夠看到在ParNew/CMS有最好的性能,但咱們也實驗了G1垃圾回收器。性能優化

 

3.1.       理解GC基礎知識

 

理解GC工做機制很是重要,由於須要調整大量的參數。Oracle的Hotspot JVM內存管理白皮書是開始學習Hotspot JVM GC算法很是好的資料。瞭解G1垃圾回收器,請查看該論文網絡

 

3.2.       仔細考量GC需求

 

爲下降應用性能的GC開銷,能夠優化GC的一些特徵。吞吐量、延遲等這些GC特徵應該長時間測試運行觀察,確保特徵數據來自於應用程序的處理對象數量發生變化的多個GC週期。

 

l  Stop-the-world回收器回收垃圾時會暫停應用線程。停頓的時長和頻率不該該對應用遵照SLA產生不利的影響。

 

l  併發GC算法與應用線程競爭CPU週期。這個開銷不該該影響應用吞吐量。

 

l  不壓縮GC算法會引發堆碎片化,致使full GC長時間Stop-the-world停頓。

 

l  垃圾回收工做須要佔用內存。一些GC算法產生更高的內存佔用。若是應用程序須要較大的堆空間,要確保GC的內存開銷不能太大。

 

l  清晰地瞭解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.3.       理解GC指標

 

優化以前要先衡量。瞭解GC日誌的詳細細節(使用這些選項:-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime)能夠對該應用的GC特徵有整體的把握。

 

LinkedIn的內部監控和報表系統,inGraphsNaarad,生成了各類有用的指標可視化圖形,好比GC停頓時間百分比,一次停頓最大持續時間,長時間內GC頻率。除了Naarad,有不少開源工具好比gclogviewer能夠從GC日誌建立可視化圖形。

 

在這個階段,須要肯定GC頻率和停頓時長是否影響應用知足延遲性需求的能力。

 

3.4.       下降GC頻率

 

在分代GC算法中,下降回收頻率能夠經過:(1)下降對象分配/提高率;(2)增長代空間的大小。

 

在Hotspot JVM中,新生代GC停頓時間取決於一次垃圾回收後對象的數量,而不是新生代自身的大小。增長新生代大小對於應用性能的影響須要仔細評估:

 

l  若是更多的數據存活並且被複制到survivor區域,或者每次垃圾回收更多的數據提高到老年代,增長新生代大小可能致使更長的新生代GC停頓。

 

l  另外一方面,若是每次垃圾回收後存活對象數量不會大幅增長,停頓時間可能不會延長。在這種狀況下,減小GC頻率可能使應用整體延遲下降和(或)吞吐量增長。

 

對於大部分爲短時間存活對象的應用,僅僅須要控制前面所說的參數。對於建立長期存活對象的應用,就須要注意,被提高的對象可能很長時間都不能被老年代GC週期回收。若是老年代GC觸發閾值(老年代空間佔用率百分比)比較低,應用將陷入不斷的GC週期。設置高的GC觸發閾值可避免這一問題。

 

因爲咱們的應用在堆中維持了長期存活對象的較大緩存,將老年代GC觸發閾值設置爲-XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly。咱們也試圖增長新生代大小來減小新生代回收頻率,可是並無採用,由於這增長了應用延遲。

 

3.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。

 

3.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]。

 

3.7.       瞭解GC的CPU和內存開銷

 

併發GC一般會增長CPU的使用。咱們觀察了運行良好的CMS默認設置,併發GC和G1垃圾回收器共同工做引發的CPU使用增長顯著下降了應用的吞吐量和延遲。與CMS相比,G1可能佔用了應用更多的內存開銷。對於低吞吐量的非計算密集型應用,GC的高CPU使用率可能不須要擔憂。

 

高吞吐低延遲Java應用的垃圾回收優化


圖2 ParNew/CMS和G1的CPU使用百分數%:相對來講CPU使用率變化明顯的節點使用G1


選項-XX:G1RSetUpdatingPauseTimePercent=20

高吞吐低延遲Java應用的垃圾回收優化

 

 

圖3 ParNew/CMS和G1每秒服務的請求數:吞吐量較低的節點使用G1
選項-XX:G1RSetUpdatingPauseTimePercent=20

 

3.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避免可能的交換懲罰。

 

4.    LinkedIn動態信息數據平臺的GC優化

 

對於該平臺原型系統,咱們使用Hotspot JVM的兩個算法優化垃圾回收:

 

l  新生代垃圾回收使用ParNew,老年代垃圾回收使用CMS。

 

l  新生代和老年代使用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。

 

 

 

 

本教程由尚硅谷教育大數據研究院出品,如需轉載請註明來源,歡迎你們關注尚硅谷公衆號(atguigu)瞭解更多。

相關文章
相關標籤/搜索