首頁  >  文章  >  Java  >  Java垃圾回收時間也可以輕鬆降低,實例講解Ali-HBase的GC

Java垃圾回收時間也可以輕鬆降低,實例講解Ali-HBase的GC

php是最好的语言
php是最好的语言原創
2018-07-28 16:55:512261瀏覽

如何降低90%Java垃圾回收時間? GC在Java中大家應該很熟悉了,GC優化是怎麼進行的呢,以下為大家詳細講解。 JVM的GC機制對開發者屏蔽了記憶體管理的細節,提高了開發效率。 apache php mysql

在不久前,我們準備在Ali-HBase上突破這個被普遍認知的痛點,為此進行了深度分析及全面創新的工作,獲得了一些比較好的效果。以螞蟻風控場景為例,HBase的線上young GC時間從120ms減少到15ms,結合阿里巴巴JDK團隊提供的利器-ZenGC,進一步在實驗室壓測環境做到了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演算法—ZenGC。集團HBase基於這個新的ZenGC演算法進行改造,我們在實驗室中壓測的young GC時間從15ms減少到5ms,這是一個未曾期望的極致效果。

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

更快更省的CCSMap

目前HBase使用的儲存模型是LSMTree模型,寫入的資料會在記憶體中暫存到一定規模後再dump到磁碟上形成檔案。

下面我們將其簡稱為寫入快取。寫快取是可查詢的,這就要求資料在記憶體中有序。為了提高並發讀寫效率,並達成資料有序且支援seek&scan的基本要求,SkipList是使用得比較廣泛的資料結構。

Java垃圾回收時間也可以輕鬆降低,實例講解Ali-HBase的GC

我們以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,支援無鎖的並發寫入和查詢

  • 控制指針和資料都存放在連續記憶體中

Java垃圾回收時間也可以輕鬆降低,實例講解Ali-HBase的GC

上圖所展示的即是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

Java垃圾回收時間也可以輕鬆降低,實例講解Ali-HBase的GC優化前

Java垃圾回收時間也可以輕鬆降低,實例講解Ali-HBase的GC 優化後

使用了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回收。它的麻煩主要體現在:

HBase Block的大小不是固定的,且相對較大,記憶體容易碎片化

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

讀取快取優化的想法則是,向JVM申請一塊永不歸還的記憶體作為BlockCache,我們自己對記憶體進行固定大小的分段,當Block載入到記憶體時,我們將Block拷貝到分好段的區間內,並標示為已使用。當這個Block不被需要時,我們會標記該區間為可用,可以重新存放新的Block,這就是BucketCache。關於BucketCache中的記憶體空間分配與回收(這一塊的設計與研發在多年前已完成)

Java垃圾回收時間也可以輕鬆降低,實例講解Ali-HBase的GCBucketCache

很多基於堆外記憶體的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時間:

Java垃圾回收時間也可以輕鬆降低,實例講解Ali-HBase的GC

Java垃圾回收時間也可以輕鬆降低,實例講解Ali-HBase的GC

(CCSMap BucketCache優化後的效果)

淺談ZenGC

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

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

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

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

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

普通租戶GC頻率很高,但由於升遷的物件少,跨世代引用少,Young區的GC時間得到了很好的控制。在實驗室場景模擬環境中,我們將young GC優化到了5ms。

Java垃圾回收時間也可以輕鬆降低,實例講解Ali-HBase的GC

(ZenGC最佳化後的效果,單位問題,此處為us)

Java垃圾回收時間也可以輕鬆降低,實例講解Ali-HBase的GC

Java垃圾回收時間也可以輕鬆降低,實例講解Ali-HBase的GC

##在雲端的使用

Ali-HBase目前已經在阿里雲提供商業化服務,任何有需求的用戶都可以在阿里雲端使用深入改進的、一站式的HBase服務。雲端HBase版本與自建HBase相比在維運、可靠性、效能、穩定性、安全性、成本等方面都有許多的改進。

相關文章:

降低java垃圾回收開銷的5個建議

java垃圾回收

相關影片:

垃圾回收機制-韓順平2016年最新PHP物件導向程式設計影片教學#

以上是Java垃圾回收時間也可以輕鬆降低,實例講解Ali-HBase的GC的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn