HBase原理--RegionServer核心組件之BlockCache

衆所周知,提高數據庫讀取性能的一個核心方法是,儘量將熱點數據存儲到內存中,以免昂貴的IO開銷。現代系統架構中,諸如Redis這類緩存組件已是體系中的核心組件,一般將其部署在數據庫的上層,攔截系統的大部分請求,保證數據庫的「安全」,提高整個系統的讀取效率。算法

一樣爲了提高讀取性能,HBase也實現了一種讀緩存結構——BlockCache。客戶端讀取某個Block,首先會檢查該Block是否存在於Block Cache,若是存在就直接加載出來,若是不存在則去HFile文件中加載,加載出來以後放到Block Cache中,後續同一請求或者鄰近數據查找請求能夠直接從內存中獲取,以免昂貴的IO操做。數據庫

從字面意思能夠看出來,BlockCache主要用來緩存Block。須要關注的是,Block是HBase中最小的數據讀取單元,即數據從HFile中讀取都是以Block爲最小單元執行的。緩存

BlockCache是RegionServer級別的,一個RegionServer只有一個BlockCache,在RegionServer啓動時完成BlockCache的初始化工做。到目前爲止,HBase前後實現了3種BlockCache方案,LRUBlockCache是最先的實現方案,也是默認的實現方案;HBase 0.92版本實現了第二種方案SlabCache,參見HBASE-4027;HBase 0.96以後官方提供了另外一種可選方案BucketCache,參見HBASE-7404。安全

這3種方案的不一樣之處主要在於內存管理模式,其中LRUBlockCache是將全部數據都放入JVM Heap中,交給JVM進行管理。然後兩種方案採用的機制容許將部分數據存儲在堆外。這種演變本質上是由於LRUBlockCache方案中JVM垃圾回收機制常常致使程序長時間暫停,而採用堆外內存對數據進行管理能夠有效緩解系統長時間GC。架構

LRUBlockCache

LRUBlockCache是HBase目前默認的BlockCache機制,實現相對比較簡單。它使用一個ConcurrentHashMap管理BlockKey到Block的映射關係,緩存Block只須要將BlockKey和對應的Block放入該HashMap中,查詢緩存就根據BlockKey從HashMap中獲取便可。同時,該方案採用嚴格的LRU淘汰算法,當Block Cache總量達到必定閾值以後就會啓動淘汰機制,最近最少使用的Block會被置換出來。在具體的實現細節方面,須要關注如下三點。併發

1.緩存分層策略異步

HBase採用了緩存分層設計,將整個BlockCache分爲三個部分:single-access、multi-access和in-memory,分別佔到整個BlockCache大小的25%、50%、25%。性能

在一次隨機讀中,一個Block從HDFS中加載出來以後首先放入single-access區,後續若是有屢次請求訪問到這個Block,就會將這個Block移到multi-access區。而in-memory區表示數據能夠常駐內存,通常用來存放訪問頻繁、量小的數據,好比元數據,用戶能夠在建表的時候設置列簇屬性IN_MEMORY=true,設置以後該列簇的Block在從磁盤中加載出來以後會直接放入in-memory區。測試

須要注意的是,設置IN_MEMORY=true並不意味着數據在寫入時就會被放到in-memory區,而是和其餘BlockCache區同樣,只有從磁盤中加載出Block以後纔會放入該區。另外,進入in-memory區的Block並不意味着會一直存在於該區,仍會基於LRU淘汰算法在空間不足的狀況下淘汰最近最不活躍的一些Block。spa

由於HBase系統元數據(hbase:meta,hbase:namespace等表)都存放在in-memory區,所以對於不少業務表來講,設置數據屬性IN_MEMORY=true時須要很是謹慎,必定要確保此列簇數據量很小且訪問頻繁,不然可能會將hbase:meta等元數據擠出內存,嚴重影響全部業務性能。

2. LRU淘汰算法實現
在每次cache block時,系統將BlockKey和Block放入HashMap後都會檢查BlockCache總量是否達到閾值,若是達到閾值,就會喚醒淘汰線程對Map中的Block進行淘汰。系統設置3個MinMaxPriorityQueue,分別對應上述3個分層,每一個隊列中的元素按照最近最少被使用的規則排列,系統會優先取出最近最少使用的Block,將其對應的內存釋放。可見,3個分層中的Block會分別執行LRU淘汰算法進行淘汰。

3. LRUBlockCache方案優缺點
LRUBlockCache方案使用JVM提供的HashMap管理緩存,簡單有效。但隨着數據從single-access區晉升到multi-access區或長時間停留在single-access區,對應的內存對象會從young區晉升到old區,晉升到old區的Block被淘汰後會變爲內存垃圾,最終由CMS回收(Conccurent Mark Sweep,一種標記清除算法),顯然這種算法會帶來大量的內存碎片,碎片空間一直累計就會產生臭名昭著的FullGC。尤爲在大內存條件下,一次Full GC極可能會持續較長時間,甚至達到分鐘級別。Full GC會將整個進程暫停,稱爲stop-the-world暫停(STW),所以長時間Full GC必然會極大影響業務的正常讀寫請求。正由於該方案有這樣的弊端,以後相繼出現了SlabCache方案和BucketCache方案。

SlabCache

爲了解決LRUBlockCache方案中因JVM垃圾回收致使的服務中斷問題,SlabCache方案提出使用Java NIO DirectByteBuffer技術實現堆外內存存儲,再也不由JVM管理數據內存。默認狀況下,系統在初始化的時候會分配兩個緩存區,分別佔整個BlockCache大小的80%和20%,每一個緩存區分別存儲固定大小的Block,其中前者主要存儲小於等於64K的Block,後者存儲小於等於128K的Block,若是一個Block太大就會致使兩個區都沒法緩存。和LRUBlockCache相同,SlabCache也使用Least-Recently-Used算法淘汰過時的Block。和LRUBlockCache不一樣的是,SlabCache淘汰Block時只須要將對應的BufferByte標記爲空閒,後續cache對其上的內存直接進行覆蓋便可。

線上集羣環境中,不一樣表不一樣列簇設置的BlockSize均可能不一樣,很顯然,默認只能存儲小於等於128KB Block的SlabCache方案不能知足部分用戶場景。好比,用戶設置BlockSize=256K,簡單使用SlabCache方案就不能達到緩存這部分Block的目的。所以HBase在實際實現中將SlabCache和LRUBlockCache搭配使用,稱爲DoubleBlockCache。在一次隨機讀中,一個Block從HDFS中加載出來以後會在兩個Cache中分別存儲一份。緩存讀時首先在LRUBlockCache中查找,若是CacheMiss再在SlabCache中查找,此時若是命中,則將該Block放入LRUBlockCache中。

通過實際測試,DoubleBlockCache方案有不少弊端。好比,SlabCache中固定大小內存設置會致使實際內存使用率比較低,並且使用LRUBlockCache緩存Block依然會由於JVM GC產生大量內存碎片。所以在HBase 0.98版本以後,已經不建議使用該方案。

BucketCache

SlabCache方案在實際應用中並無很大程度改善原有LRUBlockCache方案的GC弊端,還額外引入了諸如堆外內存使用率低的缺陷。然而它的設計並非一無可取,至少在使用堆外內存這方面給予了後續開發者不少啓發。站在SlabCache的肩膀上,社區工程師設計開發了另外一種很是高效的緩存方案——BucketCache。

BucketCache經過不一樣配置方式能夠工做在三種模式下:heap,offheap和f ile。heap模式表示這些Bucket是從JVM Heap中申請的;offheap模式使用DirectByteBuffer技術實現堆外內存存儲管理;f ile模式使用相似SSD的存儲介質來緩存Data Block。不管工做在哪一種模式下,BucketCache都會申請許多帶有固定大小標籤的Bucket,和SlabCache同樣,一種Bucket存儲一種指定BlockSize的Data Block,但和SlabCache不一樣的是,BucketCache會在初始化的時候申請14種不一樣大小的Bucket,並且若是某一種Bucket空間不足,系統會從其餘Bucket空間借用內存使用,所以不會出現內存使用率低的狀況。

實際實現中,HBase將BucketCache和LRUBlockCache搭配使用,稱爲CombinedBlock-Cache。和DoubleBlockCache不一樣,系統在LRUBlockCache中主要存儲Index Block和Bloom Block,而將Data Block存儲在BucketCache中。所以一次隨機讀須要先在LRUBlockCache中查到對應的Index Block,而後再到BucketCache查找對應Data Block。BucketCache經過更加合理的設計修正了SlabCache的弊端,極大下降了JVM GC對業務請求的實際影響,但其也存在一些問題。好比,使用堆外內存會存在拷貝內存的問題,在必定程度上會影響讀寫性能。固然,在以後的2.0版本中這個問題獲得瞭解決,參見HBASE-11425。

相比LRUBlockCache,BucketCache實現相對比較複雜。它沒有使用JVM內存管理算法來管理緩存,而是本身對內存進行管理,所以大大下降了由於出現大量內存碎片致使Full GC發生的風險。鑑於生產線上CombinedBlockCache方案使用的廣泛性,下文主要介紹BucketCache的具體實現方式(包括BucketCache的內存組織形式、緩存寫入讀取流程等)以及配置使用方式。

1. BucketCache的內存組織形式

下圖所示爲BucketCache的內存組織形式,圖中上半部分是邏輯組織結構,下半部分是對應的物理組織結構。HBase啓動以後會在內存中申請大量的Bucket,每一個Bucket的大小默認爲2MB。每一個Bucket會有一個baseoffset變量和一個size標籤,其中baseoffset變量表示這個Bucket在實際物理空間中的起始地址,所以Block的物理地址就能夠經過baseoffset和該Block在Bucket的偏移量惟一肯定;size標籤表示這個Bucket能夠存放的Block大小,好比圖中左側Bucket的size標籤爲65KB,表示能夠存放64KB的Block,右側Bucket的size標籤爲129KB,表示能夠存放128KB的Block。

image.png

HBase中使用BucketAllocator類實現對Bucket的組織管理。

1)HBase會根據每一個Bucket的size標籤對Bucket進行分類,相同size標籤的Bucket由同一個BucketSizeInfo管理,如上圖所示,左側存放64KB Block的Bucket由65KB BucketSizeInfo管理,右側存放128KB Block的Bucket由129KBBucketSizeInfo管理。可見,BucketSize大小總會比Block自己大1KB,這是由於Block自己並非嚴格固定大小的,總會大那麼一點,好比64K的Block老是會比64K大一些。

2)HBase在啓動的時候就決定了size標籤的分類,默認標籤有(4+1)K,(8+1)K,(16+1)K...(48+1)K,(56+1)K,(64+1)K,(96+1)K...(512+1)K。並且系統會首先從小到大遍歷一次全部size標籤,爲每種size標籤分配一個Bucket,最後全部剩餘的Bucket都分配最大的size標籤,默認分配 (512+1)K,以下圖所示。

image.png

3)Bucket的size標籤能夠動態調整,好比64K的Block數目比較多,65K的Bucket用完了之後,其餘size標籤的徹底空閒的Bucket能夠轉換成爲65K的Bucket,可是會至少保留一個該size的Bucket。

2. BucketCache中Block緩存寫入、讀取流程

圖所示是Block寫入緩存以及從緩存中讀取Block的流程,圖中主要包括5個模塊:

image.png
BucketCache中Block緩存寫入及讀取流程

•RAMCache是一個存儲blockKey和Block對應關係的HashMap。•WriteThead是整個Block寫入的中心樞紐,主要負責異步地將Block寫入到內存空間。
•BucketAllocator主要實現對Bucket的組織管理,爲Block分配內存空間。
•IOEngine是具體的內存管理模塊,將Block數據寫入對應地址的內存空間。
•BackingMap也是一個HashMap,用來存儲blockKey與對應物理內存偏移量的映射關係,而且根據blockKey定位具體的Block。圖中實線表示Block寫入流程,虛線表示Block緩存讀取流程。

Block緩存寫入流程以下:

1)將Block寫入RAMCache。實際實現中,HBase設置了多個RAMCache,系統首先會根據blockKey進行hash,根據hash結果將Block分配到對應的RAMCache中。

2)WriteThead從RAMCache中取出全部的Block。和RAMCache相同,HBase會同時啓動多個WriteThead併發地執行異步寫入,每一個WriteThead對應一個RAMCache。

3)每一個WriteThead會遍歷RAMCache中全部Block,分別調用bucketAllocator爲這些Block分配內存空間。

4)BucketAllocator會選擇與Block大小對應的Bucket進行存放,而且返回對應的物理地址偏移量offset。

5)WriteThead將Block以及分配好的物理地址偏移量傳給IOEngine模塊,執行具體的內存寫入操做。

6)寫入成功後,將blockKey與對應物理內存偏移量的映射關係寫入BackingMap中,方便後續查找時根據blockKey直接定位。

Block緩存讀取流程以下:

1)首先從RAMCache中查找。對於尚未來得及寫入Bucket的緩存Block,必定存儲在RAMCache中。

2)若是在RAMCache中沒有找到,再根據blockKey在BackingMap中找到對應的物理偏移地址量offset。

3)根據物理偏移地址offset直接從內存中查找對應的Block數據。

文章基於《HBase原理與實踐》一書

相關文章
相關標籤/搜索