[譯]Linkedin對JVM垃圾回收暫停的研究和解決方案

原文:Eliminating Large JVM GC Pauses Caused by Background IO Traffic

譯文由杰微刊兼職譯者張帆翻譯。

在生產環境中,我們屢次看到在JVM(Java虛擬機)中運行的應用程序偶爾會遭遇比較嚴重的暫停,我們稱之為STW(Stop-The-World),這個現象是因為JVM的GC日志記錄進程被后臺IO阻塞(比如系統頁緩存的回寫)鎖定所引起的。在STW暫停期間,JVM暫停了所有的應用線程,應用程序停止響應用戶的請求,因此對于一些等待敏感的用戶操作,這種暫停會造成不可接受的延遲。

我們的調查顯示,暫停是由于JVM GC(垃圾回收)在GC日志記錄過程中的write()系統調用誘導引起的。這類寫日志的操作,即便是采用了異步寫模式(比如緩沖IO或者非阻塞IO),仍然會被操作系統的機制包括頁緩存寫會鎖定相當長的時間。

我們討論了多種方案來緩解該問題。對于延遲敏感的Java應用,我們建議將Java日志文件已到一個獨立的或高性能的磁盤上(如SSD,tmpfs等)。

生產問題

當JVM管理的堆空間進行了垃圾回收,JVM可能會停止,從而導致了應用的STW暫停。鑒于啟動Java實例時配置的選項不同,不同類型的GC和JVM活動會被記錄到GC日志文件中。

盡管一些GC導致的STW暫停比如掃描、標志、整理堆對象是我們眾所周知的,但是我們發現依然有很多大型的STW暫停是有后臺IO阻塞引起。在我們的生產環境中,我們看到了在關鍵任務的Java應用中很多無法解釋的大型STW暫停(>5s)。這樣的暫停不能被應用層面的邏輯和JVM GC活動所解釋。如下面所示,展現了一個大型的超過4s的STW暫停和一些GC信息。垃圾收集器是G1。G1具有一個8GB的堆空間和并行的新生代垃圾收集,一般來講整個垃圾回收過程僅需小于1s即可完成,簡單的GC選項使開銷較小。然而,應用線程竟然暫停了超過4s。GC(如回收堆空間)的工作量不足以解釋4.17s這樣巨大的暫停時間。

``` 2015-12-20T16:09:04.088-0800: 95.743: [GC pause (G1 Evacuation Pause) (young) (initial-mark) 8258M->6294M(10G), 0.1343256 secs] 2015-12-20T16:09:08.257-0800: 99.912: Total time for which application threads were stopped: 4.1692476 seconds

``` 使用G1收集器產生的4,17s的GC STW暫停

另一個例子,下面的GC日志快照展示了一個11.45s的STW暫停。垃圾收集器是CMS(Concurrent Mode Sweep)。"用戶"/"系統"時間可以忽略不計,然而"real"GC時間大于11s。最后一行明確了11.45s的應用暫停時間。

2016-01-14T22:08:28.028+0000: 312052.604: [GC (Allocation Failure) 312064.042: [ParNew Desired survivor size 1998848 bytes, new threshold 15 (max 15) - age 1: 1678056 bytes, 1678056 total : 508096K->3782K(508096K), 0.0142796 secs] 1336653K->835675K(4190400K), 11.4521443 secs] [Times: user=0.18 sys=0.01, real=11.45 secs] 2016-01-14T22:08:39.481+0000: 312064.058: Total time for which application threads were stopped: 11.4566012 seconds

由CMS收集器引起的11.45sGC STW暫停

由于應用是十分延遲敏感的,我們花費了相當大的努力來研究這個問題。最終,我們重現了問題,找到了關鍵誘因,然后提出了幾項解決方案來解決它。

在實驗室環境中重現問題

首先,我們先從在實驗室環境中重現這個無法解釋的大型JVM暫停開始。出于可控性和重復性的考慮,我們設計了一個簡單的工作負載來移除生產應用程序的復雜性。

我們在兩個場景中運行這個工作負載:具備和不具備后臺IO活動。不具備后臺IO的場景用作“基線”,另一個引入后臺IO的場景用于重現問題。

Java工作負載

我們所使用的Java工作負載用向隊列中持續分配10KB的對象。當對象數量達到100,000時,一半的對象會被移除隊列。所以堆中對象的最大值時100,000個對象,約占用原始大小10GB。進程會持續固定的一段時間(如5min)。

Java源碼和生成后臺IO的腳本,都開源在這里

https://github.com/zhenyun/JavaGCworkload。我們考慮的主要性能指標是大JVM GC暫停的次數。

后臺IO

后臺IO是由腳本引入,該腳本負責持續的復制大文件。后臺工作負載大約產生150MB/s的寫負載,足以使單個硬盤跑滿。為了看到產生的IO負載有多嚴重,我們使用"sar -d -p 2"來收集一下統計數據:await(設備發出的IO請求送達的平均時間(單位,毫秒)),tps(每秒傳輸到物理設備上的數量總和)和wrsec-per-s(寫入到設備的扇區數)。平均數值是:await=421ms,tps=305,wrsec-per-s=302K。

系統配置

場景1(無后臺IO負載)

作為基線,本次運行無后臺IO負載。所有JVM GC暫停的時間序列數據所示如下圖所示。沒有發現超過250ms的單一暫停。


場景一中的所有JVM GC暫停(無后臺IO負載)

場景2(有后臺IO負載)

當后臺IO運行時,同樣的Java工作負載在5分鐘的運行期間,能夠看到3.6s的STW暫停,和三個超過0.5s的暫停。


場景二中的所有JVM GC暫停(有后臺IO負載)

調查

為了弄明白是什么系統調用引起的STW暫停,我們使用strace工具來取得由JVM實例產生的系統調用快照。

我們首先驗證了JVM將GC信息記錄到文件使用了異步IO。此外,我們追溯自啟動JVM所發出的所有系統調用。GC日志文件采用了異步模式打開,并且沒有觀察到由fsync()調用。

16:25:35.411993 open("gc.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3 <0.000073> 捕獲的JVM打開GC日志文件產生的open()系統調用

然而,快照顯示由JVM調用的異步write()系統調用有不同尋常的較長的執行時間。檢查系統調用和JVM暫停的時間戳,我們發現他們具備強烈的關聯關系。在下面的兩張圖表,我們展示了2分鐘內的延遲情況。

時間序列相關(JVM STW暫停)。

時間序列相關(write()系統調用延遲)

接著,我們放大焦點,關注發生在13:32:35時最大1.59s暫停。下面展示了相關的GC日志和strace輸出:


GC日志和strace輸出

讓我們來嘗試理解究竟發生了什么:

1、35.04時(第2行),新生代GC啟動,花費0.12s完成

2、新生代GC在35.17完成,JVM進行系統調用write(),嘗試將新生代GC統計輸出到GC日志文件(第4行)

3、write()調用被鎖定1.47s,最終在36.64(第5行)完成,花費1.47s。

4、當write()調用在36.64返回JVM,JVM紀錄了1.59s的STW暫停(0.12+1.47)(第3行)

換句話說,實際的STW暫停由兩部分組成:(1)GC時間(本例中新生代GC所花費時間);(2)GC日記錄日志所花費的時間(本例中的write()時間)。

這些數據表明,GC日志記錄過程算在了JVM STW暫停中,日志記錄的時間被當作STW暫停的一部分。特別的,整個應用的暫停主要包含兩部分:因為JVM GC活動引起的暫停和因為在JVM GC日志記錄時由操作系統阻塞write()系統調用所產生的暫停。下面的圖表展示了兩者的關系。


日志記錄期間JVM和操作系統的相互作用

如果GC日志記錄被操作系統阻塞,阻塞的時間也被計算為STW暫停的一部分。然而新問題是,為何緩沖寫會被阻塞?翻閱大量的資源包括內核源碼,我們意識到緩沖寫可能卡在內核代碼。包括多個原因,如:(1)穩定頁寫操作;(2)日志提交。

穩定頁寫操作:JVM向GC日志文件中的寫操作首先修改了相關的文件緩存頁內容。即使緩存頁稍后會通過操作系統的寫回機制持久化到磁盤文件中,對內存中頁的修改仍然會造成由“穩定頁寫操作”引起的頁面爭用。在“穩定頁寫操作”時,如果頁面正在被操作系統寫回,對這個頁的write()操作必須等待寫回完成。頁面鎖定來確保數據的一致性,避免部分新的一頁保留到磁盤。

日志提交:對于日志文件系統,適當的日志在文件寫入期間生成。寫入新內容到GC日志文件導致新的塊被分配,文件系統需要先將日志數據提交到文件。在日志提交期間,如果操作系統具有其他IO活動,提交操作可能需要稍作等待。如果后臺IO活動比較繁重,則等待時間會明顯加長。值得注意的是,EXT4文件系統具備一項“延遲分配”的特性,來推遲某些特定日志數據的操作系統寫回時間,可以有效緩解這個問題。此外要注意的是,將EXT4的數據模式從默認的“有序”更改為“寫回”并不會真正處理這個問題,因為日志需要在寫擴展調用返回前就被持久化。

后臺IO活動

從特定的JVM垃圾收集的角度來說,在典型的生產環境中,后臺IO活動是不可避免的。有幾類產生IO活動的源頭:(1)系統活動;(2)管理軟件;(3)其他協同應用;(4)同一JVM實例產生的IO。首先,操作系統包含許多機制(比如“/proc”文件系統)將數據寫入底層磁盤。第二,系統級的軟件比如CFEngine也會產生磁盤IO。第三,如果節點需要和其他應用協同共享磁盤,則其他應用會競爭IO。第四,特定的JVM實例也可能產生除GC日志之外的磁盤IO。

解決方案

盡管在當前的HotSpot JVM實現(也存在于其他虛擬機)中,GC日志記錄過程會被后臺IO活動阻塞,我們仍然有很多解決方案來幫助在寫入到GC日志文件時緩解這個問題。

首先,增強JVM可以完全的解決這個問題。尤其,當GC日志記錄活動和引起STW暫停的JVM GC進程分裂,這個問題就會消失不見。舉例來說,JVM可以將GC日志記錄功能,放入一個不同的線程來單獨處理日志文件寫操作,因此,不會造成STW暫停。但是獨立線程來記錄日志會存在JVM崩潰時丟失GC日志信息的風險。暴露給用戶一個JVM標志位允許用戶指定偏好應當時不錯的選擇。

由于后臺IO引起的STW暫停時間的長短依賴于究竟IO負載有多繁重,那么我們可以采用各種方式來減少后臺IO的強烈程度。舉例來說,在同一個節點上取消已分配的其他IO密集型應用程序,減少其它類型的日志記錄,改進日志循環等。

對于延遲敏感的應用來講,比如服務用戶進行交互操作,稍微大的STW暫停(比如大于0.25s)都是不可容忍的。因此,需要實施特別的方案。確保沒有由系統誘導產生的較大STW暫停的最底線就是使GC日志記錄動作避免被操作系統IO活動阻塞。

一種解決方案是將GC日志放入tmpfs(比如,-Xloggc:/tmpfs/gc.log)。因為tmpfs沒有磁盤備份,寫入到tmpfs不會產生磁盤活動,因此也不會被磁盤IO阻塞。但是這種方式存在兩個問題:(1)GC日志文件會在系統宕機時丟失;(2)tmpfs消耗物理內存。一種緩解方案就是隔段時間將日志文件持久化備份,可以減少丟失的數量。

另一種方案就是將GC日志文件放置在SSD(Solid-State Drives,固態硬盤)上,固態硬盤具備更好的IO性能。根據IO負載,SSD可用作GC日志記錄專屬的驅動器,或者與其他IO負載應用共享。然而,SSD的價格需要考慮在成本中。

相較使用SSD,一個更具成本效益的優勝方案是將GC日志文件放入一個專用的硬盤上面。由于該硬盤僅提供GC日志記錄,所以產生的磁盤IO看起來能達到降低暫停,提高JVM性能的目標。事實上,上面我們展示的場景1中,就用了這種配置方式,因此采用這種方式可以保證在GC日志記錄驅動器上沒有其它IO活動存在。

評估將GC日志放置在SSD和tmpfs上

我們采用了專有文件系統的方式,將GC日志放置在SSD和tmpfs驅動器上。我們運行了和場景2中同樣的Java負載和后臺IO負載。

對于SSD和tmpfs,我們觀察到了相似的結果,下面的圖表展示了將GC日志文件放置在SSD磁盤上的結果。我們發現JVM暫停性能比場景1中略差,但是所有的暫停均小于0.25s。結果表明,后臺IO負載并沒有影響應用的性能。

將GC日志記錄到SSD上所有的JVM STW暫停

結論

延遲敏感的Java應用程序需要較小的JVM GC暫停。然而,當磁盤IO繁重時,JVM明顯會被阻塞一段時間。

我們研究了這個問題,并且得出以下結論:

1、JVM GC需要write()系統調用來記錄GC活動;

2、由于后臺磁盤IO,像write()這樣的調用會被阻塞;

3、GC日志記錄過程是JVM暫停的一部分,因此write()調用的時間會計算在JVM STW暫停時間中。

我們提出了一系列的解決方案來緩解這個問題。尤其的,調查結果可以用于提高 JVM 實現,以避免此問題。對于延遲敏感的應用程序,一個即刻生效的解決方案即是將GC日志文件放置在單獨的硬盤或者高性能的磁盤驅動器,如SSD上面,來避免IO競爭。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容

  • 譯者著:其實本文的中心意思非常簡單,沒有耐心的讀者建議直接拉到最后看結論部分,有興趣的讀者可以詳細閱讀一下。 原文...
    重度恐高癥閱讀 7,932評論 6 51
  • 垃圾回收算法具體實現 翻譯原文 => plumbr Java GC handbook 前文參見: Java垃圾回收...
    foxracle閱讀 2,870評論 0 15
  • 原文閱讀 前言 這段時間懈怠了,罪過! 最近看到有同事也開始用上了微信公眾號寫博客了,挺好的~給他們點贊,這博客我...
    碼農戲碼閱讀 6,005評論 2 31
  • Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、并發數低,數據庫頻...
    Rick617閱讀 7,355評論 1 9
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,785評論 18 139