如何降低90%Java垃圾回收時間?以阿里HBase的GC優化實踐為例

導讀:GC一直是Java應用中討論的一個熱門話題,尤其在像HBase這樣的大型在線存儲系統中,大堆下(百GB)的GC停頓延遲產生的在線實時影響,成為內核和應用開發者的一大痛點。

過去的一年里,我們準備在Ali-HBase上突破這個被普遍認知的痛點,為此進行了深度分析及全面創新的工作,獲得了一些比較好的效果。以螞蟻風控場景為例,HBase的線上young GC時間從120ms減少到15ms,結合阿里巴巴JDK團隊提供的利器——AliGC,進一步在實驗室壓測環境做到了5ms。本文主要介紹我們過去在這方面的一些工作和技術思想。

背景

JVM的GC機制對開發者屏蔽了內存管理的細節,提高了開發效率。說起GC,很多人的第一反應可能是JVM長時間停頓或者FGC導致進程卡死不可服務的情況。但就HBase這樣的大數據存儲服務而言,JVM帶來的GC挑戰相當復雜和艱難。原因有三:

1、內存規模巨大。線上HBase進程多數為96G大堆,今年新機型已經上線部分160G以上的堆配置

2、對象狀態復雜。HBase服務器內部會維護大量的讀寫cache,達到數十GB的規模。HBase以表格的形式提供有序的服務數據,數據以一定的結構組織起來,這些數據結構產生了過億級別的對象和引用

3、young?GC頻率高。訪問壓力越大,young區的內存消耗越快,部分繁忙的集群可以達到每秒1~2次youngGC, 大的young區可以減少GC頻率,但是會帶來更大的young?GC停頓,損害業務的實時性需求。

思路

1.??HBase作為一個存儲系統,使用了大量的內存作為寫buffer和讀cache,比如96G的大堆(4G young + 92G old)下,寫buffer+讀cache會占用70%以上的內存(約70G),本身堆內的內存水位會控制在85%,而剩余的占用內存就只有在10G以內了。所以,如果我們能在應用層面自管理好這70G+的內存,那么對于JVM而言,百G大堆的GC壓力就會等價于10G小堆的GC壓力,并且未來面對更大的堆也不會惡化膨脹。 在這個解決思路下,我們線上的young?GC時間獲得了從120ms到15ms的優化效果。

2.??在一個高吞吐的數據密集型服務系統中,大量的臨時對象被頻繁創建與回收,如何能夠針對性管理這些臨時對象的分配與回收,AliJDK團隊研發了一種新的基于租戶的GC算法—AliGC。集團HBase基于這個新的AliGC算法進行改造,我們在實驗室中壓測的young?GC時間從15ms減少到5ms,這是一個未曾期望的極致效果。

下面將逐一介紹Ali-HBase版本GC優化所使用的關鍵技術。

消滅一億個對象:更快更省的CCSMap

目前HBase使用的存儲模型是LSMTree模型,寫入的數據會在內存中暫存到一定規模后再dump到磁盤上形成文件。

下面我們將其簡稱為寫緩存。寫緩存是可查詢的,這就要求數據在內存中有序。為了提高并發讀寫效率,并達成數據有序且支持seek&scan的基本要求,SkipList是使用得比較廣泛的數據結構。


我們以JDK自帶的ConcurrentSkipListMap為例子進行分析,它有下面三個問題:

1.??內部對象繁多。每存儲一個元素,平均需要4個對象(index+node+key+value,平均層高為1)

2.??新插入的對象在young區,老對象在old區。當不斷插入元素時,內部的引用關系會頻繁發生變化,無論是ParNew算法的CardTable標記,還是G1算法的RSet標記,都有可能觸發old區掃描。

3.??業務寫入的KeyValue元素并不是規整長度的,當它晉升到old區時,可能產生大量的內存碎片。

問題1使得young區GC的對象掃描成本很高,young?GC時晉升對象更多。問題2使得young?GC時需要掃描的old區域會擴大。問題3使得內存碎片化導致的FGC概率升高。當寫入的元素較小時,問題會變得更加嚴重。我們曾對線上的RegionServer進程進行統計,活躍Objects有1億2千萬之多!

分析完當前young?GC的最大敵人后,一個大膽的想法就產生了,既然寫緩存的分配,訪問,銷毀,回收都是由我們來管理的,如果讓JVM“看不到”寫緩存,我們自己來管理寫緩存的生命周期,GC問題自然也就迎刃而解了。

說起讓JVM“看不到”,可能很多人想到的是off-heap的解決方案,但是這對寫緩存來說沒那么簡單,因為即使把KeyValue放到offheap,也無法避免問題1和問題2。而1和2也是young?GC的最大困擾。

問題現在被轉化成了:如何不使用JVM對象來構建一個有序的支持并發訪問的Map。
當然我們也不能接受性能損失,因為寫入Map的速度和HBase的寫吞吐息息相關。
需求再次強化:如何不使用對象來構建一個有序的支持并發訪問的Map,且不能有性能損失。

為了達成這個目標,我們設計了這樣一個數據結構:

·??????它使用連續的內存(堆內or堆外),我們通過代碼控制內部結構而不是依賴于JVM的對象機制

·??????在邏輯上也是一個SkipList,支持無鎖的并發寫入和查詢

·??????控制指針和數據都存放在連續內存中


上圖所展示的即是CCSMap(CompactedConcurrentSkipListMap)的內存結構。 我們以大塊的內存段(Chunk)的方式申請寫緩存內存。每個Chunk包含多個Node,每個Node對應一個元素。新插入的元素永遠放在已使用內存的末尾。Node內部復雜的結構,存放了Index/Next/Key/Value等維護信息和數據。新插入的元素需要拷貝到Node結構中。當HBase發生寫緩存dump時,整個CCSMap的所有Chunk都會被回收。當元素被刪除時,我們只是邏輯上把元素從鏈表里"踢走",不會把元素實際從內存中收回(當然做實際回收也是有方法,就HBase而言沒有那個必要)。

插入KeyValue數據時雖然多了一遍拷貝,但是就絕大多數情況而言,拷貝反而會更快。因為從CCSMap的結構來看,一個Map中的元素的控制節點和KeyValue在內存上是鄰近的,利用CPU緩存的效率更高,seek會更快。對于SkipList來說,寫速度其實是bound在seek速度上的,實際拷貝產生的overhead遠不如seek的開銷。根據我們的測試,CCSMap和JDK自帶的ConcurrentSkipListMap相比,50Byte長度KV的測試中,讀寫吞吐提升了20~30%。

由于沒有了JVM對象,每個JVM對象至少占用16Byte空間也可以被節省掉(8byte為標記預留,8byte為類型指針)。還是以50Byte長度KeyValue為例,CCSMap和JDK自帶的ConcurrentSkipListMap相比,內存占用減少了40%。

CCSMap在生產中上線后,實際優化效果: young?GC從120ms+減少到了30ms


優化前


優化后

使用了CCSMap后,原來的1億2千萬個存活對象被縮減到了千萬級別以內,大大減輕了GC壓力。由于緊致的內存排布,寫入吞吐能力也得到了30%的提升

永不晉升的Cache:BucketCache

HBase以Block的方式組織磁盤上的數據。一個典型的HBase Block大小在16K~64K之間。HBase內部會維護BlockCache來減少磁盤的I/O。BlockCache和寫緩存一樣,不符合GC算法理論里的分代假說,天生就是對GC算法不友好的 —— 既不稍縱即逝,也不永久存活。

一段Block數據從磁盤被load到JVM內存中,生命周期從分鐘到月不等,絕大部分Block都會進入old區,只有Major GC時才會讓它被JVM回收。它的麻煩主要體現在:

1.??HBase Block的大小不是固定的,且相對較大,內存容易碎片化

2.??在ParNew算法上,晉升麻煩。麻煩不是體現在拷貝代價上,而是因為尺寸較大,尋找合適的空間存放HBase Block的代價較高

讀緩存優化的思路則是,向JVM申請一塊永不歸還的內存作為BlockCache,我們自己對內存進行固定大小的分段,當Block加載到內存中時,我們將Block拷貝到分好段的區間內,并標記為已使用。當這個Block不被需要時,我們會標記該區間為可用,可以重新存放新的Block,這就是BucketCache。關于BucketCache中的內存空間分配與回收(這一塊的設計與研發在多年前已完成),詳細可以參考?:?http://zjushch.iteye.com/blog/1751387

很多基于堆外內存的RPC框架,也會自己管理堆外內存的分配和回收,一般通過顯式釋放的方式進行內存回收。但是對HBase來說,卻有一些困難。我們將Block對象視為需要自管理的內存片段。Block可能被多個任務引用,要解決Block的回收問題,最簡單的方式是將Block對每個任務copy到棧上(copy的block一般不會晉升到old區),轉交給JVM管理就可以。

實際上,我們之前一直使用的是這種方法,實現簡單,JVM背書,安全可靠。但這是有損耗的內存管理方式,為了解決GC問題,引入了每次請求的拷貝代價。由于拷貝到棧上需要支付額外的cpu拷貝成本和young區內存分配成本,在cpu和總線越來越珍貴的今天,這個代價顯得高昂。

于是我們轉而考慮使用引用計數的方式管理內存,HBase上遇到的主要難點是:

1.??HBase內部會有多個任務引用同一個Block

2.??同一個任務內可能有多個變量引用同一個Block。引用者可能是棧上臨時變量,也可能是堆上對象域。

3.??Block上的處理邏輯相對復雜,Block會在多個函數和對象之間以參數、返回值、域賦值的方式傳遞。

4.??Block可能是受我們管理的,也可能是不受我們管理的(某些Block需要手動釋放,某些不需要)。

5.??Block可能被轉換為Block的子類型。

這幾點綜合起來,對如何寫出正確的代碼是一個挑戰。但在C++ 上,使用智能指針來管理對象生命周期是很自然的事情,為什么到了Java里會有困難呢?

Java中變量的賦值,在用戶代碼的層面上,只會產生引用賦值的行為,而C++ 中的變量賦值可以利用對象的構造器和析構器來干很多事情,智能指針即基于此實現(當然C++的構造器和析構器使用不當也會引發很多問題,各有優劣,這里不討論)

于是我們參考了C++的智能指針,設計了一個Block引用管理和回收的框架ShrableHolder來抹平coding中各種if else的困難。它有以下的范式:

1.??ShrableHolder可以管理有引用計數的對象,也可以管理非引用計數的對象

2.??ShrableHolder在被重新賦值時,釋放之前的對象。如果是受管理的對象,引用計數減1,如果不是,則無變化。

3.??ShrableHolder在任務結束或者代碼段結束時,必須被調用reset

4.??ShrableHolder不可直接賦值。必須調用ShrableHolder提供的方法進行內容的傳遞

5.??因為ShrableHolder不可直接賦值,需要傳遞包含生命周期語義的Block到函數中時,ShrableHolder不能作為函數的參數。

根據這個范式寫出來的代碼,原來的代碼邏輯改動很少,不會引入if else。雖然看上去仍然有一些復雜度,所幸的是,受此影響的區間還是局限于非常局部的下層,對HBase而言還是可以接受的。為了保險起見,避免內存泄漏,我們在這套框架里加入了探測機制,探測長時間不活動的引用,發現之后會強制標記為刪除。

將BucketCache應用之后,減少了BlockCache的晉升開銷,減少了young?GC時間:

(CCSMap+BucketCache優化后的效果) ?

追求極致:AliGC

經過以上兩個大的優化之后,螞蟻風控生產環境的young?GC時間已經縮減到15ms。由于ParNew+CMS算法在這個尺度上再做優化已經很困難了,我們轉而投向AliGC的懷抱。AliGC在G1算法的基礎上做了深度改進,內存自管理的大堆HBase和AliGC產生了很好的化學反應。

AliGC是阿里巴巴JVM團隊基于G1算法, 面向大堆 (LargeHeap) 應用場景,優化的GC算法的統稱。這里主要介紹下多租戶GC。

多租戶GC包含的三層核心邏輯:1)?在JavaHeap上,對象的分配按照租戶隔離,不同的租戶使用不同的Heap區域;2)允許GC以更小的代價發生在租戶粒度,而不僅僅是應用的全局;3)允許上層應用根據業務需求對租戶靈活映射。

AliGC將內存Region劃分為了多個租戶,每個租戶內獨立觸發GC。在個基礎上,我們將內存分為普通租戶和中等生命周期租戶。中等生命周期對象指的是,既不稍縱即逝,也不永久存在的對象。由于經過以上兩個大幅優化,現在堆中等生命周期對象數量和內存占用已經很少了。但是中等生命周期對象在生成時會被old區對象引用,每次young?GC都需要掃描RSet,現在仍然是young?GC的耗時大頭。

借助于AJDK團隊的ObjectTrace功能,我們找出中等生命周期對象中最"大頭"的部分,將這些對象在生成時直接分配到中等生命周期租戶的old區,避免RSet標記。而普通租戶則以正常的方式進行內存分配。

普通租戶GC頻率很高,但是由于晉升的對象少,跨代引用少,Young區的GC時間得到了很好的控制。在實驗室場景仿真環境中,我們將young?GC優化到了5ms。


(AliGC優化后的效果,單位問題,此處為us) ?

云端使用

阿里HBase目前已經在阿里云提供商業化服務,任何有需求的用戶都可以在阿里云端使用深入改進的、一站式的HBase服務。云HBase版本與自建HBase相比在運維、可靠性、性能、穩定性、安全、成本等方面均有很多的改進,更多內容歡迎大家關注?https://www.aliyun.com/product/hbase

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

推薦閱讀更多精彩內容