簡書滌生个唧。
轉(zhuǎn)載請(qǐng)注明原創(chuàng)出處庇忌,謝謝睛挚!
如果讀完覺得有收獲的話,歡迎點(diǎn)贊加關(guān)注溃蔫。
說明
本篇原文作者是 LinkedIn 的 Swapnil Ghike健提,這篇文章講述了 LinkedIn 的 Feed 產(chǎn)品的 GC 優(yōu)化過程,雖然文章寫作于 April 8, 2014伟叛,但其中的很多內(nèi)容和知識(shí)點(diǎn)非常有參考意義私痹。因此,翻譯后獻(xiàn)給各位同學(xué)统刮。
原文鏈接:Garbage Collection Optimization for High-Throughput and Low-Latency Java Applications紊遵。
背景
高性能應(yīng)用構(gòu)成了現(xiàn)代網(wǎng)絡(luò)的支柱。LinkedIn 內(nèi)部有許多高吞吐量服務(wù)來滿足每秒成千上萬的用戶請(qǐng)求网沾。為了獲得最佳的用戶體驗(yàn)癞蚕,以低延遲響應(yīng)這些請(qǐng)求是非常重要的。
例如辉哥,我們的用戶經(jīng)常使用的產(chǎn)品是 Feed —— 它是一個(gè)不斷更新的專業(yè)活動(dòng)和內(nèi)容的列表桦山。Feed 在 LinkedIn 的系統(tǒng)中隨處可見,包括公司頁面醋旦、學(xué)校頁面以及最重要的主頁資訊信息恒水。基礎(chǔ) Feed 數(shù)據(jù)平臺(tái)為我們的經(jīng)濟(jì)圖譜(會(huì)員饲齐、公司钉凌、群組等)中各種實(shí)體的更新建立索引,它必須高吞吐低延遲地實(shí)現(xiàn)相關(guān)的更新捂人。
為了將這些高吞吐量御雕、低延遲類型的 Java 應(yīng)用程序用于生產(chǎn),開發(fā)人員必須確保在應(yīng)用程序開發(fā)周期的每個(gè)階段都保持一致的性能滥搭。確定最佳垃圾收集(Garbage Collection, GC)配置對(duì)于實(shí)現(xiàn)這些指標(biāo)至關(guān)重要酸纲。
這篇博文將通過一系列步驟來明確需求并優(yōu)化 GC,它的目標(biāo)讀者是對(duì)使用系統(tǒng)方法進(jìn)行 GC 優(yōu)化來實(shí)現(xiàn)應(yīng)用的高吞吐低延遲目標(biāo)感興趣的開發(fā)人員瑟匆。在 LinkedIn 構(gòu)建下一代 Feed 數(shù)據(jù)平臺(tái)的過程中闽坡,我們總結(jié)了該方法。這些方法包括但不限于以下幾點(diǎn):并發(fā)標(biāo)記清除(Concurrent Mark Sweep愁溜,CMS) 和 G1 垃圾回收器的 CPU 和內(nèi)存開銷疾嗅、避免長期存活對(duì)象導(dǎo)致的持續(xù) GC、優(yōu)化 GC 線程任務(wù)分配提升性能冕象,以及可預(yù)測 GC 停頓時(shí)間所需的 OS 配置代承。
優(yōu)化 GC 的正確時(shí)機(jī)?
GC 的行為可能會(huì)因代碼優(yōu)化以及工作負(fù)載的變化而變化渐扮。因此次泽,在一個(gè)已實(shí)施性能優(yōu)化的接近完成的代碼庫上進(jìn)行 GC 優(yōu)化非常重要穿仪。而且在端到端的基本原型上進(jìn)行初步分析也很有必要,該原型系統(tǒng)使用存根代碼并模擬了可代表生產(chǎn)環(huán)境的工作負(fù)載意荤。這樣可以獲取該架構(gòu)延遲和吞吐量的真實(shí)邊界啊片,進(jìn)而決定是否進(jìn)行縱向或橫向擴(kuò)展。
在下一代 Feed 數(shù)據(jù)平臺(tái)的原型開發(fā)階段玖像,我們幾乎實(shí)現(xiàn)了所有端到端的功能紫谷,并且模擬了當(dāng)前生產(chǎn)基礎(chǔ)設(shè)施提供的查詢工作負(fù)載。這使我們?cè)诠ぷ髫?fù)載特性上有足夠的多樣性捐寥,可以在足夠長的時(shí)間內(nèi)測量應(yīng)用程序性能和 GC 特征笤昨。
優(yōu)化 GC 的步驟
下面是一些針對(duì)高吞吐量、低延遲需求優(yōu)化 GC 的總體步驟握恳。此外瞒窒,還包括在 Feed 數(shù)據(jù)平臺(tái)原型實(shí)施的具體細(xì)節(jié)。盡管我們還對(duì) G1 垃圾收集器進(jìn)行了試驗(yàn)乡洼,但我們發(fā)現(xiàn) ParNew/CMS 具有最佳的 GC 性能崇裁。
1. 理解 GC 基礎(chǔ)知識(shí)
由于 GC 優(yōu)化需要調(diào)整大量的參數(shù),因此理解 GC 工作機(jī)制非常重要束昵。Oracle 的 Hotspot JVM 內(nèi)存管理白皮書是開始學(xué)習(xí) Hotspot JVM GC 算法非常好的資料拔稳。而了解 G1 垃圾回收器的理論知識(shí),可以參閱該文章锹雏。
2. 仔細(xì)考量 GC 需求
為了降低對(duì)應(yīng)用程序性能的開銷巴比,可以優(yōu)化 GC 的一些特征。像吞吐量和延遲一樣礁遵,這些 GC 特征應(yīng)該在長時(shí)間運(yùn)行的測試中觀察到轻绞,以確保應(yīng)用程序能夠在經(jīng)歷多個(gè) GC 周期中處理流量的變化。
- Stop-the-world 回收器回收垃圾時(shí)會(huì)暫停應(yīng)用線程佣耐。停頓的時(shí)長和頻率不應(yīng)該對(duì)應(yīng)用遵守 SLA 產(chǎn)生不利的影響铲球。
- 并發(fā) GC 算法與應(yīng)用線程競爭 CPU 周期。這個(gè)開銷不應(yīng)該影響應(yīng)用吞吐量晰赞。
- 非壓縮 GC 算法會(huì)引起堆碎片化,進(jìn)而導(dǎo)致的 Full GC 長時(shí)間 Stop-the-world选侨,因此掖鱼,堆碎片應(yīng)保持在最小值。
- 垃圾回收工作需要占用內(nèi)存援制。某些 GC 算法具有比其他算法更高的內(nèi)存占用戏挡。如果應(yīng)用程序需要較大的堆空間,要確保 GC 的內(nèi)存開銷不能太大晨仑。
- 要清楚地了解 GC 日志和常用的 JVM 參數(shù)褐墅,以便輕松地調(diào)整 GC 行為拆檬。因?yàn)?GC 運(yùn)行隨著代碼復(fù)雜性增加或工作負(fù)載特性的改變而發(fā)生變化
我們使用 Linux 操作系統(tǒng)、Hotspot Java7u51妥凳、32GB 堆內(nèi)存竟贯、6GB 新生代(Young Gen)和 -XX:CMSInitiatingOccupancyFraction 值為 70(Old GC 觸發(fā)時(shí)其空間占用率)開始實(shí)驗(yàn)。設(shè)置較大的堆內(nèi)存是用來維持長期存活對(duì)象的對(duì)象緩存逝钥。一旦這個(gè)緩存生效屑那,晉升到 Old Gen 的對(duì)象速度會(huì)顯著下降。
使用最初的 JVM 配置艘款,每 3 秒發(fā)生一次 80ms 的 Young GC 停頓持际,超過 99.9% 的應(yīng)用請(qǐng)求延遲 100ms(999線)。這樣的 GC 效果可能適合于 SLA 對(duì)延遲要求不太嚴(yán)格應(yīng)用哗咆。然而蜘欲,我們的目標(biāo)是盡可能減少應(yīng)用請(qǐng)求的 999 線。GC 優(yōu)化對(duì)于實(shí)現(xiàn)這一目標(biāo)至關(guān)重要晌柬。
3. 理解 GC 指標(biāo)
衡量應(yīng)用當(dāng)前情況始終是優(yōu)化的先決條件姥份。了解 GC 日志的詳細(xì)細(xì)節(jié)(使用以下選項(xiàng)):
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime
可以對(duì)該應(yīng)用的 GC 特征有總體的把握。
在 LinkedIn 的內(nèi)部監(jiān)控 inGraphs 和報(bào)表系統(tǒng) Naarad空繁,生成了各種有用的指標(biāo)可視化圖形殿衰,比如 GC 停頓時(shí)間百分比、一次停頓最大持續(xù)時(shí)間以及長時(shí)間內(nèi) GC 頻率盛泡。除了 Naarad闷祥,有很多開源工具比如 gclogviewer 可以從 GC 日志創(chuàng)建可視化圖形。
在此階段傲诵,可以確定 GC 頻率和暫停持續(xù)時(shí)間是否滿足應(yīng)用程序滿足延遲的要求凯砍。
4. 降低 GC 頻率
在分代 GC 算法中,降低 GC 頻率可以通過:(1)降低對(duì)象分配/晉升率拴竹;(2)增加各代空間的大小悟衩。
在 Hotspot JVM 中,Young GC 停頓時(shí)間取決于一次垃圾回收后存活下來的對(duì)象的數(shù)量栓拜,而不是 Young Gen 自身的大小座泳。增加 Young Gen 大小對(duì)于應(yīng)用性能的影響需要仔細(xì)評(píng)估:
- 如果更多的數(shù)據(jù)存活而且被復(fù)制到 Survivor 區(qū)域,或者每次 GC 更多的數(shù)據(jù)晉升到 Old Gen幕与,增加 Young Gen 大小可能導(dǎo)致更長的 Young GC 停頓挑势。較長的 GC 停頓可能會(huì)導(dǎo)致應(yīng)用程序延遲增加和(或)吞吐量降低。
- 另一方面啦鸣,如果每次垃圾回收后存活對(duì)象數(shù)量不會(huì)大幅增加潮饱,停頓時(shí)間可能不會(huì)延長。在這種情況下诫给,降低 GC 頻率可能會(huì)使整個(gè)應(yīng)用總體延遲降低和(或)吞吐量增加香拉。
對(duì)于大部分為短期存活對(duì)象的應(yīng)用啦扬,僅僅需要控制上述的參數(shù);對(duì)于長期存活對(duì)象的應(yīng)用凫碌,就需要注意扑毡,被晉升的對(duì)象可能很長時(shí)間都不能被 Old GC 周期回收。如果 Old GC 觸發(fā)閾值(Old Gen 占用率百分比)比較低证鸥,應(yīng)用將陷入持續(xù)的 GC 循環(huán)中僚楞。可以通過設(shè)置高的 GC 觸發(fā)閾值可避免這一問題枉层。
由于我們的應(yīng)用在堆中維持了長期存活對(duì)象的較大緩存泉褐,將 Old GC 觸發(fā)閾值設(shè)置為
-XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly
來增加觸發(fā) Old GC 的閾值。我們也試圖增加 Young Gen 大小來減少 Young GC 頻率鸟蜡,但是并沒有采用膜赃,因?yàn)檫@增加了應(yīng)用的 999 線。
5. 縮短 GC 停頓時(shí)間
減少 Young Gen 大小可以縮短 Young GC 停頓時(shí)間揉忘,因?yàn)檫@可能導(dǎo)致被復(fù)制到 Survivor 區(qū)域或者被晉升的數(shù)據(jù)更少跳座。但是,正如前面提到的泣矛,我們要觀察減少 Young Gen 大小和由此導(dǎo)致的 GC 頻率增加對(duì)于整體應(yīng)用吞吐量和延遲的影響疲眷。Young GC 停頓時(shí)間也依賴于 tenuring threshold (晉升閾值)和 Old Gen 大小(如步驟 6 所示)您朽。
在使用 CMS GC 時(shí)狂丝,應(yīng)將因堆碎片或者由堆碎片導(dǎo)致的 Full GC 的停頓時(shí)間降低到最小。通過控制對(duì)象晉升比例和減小 -XX:CMSInitiatingOccupancyFraction 的值使 Old GC 在低閾值時(shí)觸發(fā)哗总。所有選項(xiàng)的細(xì)節(jié)調(diào)整和他們相關(guān)的權(quán)衡几颜,請(qǐng)參考 Web Services 的 Java 垃圾回收 和 Java 垃圾回收精粹。
我們觀察到 Eden 區(qū)域的大部分 Young Gen 被回收讯屈,幾乎沒有 3-8 年齡對(duì)象在 Survivor 空間中死亡蛋哭,所以我們將 tenuring threshold 從 8 降低到 2 (使用選項(xiàng):-XX:MaxTenuringThreshold=2 ),以降低 Young GC 消耗在數(shù)據(jù)復(fù)制上的時(shí)間。
我們還注意到 Young GC 暫停時(shí)間隨著 Old Gen 占用率上升而延長涮母。這意味著來自 Old Gen 的壓力使得對(duì)象晉升花費(fèi)更多的時(shí)間谆趾。為解決這個(gè)問題,將總的堆內(nèi)存大小增加到 40GB叛本,減小 -XX:CMSInitiatingOccupancyFraction 的值到 80沪蓬,更快地開始 Old GC。盡管 -XX:CMSInitiatingOccupancyFraction 的值減小了炮赦,增大堆內(nèi)存可以避免頻繁的 Old GC。在此階段样勃,我們的結(jié)果是 Young GC 暫停 70ms吠勘,應(yīng)用的 999 線在 80ms性芬。
6. 優(yōu)化 GC 工作線程的任務(wù)分配
為了進(jìn)一步降低 Young GC 停頓時(shí)間,我們決定研究 GC 線程綁定任務(wù)的參數(shù)來進(jìn)行優(yōu)化剧防。
-XX:ParGCCardsPerStrideChunk 參數(shù)控制 GC 工作線程的任務(wù)粒度植锉,可以幫助不使用補(bǔ)丁而獲得最佳性能,這個(gè)補(bǔ)丁用來優(yōu)化 Young GC 中的 Card table(卡表掃描時(shí)間)峭拘。有趣的是俊庇,Young GC 時(shí)間隨著 Old Gen 的增加而延長。將這個(gè)選項(xiàng)值設(shè)為 32678鸡挠,Young GC 停頓時(shí)間降低到平均 50ms辉饱。此時(shí)應(yīng)用的 999 線在 60ms。
還有一些的參數(shù)可以將任務(wù)映射到 GC 線程拣展,如果操作系統(tǒng)允許的話彭沼,-XX:+BindGCTaskThreadsToCPUs 參數(shù)可以綁定 GC 線程到個(gè)別的 CPU 核。使用親緣性 -XX:+UseGCTaskAffinity 參數(shù)可以將任務(wù)分配給 GC 工作線程备埃。然而姓惑,我們的應(yīng)用并沒有從這些選項(xiàng)帶來任何好處。實(shí)際上按脚,一些調(diào)查顯示這些選項(xiàng)在 Linux 系統(tǒng)不起作用[1,2]于毙。
7. 了解 GC 的 CPU 和內(nèi)存開銷
并發(fā) GC 通常會(huì)增加 CPU 使用率。雖然我們觀察到 CMS 的默認(rèn)設(shè)置運(yùn)行良好辅搬,但是 G1 收集器的并發(fā) GC 工作會(huì)導(dǎo)致 CPU 使用率的增加唯沮,顯著降低了應(yīng)用程序的吞吐量和延遲。與 CMS 相比伞辛,G1 還增加了內(nèi)存開銷烂翰。對(duì)于不受 CPU 限制的低吞吐量應(yīng)用程序,GC 導(dǎo)致的高 CPU 使用率可能不是一個(gè)緊迫的問題蚤氏。
8. 為 GC 優(yōu)化系統(tǒng)內(nèi)存和 I/O 管理
通常來說甘耿,GC 停頓有兩種特殊情況:
(1)低 user time,高 sys time 和高 real time
(2)低 user time竿滨,低 sys time 和高 real time佳恬。
這意味著基礎(chǔ)的進(jìn)程/OS設(shè)置存在問題。
情況 (1) 可能意味著 JVM 頁面被 Linux 竊扔谟巍毁葱;
情況 (2) 可能意味著 GC 線程被 Linux 用于磁盤刷新,并卡在內(nèi)核中等待 I/O贰剥。
在這些情況下倾剿,如何設(shè)置參數(shù)可以參考該PPT。
另外,為了避免在運(yùn)行時(shí)造成性能損失前痘,我們可以使用 JVM 選項(xiàng) -XX:+AlwaysPreTouch 在應(yīng)用程序啟動(dòng)時(shí)先訪問所有分配給它的內(nèi)存凛捏,讓操作系統(tǒng)把內(nèi)存真正的分配給 JVM。我們還可以將 vm.swappability 設(shè)置為0芹缔,這樣操作系統(tǒng)就不會(huì)交換頁面到 swap(除非絕對(duì)必要)坯癣。
可能你會(huì)使用 mlock 將 JVM 頁固定到內(nèi)存中,這樣操作系統(tǒng)就不會(huì)將它們交換出去最欠。但是示罗,如果系統(tǒng)用盡了所有的內(nèi)存和交換空間,操作系統(tǒng)將終止一個(gè)進(jìn)程來回收內(nèi)存芝硬。通常情況下蚜点,Linux 內(nèi)核會(huì)選擇具有高駐留內(nèi)存占用但運(yùn)行時(shí)間不長的進(jìn)程 (OOM 情況下殺死進(jìn)程的工作流)。
在我們的例子中吵取,這個(gè)進(jìn)程很有可能就是我們的應(yīng)用程序禽额。優(yōu)雅的降級(jí)是服務(wù)優(yōu)秀的屬性之一,不過服務(wù)突然終止的可能性對(duì)于可操作性來說并不好 —— 因此皮官,我們不使用 mlock脯倒,只是通過 vm.swapability 來盡可能避免交換內(nèi)存頁到 swap 的懲罰。
LinkedIn Feed 數(shù)據(jù)平臺(tái)的 GC 優(yōu)化
對(duì)于該 Feed 平臺(tái)原型系統(tǒng)捺氢,我們使用 Hotspot JVM 的兩個(gè) GC 算法優(yōu)化垃圾回收:
- Young GC 使用 ParNew藻丢,Old GC 使用 CMS。
- Young Gen 和 Old Gen 使用 G1摄乒。G1 試圖解決堆大小為 6GB 或更大時(shí)悠反,暫停時(shí)間穩(wěn)定且可預(yù)測在 0.5 秒以下的問題。在我們用 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)用每三秒進(jìn)行一次 40-60ms 的 Young GC 和每小時(shí)一個(gè) CMS GC。JVM 參數(shù)如下:
// 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
使用這些參數(shù)缨硝,對(duì)于成千上萬讀請(qǐng)求的吞吐量,我們應(yīng)用程序的 999 線降低到 60ms罢低。
感謝
參與了原型應(yīng)用程序開發(fā)的同學(xué)有:Ankit Gupta、Elizabeth Bennett、Raghu Hiremagalur匀钧、Roshan Sumbaly、Swapnil Ghike、Tom Chiang 和 Vivek Nelamangala日杈。
另外,感謝 Cuong Tran莉擒、David Hoa 和 Steven Ihde 在系統(tǒng)優(yōu)化方面的幫助。
參考
[1] -XX:+BindGCTaskThreadsToCPUs 參數(shù)似乎在Linux 系統(tǒng)上不起作用涨冀,因?yàn)?hotspot/src/os/linux/vm/os_linux.cpp 的 distribute_processes 方法在 JDK7 或 JDK8 中沒有實(shí)現(xiàn)。
[2] -XX:+UseGCTaskAffinity 參數(shù)在 JDK7 和 JDK8 的所有平臺(tái)似乎都不起作用鹿鳖,因?yàn)槿蝿?wù)的親緣性屬性永遠(yuǎn)被設(shè)置為 sentinel_worker = (uint) -1扁眯。源碼見 hotspot/src/share/vm/gc_implementation/parallelScavenge/{gcTaskManager.cpp翅帜,gcTaskThread.cpp, gcTaskManager.cpp}姻檀。
[3] G1 存在一些內(nèi)存泄露的 bug,可能 Java7u51 沒有修改涝滴。這個(gè) bug 僅在 Java 8 修正了绣版。