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

HBase系統中一張表會被水平切分紅多個Region,每一個Region負責本身區域的數據讀寫請求。水平切分意味着每一個Region會包含全部的列簇數據,HBase將不一樣列簇的數據存儲在不一樣的Store中,每一個Store由一個MemStore和一系列HFile組成,如圖所示。數據庫

image.png
Region結構組成數組

HBase基於LSM樹模型實現,全部的數據寫入操做首先會順序寫入日誌HLog,再寫入MemStore,當MemStore中數據大小超過閾值以後再將這些數據批量寫入磁盤,生成一個新的HFile文件。LSM樹架構有以下幾個很是明顯的優點:緩存

•這種寫入方式將一次隨機IO寫入轉換成一個順序IO寫入(HLog順序寫入)加上一次內存寫入(MemStore寫入),使得寫入性能獲得極大提高。大數據領域中對寫入性能有較高要求的數據庫系統幾乎都會採用這種寫入模型,好比分佈式列式存儲系統Kudu、時間序列存儲系統Druid等。安全

•HFile中KeyValue數據須要按照Key排序,排序以後能夠在文件級別根據有序的Key創建索引樹,極大提高數據讀取效率。然而HDFS自己只容許順序讀寫,不能更新,所以須要數據在落盤生成HFile以前就完成排序工做,MemStore就是KeyValue數據排序的實際執行者。數據結構

•MemStore做爲一個緩存級的存儲組件,老是緩存着最近寫入的數據。對於不少業務來講,最新寫入的數據被讀取的機率會更大,最典型的好比時序數據,80%的請求都會落到最近一天的數據上。實際上對於某些場景,新寫入的數據存儲在MemStore對讀取性能的提高相當重要。多線程

•在數據寫入HFile以前,能夠在內存中對KeyValue數據進行不少更高級的優化。好比,若是業務數據保留版本僅設置爲1,在業務更新比較頻繁的場景下,MemStore中可能會存儲某些數據的多個版本。這樣,MemStore在將數據寫入HFile以前實際上能夠丟棄老版本數據,僅保留最新版本數據。架構

MemStore內部結構

上面講到寫入(包括更新刪除操做)HBase中的數據都會首先寫入MemStore,除此以外,MemStore還要承擔業務多線程併發訪問的職責。那麼一個很現實的問題就是,MemStore應該採用什麼樣的數據結構,既可以保證高效的寫入效率,又可以保證高效的多線程讀取效率?併發

實際實現中,HBase採用了跳躍表這種數據結構,固然,HBase並無直接使用原始跳躍表,而是使用了JDK自帶的數據結構ConcurrentSkipListMap。ConcurrentSkipListMap底層使用跳躍表來保證數據的有序性,並保證數據的寫入、查找、刪除操做均可以在O(logN)的時間複雜度完成。除此以外,ConcurrentSkipListMap有個很是重要的特色是線程安全,它在底層採用了CAS原子性操做,避免了多線程訪問條件下昂貴的鎖開銷,極大地提高了多線程訪問場景下的讀寫性能。異步

MemStore由兩個ConcurrentSkipListMap(稱爲A和B)實現,寫入操做(包括更新刪除操做)會將數據寫入ConcurrentSkipListMap A,當ConcurrentSkipListMap A中數據量超過必定閾值以後會建立一個新的ConcurrentSkipListMap B來接收用戶新的請求,以前已經寫滿的ConcurrentSkipListMap A會執行異步f lush操做落盤造成HFile。分佈式

MemStore的GC問題

MemStore從本質上來看就是一塊緩存,能夠稱爲寫緩存。衆所周知在Java系統中,大內存系統總會面臨GC問題,MemStore自己會佔用大量內存,所以GC的問題不可避免。不只如此,HBase中MemStore工做模式的特殊性更會引發嚴重的內存碎片,存在大量內存碎片會致使系統看起來彷佛還有不少空間,但實際上這些空間都是一些很是小的碎片,已經分配不出一塊完整的可用內存,這時會觸發長時間的Full GC。

爲何MemStore的工做模式會引發嚴重的內存碎片?這是由於一個RegionServer由多個Region構成,每一個Region根據列簇的不一樣又包含多個MemStore,這些MemStore都是共享內存的。這樣,不一樣Region的數據寫入對應的MemStore,由於共享內存,在JVM看來全部MemStore的數據都是混合在一塊兒寫入Heap的。此時假如Region1上對應的全部MemStore執行落盤操做,就會出現圖所示場景。

image.png

MemStore f lush產生內存條帶

上圖中不一樣Region由不一樣顏色表示,右邊圖爲JVM中MemStore所佔用的內存圖,可見不一樣Region的數據在JVM Heap中是混合存儲的,一旦深灰色條帶表示的Region1的全部MemStore數據執行f lush操做,這些深灰色條帶所佔內存就會被釋放,變成白色條帶。這些白色條帶會繼續爲寫入MemStore的數據分配空間,進而會分割成更小的條帶。從JVM全局的視角來看,隨着MemStore中數據的不斷寫入而且f lush,整個JVM將會產生大量愈來愈小的內存條帶,這些條帶實際上就是內存碎片。隨着內存碎片愈來愈小,最後甚至分配不出來足夠大的內存給寫入的對象,此時就會觸發JVM執行Full GC合併這些內存碎片。

MSLAB內存管理方式

爲了優化這種內存碎片可能致使的Full GC,HBase借鑑了線程本地分配緩存(Thread-Local Allocation Buffer,TLAB)的內存管理方式,經過順序化分配內存、內存數據分塊等特性使得內存碎片更加粗粒度,有效改善Full GC狀況。具體實現步驟以下:

1)每一個MemStore會實例化獲得一個MemStoreLAB對象。

2)MemStoreLAB會申請一個2M大小的Chunk數組,同時維護一個Chunk偏移量,該偏移量初始值爲0。

3)當一個KeyValue值插入MemStore後,MemStoreLAB會首先經過KeyValue.getBuffer()取得data數組,並將data數組複製到Chunk數組中,以後再將Chunk偏移量往前移動data. length。

4)當前Chunk滿了以後,再調用new byte[2 1024 1024]申請一個新的Chunk。

這種內存管理方式稱爲MemStore本地分配緩存(MemStore-Local AllocationBuffer,MSLAB)。下圖是針對MSLAB的一個簡單示意圖,右側爲JVM中MemStore所佔用的內存圖,和優化前不一樣的是,不一樣顏色的細條帶會彙集在一塊兒造成了2M大小的粗條帶。這是由於MemStore會在將數據寫入內存時首先申請2M的Chunk,再將實際數據寫入申請的Chunk中。這種內存管理方式,使得f lush以後殘留的內存碎片更加粗粒度,極大下降Full GC的觸發頻率。

image.png

MemStore Chunk Pool

通過MSLAB優化以後,系統由於MemStore內存碎片觸發的Full GC次數會明顯下降。然而這樣的內存管理模式並不完美,還存在一些「小問題」。好比一旦一個Chunk寫滿以後,系統會從新申請一個新的Chunk,新建Chunk對象會在JVM新生代申請新內存,若是申請比較頻繁會致使JVM新生代Eden區滿掉,觸發YGC。試想若是這些Chunk可以被循環利用,系統就不須要申請新的Chunk,這樣就會使得YGC頻率下降,晉升到老年代的Chunk就會減小,CMS GC發生的頻率也會下降。這就是MemStore Chunk Pool的核心思想,具體實現步驟以下:

1)系統建立一個Chunk Pool來管理全部未被引用的Chunk,這些Chunk就不會再被JVM看成垃圾回收。
2)若是一個Chunk沒有再被引用,將其放入Chunk Pool。
3)若是當前Chunk Pool已經達到了容量最大值,就不會再接納新的Chunk。
4)若是須要申請新的Chunk來存儲KeyValue,首先從Chunk Pool中獲取,若是可以獲取獲得就重複利用,不然就從新申請一個新的Chunk。

MSLAB相關配置

HBase中MSLAB功能默認是開啓的,默認的ChunkSize是2M,也能夠經過參數"hbase.hregion.memstore.mslab.chunksize"進行設置,建議保持默認值。Chunk Pool功能默認是關閉的,須要配置參數"hbase.hregion.memstore.chunkpool.maxsize"爲大於0的值才能開啓,該值默認是0。"hbase.hregion.memstore.chunkpool.maxsize"取值爲[0, 1],表示整個MemStore分配給Chunk Pool的總大小爲hbase.hregion.memstore.chunkpool. maxsize * Memstore Size。另外一個相關參數"hbase.hregion.memstore.chunkpool.initialsize"取值爲[0, 1],表示初始化時申請多少個Chunk放到Pool裏面,默認是0,表示初始化時不申請內存。

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

相關文章
相關標籤/搜索