天天看點

優酷洋芋資深工程師:GC 調優實戰

對于線上高并發、高吞吐的java web服務來說,長時間的gc暫停(也叫 stop- the-world)會嚴重影響系統吞吐、穩定性和使用者體驗。下文是我們的一個真實線上web系統針對gc調優過程的一個總結。這個系統在調優前,經常會反映有超秒的gc暫停問題,這種gc問題可能會導緻調用方(可能是上層服務調用方、負載均衡層或用戶端)阻塞、逾時、甚至雪崩的情況。我們在系統資源不變的情況下,經過多輪調優,大幅降低了gc的頻率和暫停時間。

1、統計應用資料(峰值tps、平均tps,每秒平均配置設定記憶體大小、每個請求的平均配置設定記憶體大小)

2、統計gc配置設定、回收記憶體的資料(minorgc、fullgc停頓時長,平均多長時間觸發一次gc,每次eden->old的平均晉升大小等)

3、搭建壓力測試環境

4、模拟線上真實使用者行為及相應壓力(記錄使用者通路的accesslog作為壓力測試源,使用的壓力測試軟體為http_load和httperf)

2、改為使用jdk6 update26版本的g1回收。設定最大回收時間為40ms,通過12小時的觀察,發現有大量逾時,感覺g1在jdk6上還不夠成熟,是以決定暫時放棄g1,改為parallelgc;

3、使用parallelgc後,壓力測試發現每次minorgc的耗時降低到40ms左右(以前是200ms以上),但每隔3小時就會有一次fullgc發生,每次fullgc耗時3~4秒;

4、由于fullgc造成的應用暫停在這個應用中是不能接受的。是以放棄parallelgc,改為使用cmsgc。

1、觀察 gcutil 發現permspace接近100%,調大permsize 和 maxpermsize;

2、調整-xms和-xmx相等(如果xms小于xmx,則應用啟動初期老生代相對較小,會導緻cms gc更加頻繁);

3、嘗試優化每次parnew的時長(優化前每次在200ms以上):

增加“-xx:+printtenuringdistribution”參數觀察gc.log,發現對象在survivorspace中的age過多,會導緻大量老對象在新生代無法晉升到老生代。而jvm在parnewgc時分析這些老對象的引用關系是非常耗時的。觀察maxtenuringthresh-old 和 targetsurvivorratio 設定的過大,是以将 maxtenuringthreshold 值調小為15即達到優化目的(優化後每次parnew在20~40ms之間)。并且為了提高survivorspace的使用率,将targetsurvivorratio設定為100(代表強制gc關閉動态調整maxtenuringthreshold,這個參數設定為100會略為激進,增加了survivor區使用率的同時,降低了survivor區應對突發流量的承載能力,不同應用可以看情況調整)。

4、嘗試優化parnew之間的間隔時間(優化前3~4秒一次):觀察gc.log發現每次parnew後大約有不到780mb的存留對象,希望這些對象盡量活在survivorspace裡,并且同時又要保證parnew的時間間隔,是以在xmx和survivorratio不變的情況下,将xmn擴大到7800mb。(因為survivorratio=8,是以整個edenspace需要780*10=7800mb)

5、再次觀察優化後的gc情況(gcutil),發現由于大量對象都在edenspace消亡,是以oldgen的晉升比率極低(0.01%~0.02%),是以可以考慮增大cmsinitiatingoccupancyfraction以提高oldgen的使用率,降低cms gc的觸發頻率(增大到80%)。

6、去掉cmsfullgcsbeforecompaction(去掉後預設為0,表示每次fullgc後都會進行壓縮碎片整理)。因為cms gc導緻的記憶體碎片必須清除,否則oldgen的使用率會降低。

營運一段時間後,發現cmsgc超過一秒的情況非常多(圖中箭頭指向):

優酷洋芋資深工程師:GC 調優實戰

gc日志:

優酷洋芋資深工程師:GC 調優實戰

可以看出,在remark中的rescan階段耗費了1.57秒,并且這個過程是會導緻應用暫停的。問題定位在了rescan階段。

發現在rescan時新生代過大(4313641 k(7188480 k)),是導緻rescan慢的關鍵原因,如果能盡量保持新生代很小的時候就終止preclean階段,就可以控制住在rescan時新生代的大小。檢視jvm參數發現-xx:cmsscheduleremarkedenpenetration的意思是當新生代存活對象占edenspace的比例超過多少時,終止preclean階段并進入remark階段。這個參數的預設值是50%,按照現在的配置,就是7800m*50%=3900m左右,是以更改此參數設定為: -xx:cmsscheduleremarkedenpenetration=1

進行壓力測試,發現remark階段的耗時确實降低了不少,說明優化有效。

運作幾天後觀察gc日志(2011-09-05),發現每隔100000秒的cmsgc的峰值情況确實大大降低了,但是還是偶爾有超過1~2秒的cmsgc情況:

優酷洋芋資深工程師:GC 調優實戰
優酷洋芋資深工程師:GC 調優實戰

發現concurrent-abortable-preclean階段超過了-xx:cmsmaxabortableprecleantime 設定的最大值10秒,是以強制終止了preclean階段而進入remark階段。而這段時間的兩次parnew之間的間隔了17秒之多。希望的是在preclean階段産生一次minorgc,是以将preclean的最大時長調整為30秒: -xx:cmsmaxabortableprecleantime=30000

運作一段時間後,發現居然出現了fullgc,大概在3~5天左右出現一次,以下是fullgc時的日志:

優酷洋芋資深工程師:GC 調優實戰

發現在443310秒有promotion failed出現(新生代晉升到老生代空間不足導緻的fullgc),但是此時的oldgen可以算出還剩1.45g的空間(5324800k-3871691k=1453109k),而根據gclogviewer的統計,每次minorgc後平均新生代晉升到老生代的記憶體大小僅為58k。是以并不是oldgen空間不夠,而是oldgen的連續空間不夠造成的promotion failed。

換句話說,是由于oldgen在距離上次cmsgc後,又産生了大量記憶體碎片,當某個時間點在oldgen中的連續空間沒有一塊足夠58k的話,就會導緻的promotion failed。以下是sun針對這個問題的說明:

考慮如果能夠縮短cmsgc的周期,保證在出現promotion failed之前就進行cmsgc,就可以避免這個問題了。是以考慮将新生代空間縮小(相對來說就增加了老生代的空間),并且将cmsgc觸發比率降低,同時保證survivor空間不變。是以優化參數改動如下:

上面的調優保持系統穩定運作了很長時間後,突然有一台機器出現大量fullgc,觀察gc.log發現是由于持久帶滿造成的:

優酷洋芋資深工程師:GC 調優實戰

應對的方法為加大持久帶,并讓持久帶也使用cmsgc方式回收:

精細化的gc調優是需要耐心和時間的,往往一輪調優要經過gc資料集、分析、調整參數、壓力測試、灰階釋出、最終上線這幾步,上線一段時間後,通過監控發現有新的gc問題,可能又會需要再一輪的調優。而且系統版本的疊代、對象生命周期的變化、線上流量和服務依賴的變化,都可能會對gc頻率和時間有影響。是以對于線上的重點項目,建議每次大版本上線前都能建立一個gc監控、收集和調優的意識,最大程度上規避gc對系統帶來的風險。

<b>優化後整體參數</b>

<b>作者簡介:</b>高嵩,優酷洋芋大資料基礎平台資深工程師,熱衷于高并發、高可用、分布式領域。熱愛開源,為人和善,樂于分享。

<b>  </b><b>                                                  中生代技術分享群微信公衆号</b>

優酷洋芋資深工程師:GC 調優實戰