高性能應(yīng)用構(gòu)成了現(xiàn)代網(wǎng)絡(luò)的支柱研侣。LinkedIn有許多內(nèi)部高吞吐量服務(wù)來滿足每秒數(shù)千次的用戶請求喻括。要優(yōu)化用戶體驗(yàn),低延遲地響應(yīng)這些請求非常重要领追。
比如說他膳,用戶經(jīng)常用到的一個(gè)功能是了解動態(tài)信息——不斷更新的專業(yè)活動和內(nèi)容的列表。動態(tài)信息在LinkedIn隨處可見绒窑,包括公司頁面棕孙,學(xué)校頁面以及最重要的主頁』芈郏基礎(chǔ)動態(tài)信息數(shù)據(jù)平臺為我們的經(jīng)濟(jì)圖譜(會員散罕,公司,群組等等)中各種實(shí)體的更新建立索引傀蓉,它必須高吞吐低延遲地實(shí)現(xiàn)相關(guān)的更新欧漱。
圖1 LinkedIn 動態(tài)信息
這些高吞吐低延遲的Java應(yīng)用轉(zhuǎn)變?yōu)楫a(chǎn)品,開發(fā)人員必須確保應(yīng)用開發(fā)周期的每個(gè)階段一致的性能葬燎。確定優(yōu)化垃圾回收(Garbage Collection,GC)的設(shè)置對達(dá)到這些指標(biāo)非常關(guān)鍵误甚。
本文章通過一系列步驟來明確需求并優(yōu)化GC,目標(biāo)讀者是為實(shí)現(xiàn)應(yīng)用的高吞吐低延遲谱净,對使用系統(tǒng)方法優(yōu)化GC感興趣的開發(fā)人員窑邦。文章中的方法來自于LinkedIn構(gòu)建下一代動態(tài)信息數(shù)據(jù)平臺過程。這些方法包括但不局限于以下幾點(diǎn):并發(fā)標(biāo)記清除(Concurrent Mark Sweep,CMS)和G1垃圾回收器的CPU和內(nèi)存開銷壕探,避免長期存活對象引起的持續(xù)GC周期冈钦,優(yōu)化GC線程任務(wù)分配使性能提升,以及GC停頓時(shí)間可預(yù)測所需的OS設(shè)置李请。
優(yōu)化GC的正確時(shí)機(jī)瞧筛?
GC運(yùn)行隨著代碼級的優(yōu)化和工作負(fù)載而發(fā)生變化。因此在一個(gè)已實(shí)施性能優(yōu)化的接近完成的代碼庫上調(diào)整GC非常重要导盅。但是在端到端的基本原型上進(jìn)行初步分析也很有必要较幌,該原型系統(tǒng)使用存根代碼并模擬了可代表產(chǎn)品環(huán)境的工作負(fù)載。這樣可以捕捉該架構(gòu)延遲和吞吐量的真實(shí)邊界白翻,進(jìn)而決定是否縱向或橫向擴(kuò)展乍炉。
在下一代動態(tài)信息數(shù)據(jù)平臺的原型階段,幾乎實(shí)現(xiàn)了所有端到端的功能,并且模擬了當(dāng)前產(chǎn)品基礎(chǔ)架構(gòu)所服務(wù)的查詢負(fù)載岛琼。從中我們獲得了多種用來衡量應(yīng)用性能的工作負(fù)載特征和足夠長時(shí)間運(yùn)行情況下的GC特征底循。
優(yōu)化GC的步驟
下面是為滿足高吞吐,低延遲需求優(yōu)化GC的總體步驟衷恭。也包括在動態(tài)信息數(shù)據(jù)平臺原型實(shí)施的具體細(xì)節(jié)此叠。可以看到在ParNew/CMS有最好的性能随珠,但我們也實(shí)驗(yàn)了G1垃圾回收器灭袁。
1.理解GC基礎(chǔ)知識
理解GC工作機(jī)制非常重要,因?yàn)樾枰{(diào)整大量的參數(shù)窗看。Oracle的Hotspot JVM 內(nèi)存管理白皮書是開始學(xué)習(xí)Hotspot JVM GC算法非常好的資料茸歧。了解G1垃圾回收器,請查看該論文显沈。
2. 仔細(xì)考量GC需求
為降低應(yīng)用性能的GC開銷软瞎,可以優(yōu)化GC的一些特征。吞吐量拉讯、延遲等這些GC特征應(yīng)該長時(shí)間測試運(yùn)行觀察涤浇,確保特征數(shù)據(jù)來自于應(yīng)用程序的處理對象數(shù)量發(fā)生變化的多個(gè)GC周期。
Stop-the-world回收器回收垃圾時(shí)會暫停應(yīng)用線程魔慷。停頓的時(shí)長和頻率不應(yīng)該對應(yīng)用遵守SLA產(chǎn)生不利的影響只锭。
并發(fā)GC算法與應(yīng)用線程競爭CPU周期。這個(gè)開銷不應(yīng)該影響應(yīng)用吞吐量院尔。
不壓縮GC算法會引起堆碎片化蜻展,導(dǎo)致full GC長時(shí)間Stop-the-world停頓。
垃圾回收工作需要占用內(nèi)存邀摆。一些GC算法產(chǎn)生更高的內(nèi)存占用纵顾。如果應(yīng)用程序需要較大的堆空間,要確保GC的內(nèi)存開銷不能太大栋盹。
清晰地了解GC日志和常用的JVM參數(shù)對簡單調(diào)整GC運(yùn)行很有必要施逾。GC運(yùn)行隨著代碼復(fù)雜度增長或者工作特性變化而改變。
我們使用Linux OS的Hotspot?Java7u51例获,32GB堆內(nèi)存音念,6GB新生代(young generation)和-XX:CMSInitiatingOccupancyFraction值為70(老年代GC觸發(fā)時(shí)其空間占用率)開始實(shí)驗(yàn)。設(shè)置較大的堆內(nèi)存用來維持長期存活對象的對象緩存躏敢。一旦這個(gè)緩存被填充,提升到老年代的對象比例顯著下降整葡。
使用初始的GC配置件余,每三秒發(fā)生一次80ms的新生代GC停頓,超過百分之99.9的應(yīng)用延遲100ms。這樣的GC很可能適合于SLA不太嚴(yán)格要求延遲的許多應(yīng)用啼器。然而旬渠,我們的目標(biāo)是盡可能降低百分之99.9應(yīng)用的延遲,為此GC優(yōu)化是必不可少的端壳。
3.理解GC指標(biāo)
優(yōu)化之前要先衡量告丢。了解GC日志的詳細(xì)細(xì)節(jié)(使用這些選項(xiàng):-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime)可以對該應(yīng)用的GC特征有總體的把握。
LinkedIn的內(nèi)部監(jiān)控和報(bào)表系統(tǒng)损谦,inGraphs和Naarad岖免,生成了各種有用的指標(biāo)可視化圖形,比如GC停頓時(shí)間百分比照捡,一次停頓最大持續(xù)時(shí)間颅湘,長時(shí)間內(nèi)GC頻率。除了Naarad栗精,有很多開源工具比如gclogviewer可以從GC日志創(chuàng)建可視化圖形闯参。
在這個(gè)階段,需要確定GC頻率和停頓時(shí)長是否影響應(yīng)用滿足延遲性需求的能力悲立。
4.降低GC頻率
在分代GC算法中知市,降低回收頻率可以通過:(1)降低對象分配/提升率索抓;(2)增加代空間的大小。
在Hotspot JVM中,新生代GC停頓時(shí)間取決于一次垃圾回收后對象的數(shù)量竿奏,而不是新生代自身的大小。增加新生代大小對于應(yīng)用性能的影響需要仔細(xì)評估:
如果更多的數(shù)據(jù)存活而且被復(fù)制到survivor區(qū)域宙暇,或者每次垃圾回收更多的數(shù)據(jù)提升到老年代呛牲,增加新生代大小可能導(dǎo)致更長的新生代GC停頓。
另一方面嚼贡,如果每次垃圾回收后存活對象數(shù)量不會大幅增加熏纯,停頓時(shí)間可能不會延長。在這種情況下粤策,減少GC頻率可能使應(yīng)用總體延遲降低和(或)吞吐量增加樟澜。
對于大部分為短期存活對象的應(yīng)用,僅僅需要控制前面所說的參數(shù)叮盘。對于創(chuàng)建長期存活對象的應(yīng)用秩贰,就需要注意,被提升的對象可能很長時(shí)間都不能被老年代GC周期回收柔吼。如果老年代GC觸發(fā)閾值(老年代空間占用率百分比)比較低毒费,應(yīng)用將陷入不斷的GC周期。設(shè)置高的GC觸發(fā)閾值可避免這一問題愈魏。
由于我們的應(yīng)用在堆中維持了長期存活對象的較大緩存觅玻,將老年代GC觸發(fā)閾值設(shè)置為-XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly想际。我們也試圖增加新生代大小來減少新生代回收頻率,但是并沒有采用溪厘,因?yàn)檫@增加了應(yīng)用延遲胡本。
5.縮短GC停頓時(shí)間
減少新生代大小可以縮短新生代GC停頓時(shí)間,因?yàn)檫@樣被復(fù)制到survivor區(qū)域或者被提升的數(shù)據(jù)更少畸悬。但是侧甫,正如前面提到的,我們要觀察減少新生代大小和由此導(dǎo)致的GC頻率增加對于整體應(yīng)用吞吐量和延遲的影響蹋宦。新生代GC停頓時(shí)間也依賴于tenuring threshold(提升閾值)和空間大小(見第6步)披粟。
使用CMS嘗試最小化堆碎片和與之關(guān)聯(lián)的老年代垃圾回收full GC停頓時(shí)間。通過控制對象提升比例和減小-XX:CMSInitiatingOccupancyFraction的值使老年代GC在低閾值時(shí)觸發(fā)妆档。所有選項(xiàng)的細(xì)節(jié)調(diào)整和他們相關(guān)的權(quán)衡僻爽,請查看Web Services的Java 垃圾回收和Java 垃圾回收精粹。
我們觀察到Eden區(qū)域的大部分新生代被回收贾惦,幾乎沒有對象在survivor區(qū)域死亡胸梆,所以我們將tenuring threshold從8降低到2(使用選項(xiàng):-XX:MaxTenuringThreshold=2),為的是縮短新生代垃圾回收消耗在數(shù)據(jù)復(fù)制上的時(shí)間。
我們也注意到新生代回收停頓時(shí)間隨著老年代空間占用率上升而延長须板。這意味著來自老年代的壓力使得對象提升花費(fèi)更多的時(shí)間碰镜。為解決這個(gè)問題,將總的堆內(nèi)存大小增加到40GB习瑰,減小-XX:CMSInitiatingOccupancyFraction的值到80绪颖,更快地開始老年代回收。盡管-XX:CMSInitiatingOccupancyFraction的值減小了甜奄,增大堆內(nèi)存可以避免不斷的老年代GC柠横。在本階段,我們獲得了70ms新生代回收停頓和百分之99.9延遲80ms课兄。
6.優(yōu)化GC工作線程的任務(wù)分配
進(jìn)一步縮短新生代停頓時(shí)間牍氛,我們決定研究優(yōu)化與GC線程綁定任務(wù)的選項(xiàng)。
-XX:ParGCCardsPerStrideChunk?選項(xiàng)控制GC工作線程的任務(wù)粒度烟阐,可以幫助不使用補(bǔ)丁而獲得最佳性能搬俊,這個(gè)補(bǔ)丁用來優(yōu)化新生代垃圾回收的卡表掃描時(shí)間。有趣的是新生代GC時(shí)間隨著老年代空間的增加而延長蜒茄。將這個(gè)選項(xiàng)值設(shè)為32678唉擂,新生代回收停頓時(shí)間降低到平均50ms。此時(shí)百分之99.9應(yīng)用延遲60ms檀葛。
也有其他選項(xiàng)將任務(wù)映射到GC線程玩祟,如果OS允許的話,-XX:+BindGCTaskThreadsToCPUs選項(xiàng)綁定GC線程到個(gè)別的CPU核屿聋。-XX:+UseGCTaskAffinity使用affinity參數(shù)將任務(wù)分配給GC工作線程卵凑。然而庆聘,我們的應(yīng)用并沒有從這些選項(xiàng)發(fā)現(xiàn)任何益處。實(shí)際上勺卢,一些調(diào)查顯示這些選項(xiàng)在Linux系統(tǒng)不起作用[1,2]。
7.了解GC的CPU和內(nèi)存開銷
并發(fā)GC通常會增加CPU的使用象对。我們觀察了運(yùn)行良好的CMS默認(rèn)設(shè)置黑忱,并發(fā)GC和G1垃圾回收器共同工作引起的CPU使用增加顯著降低了應(yīng)用的吞吐量和延遲。與CMS相比勒魔,G1可能占用了應(yīng)用更多的內(nèi)存開銷甫煞。對于低吞吐量的非計(jì)算密集型應(yīng)用,GC的高CPU使用率可能不需要擔(dān)心冠绢。
圖2 ParNew/CMS和G1的CPU使用百分?jǐn)?shù)%:相對來說CPU使用率變化明顯的節(jié)點(diǎn)使用G1
選項(xiàng)-XX:G1RSetUpdatingPauseTimePercent=20
圖3 ParNew/CMS和G1每秒服務(wù)的請求數(shù):吞吐量較低的節(jié)點(diǎn)使用G1
選項(xiàng)-XX:G1RSetUpdatingPauseTimePercent=20
程序員面試社區(qū):236283328
8.為GC優(yōu)化系統(tǒng)內(nèi)存和I/O管理
通常來說抚吠,GC停頓發(fā)生在(1)低用戶時(shí)間,高系統(tǒng)時(shí)間和高時(shí)鐘時(shí)間和(2)低用戶時(shí)間弟胀,低系統(tǒng)時(shí)間和高時(shí)鐘時(shí)間楷力。這意味著基礎(chǔ)的進(jìn)程/OS設(shè)置存在問題。情況(1)可能說明Linux從JVM偷頁孵户,情況(2)可能說明清除磁盤緩存時(shí)Linux啟動GC線程萧朝,等待I/O時(shí)線程陷入內(nèi)核。在這些情況下如何設(shè)置參數(shù)可以參考該P(yáng)PT夏哭。
為避免運(yùn)行時(shí)性能損失检柬,啟動應(yīng)用時(shí)使用JVM選項(xiàng)-XX:+AlwaysPreTouch訪問和清零頁面。設(shè)置vm.swappiness為零竖配,除非在絕對必要時(shí)何址,OS不會交換頁面。
可能你會使用mlock將JVM頁pin在內(nèi)存中进胯,使OS不換出頁面用爪。但是,如果系統(tǒng)用盡了所有的內(nèi)存和交換空間龄减,OS通過kill進(jìn)程來回收內(nèi)存项钮。通常情況下,Linux內(nèi)核會選擇高駐留內(nèi)存占用但還沒有長時(shí)間運(yùn)行的進(jìn)程(OOM情況下killing進(jìn)程的工作流)希停。對我們而言烁巫,這個(gè)進(jìn)程很有可能就是我們的應(yīng)用程序。一個(gè)服務(wù)具備優(yōu)雅降級(適度退化)的特點(diǎn)會更好宠能,服務(wù)突然故障預(yù)示著不太好的可操作性——因此亚隙,我們沒有使用mlock而是vm.swappiness避免可能的交換懲罰。
LinkedIn動態(tài)信息數(shù)據(jù)平臺的GC優(yōu)化
對于該平臺原型系統(tǒng)违崇,我們使用Hotspot JVM的兩個(gè)算法優(yōu)化垃圾回收:
新生代垃圾回收使用ParNew阿弃,老年代垃圾回收使用CMS诊霹。
新生代和老年代使用G1。G1用來解決堆大小為6GB或者更大時(shí)存在的低于0.5秒穩(wěn)定的渣淳、可預(yù)測停頓時(shí)間的問題脾还。在我們用G1實(shí)驗(yàn)過程中,盡管調(diào)整了各種參數(shù)入愧,但沒有得到像ParNew/CMS一樣的GC性能或停頓時(shí)間的可預(yù)測值鄙漏。我們查詢了使用G1發(fā)生內(nèi)存泄漏相關(guān)的一個(gè)bug[3],但還不能確定根本原因棺蛛。
使用ParNew/CMS怔蚌,應(yīng)用每三秒40-60ms的新生代停頓和每小時(shí)一個(gè)CMS周期。JVM選項(xiàng)如下:
//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
使用這些選項(xiàng)旁赊,對于幾千次讀請求的吞吐量桦踊,應(yīng)用百分之99.9的延遲降低到60ms。