HBase採用LSM樹架構,天生適用於寫多讀少的應用場景。在真實生產線環境中,也正是由於HBase集羣出色的寫入能力,才能支持當下不少數據激增的業務。須要說明的是,HBase服務端並無提供update、delete接口,HBase中對數據的更新、刪除操做在服務器端也認爲是寫入操做,不一樣的是,更新操做會寫入一個最新版本數據,刪除操做會寫入一條標記爲deleted的KV數據。因此HBase中更新、刪除操做的流程與寫入流程徹底一致。固然,HBase數據寫入的整個流程隨着版本的迭代在不斷優化,但整體流程變化不大。算法
從總體架構的視角來看,寫入流程能夠歸納爲三個階段。shell
1)客戶端處理階段:客戶端將用戶的寫入請求進行預處理,並根據集羣元數據定位寫入數據所在的RegionServer,將請求發送給對應的RegionServer。數組
2)Region寫入階段:RegionServer接收到寫入請求以後將數據解析出來,首先寫入WAL,再寫入對應Region列簇的MemStore。緩存
3)MemStore Flush階段:當Region中MemStore容量超過必定閾值,系統會異步執行f lush操做,將內存中的數據寫入文件,造成HFile。服務器
用戶寫入請求在完成Region MemStore的寫入以後就會返回成功。MemStoreFlush是一個異步執行的過程。數據結構
1. 客戶端處理階段
HBase客戶端處理寫入請求的核心流程基本上能夠歸納爲三步。架構
步驟1:用戶提交put請求後,HBase客戶端會將寫入的數據添加到本地緩衝區中,符合必定條件就會經過AsyncProcess異步批量提交。HBase默認設置autoflush=true,表示put請求直接會提交給服務器進行處理;用戶能夠設置autoflush=false,這樣,put請求會首先放到本地緩衝區,等到本地緩衝區大小超過必定閾值(默認爲2M,能夠經過配置文件配置)以後纔會提交。很顯然,後者使用批量提交請求,能夠極大地提高寫入吞吐量,可是由於沒有保護機制,若是客戶端崩潰,會致使部分已經提交的數據丟失。併發
步驟2:在提交以前,HBase會在元數據表hbase:meta中根據rowkey找到它們歸屬的RegionServer,這個定位的過程是經過HConnection的locateRegion方法完成的。若是是批量請求,還會把這些rowkey按照HRegionLocation分組,不一樣分組的請求意味着發送到不一樣的RegionServer,所以每一個分組對應一次RPC請求。app
Client與ZooKeeper、RegionServer的交互過程如圖所示。框架
•客戶端根據寫入的表以及rowkey在元數據緩存中查找,若是可以查找出該rowkey所在的RegionServer以及Region,就能夠直接發送寫入請求(攜帶Region信息)到目標RegionServer。
•若是客戶端緩存中沒有查到對應的rowkey信息,須要首先到ZooKeeper上/hbase-root/meta-region-server節點查找HBase元數據表所在的RegionServer。向hbase:meta所在的RegionServer發送查詢請求,在元數據表中查找rowkey所在的RegionServer以及Region信息。客戶端接收到返回結果以後會將結果緩存到本地,以備下次使用。
•客戶端根據rowkey相關元數據信息將寫入請求發送給目標RegionServer,Region Server接收到請求以後會解析出具體的Region信息,查到對應的Region對象,並將數據寫入目標Region的MemStore中。
步驟3:HBase會爲每一個HRegionLocation構造一個遠程RPC請求MultiServerCallable,並經過rpcCallerFactory. newCaller()執行調用。將請求通過Protobuf序列化後發送給對應的RegionServer。
2. Region寫入階段
服務器端RegionServer接收到客戶端的寫入請求後,首先會反序列化爲put對象,而後執行各類檢查操做,好比檢查Region是不是隻讀、MemStore大小是否超過blockingMemstoreSize等。檢查完成以後,執行一系列核心操做
Region寫入流程
1)Acquire locks :HBase中使用行鎖保證對同一行數據的更新都是互斥操做,用以保證更新的原子性,要麼更新成功,要麼更新失敗。
2)Update LATEST_TIMESTAMP timestamps :更新全部待寫入(更新)KeyValue的時間戳爲當前系統時間。
3)Build WAL edit :HBase使用WAL機制保證數據可靠性,即首先寫日誌再寫緩存,即便發生宕機,也能夠經過恢復HLog還原出原始數據。該步驟就是在內存中構建WALEdit對象,爲了保證Region級別事務的寫入原子性,一次寫入操做中全部KeyValue會構建成一條WALEdit記錄。
4)Append WALEdit To WAL :將步驟3中構造在內存中的WALEdit記錄順序寫入HLog中,此時不須要執行sync操做。當前版本的HBase使用了disruptor實現了高效的生產者消費者隊列,來實現WAL的追加寫入操做。
5)Write back to MemStore:寫入WAL以後再將數據寫入MemStore。
6)Release row locks:釋放行鎖。
7)Sync wal :HLog真正sync到HDFS,在釋放行鎖以後執行sync操做是爲了儘可能減小持鎖時間,提高寫性能。若是sync失敗,執行回滾操做將MemStore中已經寫入的數據移除。
8)結束寫事務:此時該線程的更新操做纔會對其餘讀請求可見,更新才實際生效。
3. MemStore Flush階段
隨着數據的不斷寫入,MemStore中存儲的數據會愈來愈多,系統爲了將使用的內存保持在一個合理的水平,會將MemStore中的數據寫入文件造成HFile。f lush階段是HBase的很是核心的階段,理論上須要重點關注三個問題:
•MemStore Flush的觸發時機。即在哪些狀況下HBase會觸發f lush操做。
•MemStore Flush的總體流程。
•HFile的構建流程。HFile構建是MemStore Flush總體流程中最重要的一個部分,這部份內容會涉及HFile文件格式的構建、布隆過濾器的構建、HFile索引的構建以及相關元數據的構建等。
數據寫入Region的流程能夠抽象爲兩步:追加寫入HLog,隨機寫入MemStore。
1. 追加寫入HLog
HLog保證成功寫入MemStore中的數據不會由於進程異常退出或者機器宕機而丟失,但實際上並不徹底如此,HBase定義了多個HLog持久化等級,使得用戶在數據高可靠和寫入性能之間進行權衡。
(1)HLog持久化等級
HBase能夠經過設置HLog的持久化等級決定是否開啓HLog機制以及HLog的落盤方式。HLog的持久化等級分爲以下五個等級。
• SKIP_WAL:只寫緩存,不寫HLog日誌。由於只寫內存,所以這種方式能夠極大地提高寫入性能,可是數據有丟失的風險。在實際應用過程當中並不建議設置此等級,除非確認不要求數據的可靠性。
• SYNC_WAL:同步將數據寫入日誌文件中,須要注意的是,數據只是被寫入文件系統中,並無真正落盤。HDFS Flush策略詳見HADOOP-6313。
• FSYNC_WAL:同步將數據寫入日誌文件並強制落盤。這是最嚴格的日誌寫入等級,能夠保證數據不會丟失,可是性能相對比較差。
• USER_DEFAULT:若是用戶沒有指定持久化等級,默認HBase使用SYNC_WAL等級持久化數據。
用戶能夠經過客戶端設置HLog持久化等級,代碼以下:
put.setDurability(Durability.SYNC_WAL);
(2)HLog寫入模型
在HBase的演進過程當中,HLog的寫入模型幾經改進,寫入吞吐量獲得極大提高。以前的版本中,HLog寫入都須要通過三個階段:首先將數據寫入本地緩存,而後將本地緩存寫入文件系統,最後執行sync操做同步到磁盤。
很顯然,三個階段是能夠流水線工做的,基於這樣的設想,寫入模型天然就想到「生產者-消費者」隊列實現。然而以前版本中,生產者之間、消費者之間以及生產者與消費者之間的線程同步都是由HBase系統實現,使用了大量的鎖,在寫入併發量很是大的狀況下會頻繁出現惡性搶佔鎖的問題,寫入性能較差。
當前版本中,HBase使用LMAX Disruptor框架實現了無鎖有界隊列操做。基於Disruptor的HLog寫入模型如圖所示。
Hlog寫入模型
圖中最左側部分是Region處理HLog寫入的兩個先後操做:append和sync。當調用append後,WALEdit和HLogKey會被封裝成FSWALEntry類,進而再封裝成Ring BufferTruck類放入Disruptor無鎖有界隊列中。當調用sync後,會生成一個SyncFuture,再封裝成RingBufferTruck類放入同一個隊列中,而後工做線程會被阻塞,等待notify()來喚醒。
圖最右側部分是消費者線程,在Disruptor框架中有且僅有一個消費者線程工做。這個框架會從Disruptor隊列中依次取出RingBufferTruck對象,而後根據以下選項來操做:
•若是RingBufferTruck對象中封裝的是FSWALEntry,就會執行文件append操做,將記錄追加寫入HDFS文件中。須要注意的是,此時數據有可能並無實際落盤,而只是寫入到文件緩存。
•若是RingBufferTruck對象是SyncFuture,會調用線程池的線程異步地批量刷盤,刷盤成功以後喚醒工做線程完成HLog的sync操做。
2. 隨機寫入MemStore
KeyValue寫入Region分爲兩步:首先追加寫入HLog,再寫入MemStore。MemStore使用數據結構ConcurrentSkipListMap來實際存儲KeyValue,優勢是可以很是友好地支持大規模併發寫入,同時跳躍表自己是有序存儲的,這有利於數據有序落盤,而且有利於提高MemStore中的KeyValue查找性能。
KeyValue寫入MemStore並不會每次都隨機在堆上建立一個內存對象,而後再放到ConcurrentSkipListMap中,這會帶來很是嚴重的內存碎片,進而可能頻繁觸發Full GC。HBase使用MemStore-Local Allocation Buffer(MSLAB)機制預先申請一個大的(2M)的Chunk內存,寫入的KeyValue會進行一次封裝,順序拷貝這個Chunk中,這樣,MemStore中的數據從內存f lush到硬盤的時候,JVM內存留下來的就再也不是小的沒法使用的內存碎片,而是大的可用的內存片斷。
基於這樣的設計思路,MemStore的寫入流程能夠表述爲如下3步。
1)檢查當前可用的Chunk是否寫滿,若是寫滿,從新申請一個2M的Chunk。
2)將當前KeyValue在內存中從新構建,在可用Chunk的指定offset處申請內存建立一個新的KeyValue對象。
3)將新建立的KeyValue對象寫入ConcurrentSkipListMap中。
1. 觸發條件
HBase會在如下幾種狀況下觸發flush操做。
•MemStore級別限制:當Region中任意一個MemStore的大小達到了上限(hbase.hregion.memstore.flush.size,默認128MB),會觸發MemStore刷新。
•Region級別限制:當Region中全部MemStore的大小總和達到了上限(hbase.hregion. memstore.block.multiplier *hbase.hregion.memstore.flush.size),會觸發MemStore刷新。
•RegionServer級別限制:當RegionServer中MemStore的大小總和超太低水位閾值hbase.regionserver.global.memstore.size.lower.limit*hbase.regionserver.global.memstore.size,RegionServer開始強制執行flush,先flush MemStore最大的Region,再flush次大的,依次執行。若是此時寫入吞吐量依然很高,致使總MemStore大小超太高水位閾值hbase.regionserver.global.memstore.size,RegionServer會阻塞更新並強制執行flush,直至總MemStore大小降低到低水位閾值。
•當一個RegionServer中HLog數量達到上限(可經過參數hbase.regionserver.maxlogs配置)時,系統會選取最先的HLog對應的一個或多個Region進行f lush。
•HBase按期刷新MemStore :默認週期爲1小時,確保MemStore不會長時間沒有持久化。爲避免全部的MemStore在同一時間都進行flush而致使的問題,按期的f lush操做有必定時間的隨機延時。
•手動執行flush :用戶能夠經過shell命令f lush 'tablename'或者f lush'regionname'分別對一個表或者一個Region進行f lush。
2. 執行流程
爲了減小flush過程對讀寫的影響,HBase採用了相似於兩階段提交的方式,將整個flush過程分爲三個階段。
1)prepare階段:遍歷當前Region中的全部MemStore,將MemStore中當前數據集CellSkipListSet(內部實現採用ConcurrentSkipListMap)作一個快照snapshot,而後再新建一個CellSkipListSet接收新的數據寫入。prepare階段須要添加updateLock對寫請求阻塞,結束以後會釋放該鎖。由於此階段沒有任何費時操做,所以持鎖時間很短。
2)flush階段:遍歷全部MemStore,將prepare階段生成的snapshot持久化爲臨時文件,臨時文件會統一放到目錄.tmp下。這個過程由於涉及磁盤IO操做,所以相對比較耗時。
3)commit階段:遍歷全部的MemStore,將flush階段生成的臨時文件移到指定的ColumnFamily目錄下,針對HFile生成對應的storefile和Reader,把storefile添加到Store的storef iles列表中,最後再清空prepare階段生成的snapshot。
3. 生成HFile
HBase執行f lush操做以後將內存中的數據按照特定格式寫成HFile文件,本小節將會依次介紹HFile文件中各個Block的構建流程。
(1)HFile結構
本書第5章對HBase中數據文件HFile的格式進行了詳細說明,HFile結構參見下圖。HFile依次由Scanned Block、Non-scanned Block、Load-on-open以及Trailer四個部分組成。
• Scanned Block:這部分主要存儲真實的KV數據,包括Data Block、LeafIndex Block和Bloom Block。
• Non-scanned Block:這部分主要存儲Meta Block,這種Block大多數狀況下能夠不用關心。
• Load-on-open:主要存儲HFile元數據信息,包括索引根節點、布隆過濾器元數據等,在RegionServer打開HFile就會加載到內存,做爲查詢的入口。
• Trailer:存儲Load-on-open和Scanned Block在HFile文件中的偏移量、文件大小(未壓縮)、壓縮算法、存儲KV個數以及HFile版本等基本信息。Trailer部分的大小是固定的。
MemStore中KV在f lush成HFile時首先構建Scanned Block部分,即KV寫進來以後先構建Data Block並依次寫入文件,在造成Data Block的過程當中也會依次構建造成Leaf index Block、Bloom Block並依次寫入文件。一旦MemStore中全部KV都寫入完成,Scanned Block部分就構建完成。
Non-scanned Block、Load-on-open以及Trailer這三部分是在全部KV數據完成寫入後再追加寫入的。
(2)構建"Scanned Block"部分
下圖所示爲MemStore中KV數據寫入HFile的基本流程,可分爲如下4個步驟。
KV數據寫入HFile流程圖
1)MemStore執行flush,首先新建一個Scanner,這個Scanner從存儲KV數據的CellSkipListSet中依次從小到大讀出每一個cell(KeyValue)。這裏必須注意讀取的順序性,讀取的順序性保證了HFile文件中數據存儲的順序性,同時讀取的順序性是保證HFile索引構建以及布隆過濾器Meta Block構建的前提。
2)appendGeneralBloomFilter :在內存中使用布隆過濾器算法構建BloomBlock,下文也稱爲Bloom Chunk。
3)appendDeleteFamilyBloomFilter :針對標記爲"DeleteFamily"或者"DeleteFamilyVersion"的cell,在內存中使用布隆過濾器算法構建BloomBlock,基本流程和appendGeneralBloomFilter相同。
4)(HFile.Writer)writer.append :將cell寫入Data Block中,這是HFile文件構建的核心。
(3)構建Bloom Block
圖爲Bloom Block構建示意圖,實際實現中使用chunk表示Block概念,二者等價。
構建Bloom Block
布隆過濾器內存中維護了多個稱爲chunk的數據結構,一個chunk主要由兩個元素組成:
•一塊連續的內存區域,主要存儲一個特定長度的數組。默認數組中全部位都爲0,對於row類型的布隆過濾器,cell進來以後會對其rowkey執行hash映射,將其映射到位數組的某一位,該位的值修改成1。
•firstkey,第一個寫入該chunk的cell的rowkey,用來構建Bloom IndexBlock。
cell寫進來以後,首先判斷當前chunk是否已經寫滿,寫滿的標準是這個chunk容納的cell個數是否超過閾值。若是超過閾值,就會從新申請一個新的chunk,並將當前chunk放入ready chunks集合中。若是沒有寫滿,則根據布隆過濾器算法使用多個hash函數分別對cell的rowkey進行映射,並將相應的位數組位置爲1。
(4)構建Data Block
一個cell在內存中生成對應的布隆過濾器信息以後就會寫入Data Block,寫入過程分爲兩步。
1)Encoding KeyValue :使用特定的編碼對cell進行編碼處理,HBase中主要的編碼器有DiffKeyDeltaEncoder、FastDiffDeltaEncoder以及PrefixKeyDeltaEncoder等。編碼的基本思路是,根據上一個KeyValue和當前KeyValue比較以後取delta,展開講就是rowkey、column family以及column分別進行比較而後取delta。假如先後兩個KeyValue的rowkey相同,當前rowkey就可使用特定的一個f lag標記,不須要再完整地存儲整個rowkey。這樣,在某些場景下能夠極大地減小存儲空間。
2)將編碼後的KeyValue寫入DataOutputStream。
隨着cell的不斷寫入,當前Data Block會由於大小超過閾值(默認64KB)而寫滿。寫滿後Data Block會將DataOutputStream的數據f lush到文件,該Data Block此時完成落盤。
(5)構建Leaf Index Block
Data Block完成落盤以後會馬上在內存中構建一個Leaf Index Entry對象,並將該對象加入到當前Leaf Index Block。Leaf Index Entry對象有三個重要的字段。
• firstKey:落盤Data Block的第一個key。用來做爲索引節點的實際內容,在索引樹執行索引查找的時候使用。
• blockOffset:落盤Data Block在HFile文件中的偏移量。用於索引目標肯定後快速定位目標Data Block。
• blockDataSize:落盤Data Block的大小。用於定位到Data Block以後的數據加載。
Leaf Index Entry的構建如圖所示。
一樣,Leaf Index Block會隨着Leaf Index Entry的不斷寫入慢慢變大,一旦大小超過閾值(默認64KB),就須要f lush到文件執行落盤。須要注意的是,LeafIndex Block落盤是追加寫入文件的,因此就會造成HFile中Data Block、LeafIndex Block交叉出現的狀況。
和Data Block落盤流程同樣,Leaf Index Block落盤以後還須要再往上構建RootIndex Entry並寫入Root Index Block,造成索引樹的根節點。可是根節點並無追加寫入"Scanned block"部分,而是在最後寫入"Load-on-open"部分。
能夠看出,HFile文件中索引樹的構建是由低向上發展的,先生成Data Block,再生成Leaf Index Block,最後生成Root Index Block。而檢索rowkey時恰好相反,先在Root Index Block中查詢定位到某個Leaf Index Block,再在Leaf IndexBlock中二分查找定位到某個Data Block,最後將Data Block加載到內存進行遍歷查找。
(6)構建Bloom Block Index
完成Data Block落盤還有一件很是重要的事情:檢查是否有已經寫滿的BloomBlock。若是有,將該Bloom Block追加寫入文件,在內存中構建一個BloomIndex Entry並寫入Bloom Index Block。
整個流程與Data Block落盤後構建Leaf Index Entry並寫入Leaf Index Block的流程徹底同樣。在此再也不贅述。
基本流程總結:flush階段生成HFile和Compaction階段生成HFile的流程徹底相同,不一樣的是,flush讀取的是MemStore中的KeyValue寫成HFile,而Compaction讀取的是多個HFile中的KeyValue寫成一個大的HFile,KeyValue來源不一樣。KeyValue數據生成HFile,首先會構建Bloom Block以及Data Block,一旦寫滿一個Data Block就會將其落盤同時構造一個Leaf Index Entry,寫入LeafIndex Block,直至Leaf Index Block寫滿落盤。實際上,每寫入一個KeyValue就會動態地去構建"Scanned Block"部分,等全部的KeyValue都寫入完成以後再靜態地構建"Non-scanned Block"部分、"Load on open"部分以及"Trailer"部分。
在實踐過程當中,flush操做的不一樣觸發方式對用戶請求影響的程度不盡相同。正常狀況下,大部分MemStore Flush操做都不會對業務讀寫產生太大影響。好比系統按期刷新MemStore、手動執行flush操做、觸發MemStore級別限制、觸發HLog數量限制以及觸發Region級別限制等,這幾種場景只會阻塞對應Region上的寫請求,且阻塞時間較短。
然而,一旦觸發RegionServer級別限制致使f lush,就會對用戶請求產生較大的影響。在這種狀況下,系統會阻塞全部落在該RegionServer上的寫入操做,直至MemStore中數據量下降到配置閾值內。
文章基於《HBase原理與實踐》一書