HBase存儲架構圖
HBase Master
- 為Region server分配region
- 負責Region server的負載均衡
- 發現失效的Region server并重新分配其上的region
- HDFS上的垃圾文件回收(刪除表后的遺留文件)
- 處理schema更新請求(對表的增刪改查)
HBase RegionServer
- 維護master分配給他的region,處理對這些region的io請求
- 負責切分正在運行過程中變的過大的region
HBase 數據讀取過程
Hbase里有一張特殊的元數據表"hbase:meta",它保存著hbase集群中所有region所處的主機位置信息。而zookeeper中保存這這張表所在的主機位置信息。
region定位過程
- client首先從zookeeper獲取meta表所在的region server
- client查詢出meta表中包含所需查詢key范圍的region所在region server。
同時client會緩存下region與region server的映射信息,避免重復查詢。如果region由于split或者balancing等原因改變了對應的region server ,則client會重新從zk中查詢一遍,并再次緩存。
- 連接此region server,查詢。
client與region server查詢交互
- 查詢region server的讀緩存BlockCache 是否存在rowkey對應數據,如果有就返回,沒有的話就行進行第二步查詢。
- 查詢memstore(一個按key排序的樹形結構的緩沖區),即寫內存是否存儲有待查rowkey數據,如果有就返回,沒有進行第三步查詢;
- 用Block Cache indexes和bloom filters加載對應的HFile,根據rowkey遍歷查詢數據,不管有沒有都返回到client。
HBase 數據寫入過程
HBase WAL(write ahead log)
一個regionserver上所有的region共享一個HLog,一次數據的提交是先寫WAL,再寫memstore
HLog類
實現了WAL的類叫做HLog,當hregion被實例化時,HLog實例會被當做一個參數傳到HRegion的構造器中,當一個Region接收到一個更新操作時,它可以直接把數據保存到一個共享的WAL實例中去.
HLogKey類
1、當前的WAL使用的是hadoop的sequencefile格式,其key是HLogKey實例。HLogKey中記錄了寫入數據的歸屬信息,除了table和region名字外,同時還包括sequence number和timestamp,timestamp是“寫入時間“,sequence number的起始值為0,或者是最近一次存入文件系統中sequence number。Region打開存儲文件,讀取每個HFile中的最大的sequence number,如果該值大于HLog 的sequence number, 就將它作為HLog 的sequence number的值。最后當讀取了所有hfile的sequence number,hlog也就獲得了最近一次數據持久化的位置。
2、HLog sequence File的value是HBase的KeyValue對象,即對應HFile中的KeyValue
WALEdit類
1、客戶端發送的每個修改都會封裝成WALEdit類,一個WALEdit類包含了多個更新操作,可以說一個WALEdit就是一個原子操作,包含若干個操作的集合
LogSyncer類
1、Table在創建的時候,有一個參數可以設置,是否每次寫Log日志都需要往集群的其他機器同步一次,默認是每次都同步,同步的開銷是比較大的,但不及時同步又可能因為機器宕而丟日志。同步的操作現在是通過pipeline的方式來實現的,pipeline是指datanode接收數據后,再傳給另外一臺datanode,是一種串行的方式,n-Way writes是指多datanode同時接收數據,最慢的一臺結束就是整個結束,差別在于一個延遲大,一個并發高,hdfs現在正在開發中,以便可以選擇是按pipeline還是n-way writes來實現寫操作
2、Table如果設置每次不同步,則寫操作會被RegionServer緩存,并啟動一個LogSyncer線程來定時同步日志。
hbase.regionserver.optionallogflushinterval:將Hlog同步到HDFS的間隔。如果Hlog沒有積累到一定的數量,到了時間,也會觸發同步。默認是1秒,單位毫秒。
LogRoller類
- 日志寫入的大小是有限制的,LogRoller類會作為一個后臺線程運行,在特定的時間間隔內滾動日志,通過hbase.regionserver.logroll.period屬性控制,默認1小時。所以每60分鐘,會打開一個新的log文件。久而久之,會有一大堆的文件需要維護。首先,LogRoller調用HLog.rollWriter(),定時滾動日志,之后,利用HLog.cleanOldLogs()可以清除舊的日志。它首先取得所有存儲文件HFile中的最大的sequence number,之后檢查是否存在一個log所有的條目的“sequence number”均低于這個值,如果存在,將刪除這個log。
- log file的數目超過了log files的最大值。這時,會強制調用flush out 以減少log的數目。
“hbase.regionserver.hlog.blocksize”和“hbase.regionserver.logroll.multiplier”兩個參數默認將在log大小為SequenceFile(默認為64MB)的95%時回滾。所以,log的大小和log使用的時間都會導致回滾,以先到達哪個限定為準。
HBase Meta Table
hbase meta 表保存所有hbase集群中所有的region信息
- regionId = 創建region時的timestamp+"."+encode值(舊版hbase的regionId只有時間戳)+"."
- name = tablename+","+startKey+","+regionId(等同于rowkey)
- startKey,region的開始key,第一個region的startKey是空字符串
- endKey,region的結束key,最后一個region的endKey是空字符串。當startKey,endKey都為空則表示只有一個region
- encoded(Hash值),該值會作為hdfs文件系統中對應region的目錄名
- serverstartcode 是服務開始的時候的timestamp
- server 指服務器的地址和端口
- seqnumDuringOpen:?
hbase原先的設計還含有一張-ROOT-表,用來保存meta表的region信息。HBase 0.96 版本移除了這個特性(HBASE-3171),并且設置meta表再大的數據量也不會像普通表一樣進行split,因此meta表總是只有一個region(HBASE-2415)。
HBase的后臺合并
小合并:把小HFile合并成一個大HFile,這樣可以避免在讀一行的時候引用過多文件,提升讀性能。在執行合并的時候,HBase讀出已有的多個HFile內容,并把記錄寫入一個新文件。然后把新文件設置為激活狀態,刪除所有老文件,它會占用大量的磁盤和網絡IO,但相比大合并還是輕量級的,可以頻繁發生。
大合并:處理給定region的一個列族的所有HFile,將這個列族所有的HFile合并成一個文件。這個動作相當耗費資源,可以從Shell中手工觸發大合并,這也是清理被刪除記錄的唯一機會。
從合并的操作可以看出,HBase其實不適合存儲經常刪改的數據,因為刪除的記錄在大合并前依舊占用空間,而大合并又十分耗費資源。
HBase的Delete命令并不立即刪除內容,而是針對那個內容寫入一條新的刪除標記,這個刪除標記叫做“墓碑”(tombstone)。被標記的內容不能在Get和Scan操作中返回結果。作為磁盤文件,HFile在非合并的時候是不能被改變的。且因為“墓碑”記錄并不一定和被刪除的記錄在同一個HFile里面,所以HFile只有在執行一次大合并的時候才會處理墓碑記錄,被刪除記錄占用的空間才會被釋放。
Hbase Memstore&Flush
用到Memstore最主要的原因是:存儲在HDFS上的數據需要按照row key 排序。而HDFS本身被設計為順序讀寫(sequential reads/writes),不允許修改。這樣的話,HBase就不能夠高效的寫數據,因為要寫入到HBase的數據不會被排序,這也就意味著沒有為將來的檢索優化。為了解決這個問題,HBase將最近接收到的數據緩存在內存中(in Memstore),在持久化到HDFS之前完成排序,然后再快速的順序寫入HDFS。
除了解決“無序”問題外,Memstore還有一些其他的好處:
- 作為一個內存級緩存,緩存最近增加數據。一種顯而易見的場合是,新插入數據總是比老數據頻繁使用。
- 在持久化寫入之前,在內存中對Rows/Cells可以做某些優化。比如,當數據的version被設為1的時候,對于某些CF的一些數據,Memstore緩存了數個對該Cell的更新,在寫入HFile的時候,僅需要保存一個最新的版本就好了,其他的都可以直接拋棄。
每一次Memstore的flush,會為每一個ColumnFamily創建一個新的HFile。
MemStore的最小flush單元是Region而不是單個MemStore。可想而知,如果一個Region中Memstore過多,當其中某一個Memstore滿足flush條件,則該region對應的所有Memstore都會被flush,這樣每次flush的開銷必然會很大,因此我們也建議在進行表設計的時候盡量減少ColumnFamily的個數。
Flush操作如果只選擇某個Region的Store內的MemStore寫入磁盤,而不是統一寫入磁盤,那么HLog上key的一致性在Region中各個Store(ColumnFamily)下的MemStore內就會有不一致的key區間。
如下圖所示,我們假定該RegionServer上僅有一個Region,由于不同的Row在列簇上有所區別,就會出現有些不同Store內占用的內存不一致的情況,這里會根據整體內存使用的情況,或者RS使用內存的情況來決定是否執行Flush操作。如果僅僅flush使用內存較大的memstore,那么在使用的過程中,一是Scan操作在執行時就不夠統一(同一個rowkey的cf有些flush有些還沒flush),二是在HLog Replayer還原Region內Memstore故障前的狀態,不能簡單的根據Hlog的Flush_marker的標記位來執行Replay。
Memstore Flush觸發條件
1. Memstore級別限制:當Region中任意一個MemStore的大小達到了上限(hbase.hregion.memstore.flush.size,默認128MB),會觸發Memstore刷新。
2. Region級別限制:當Region中所有Memstore的大小總和達到了上限(hbase.hregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size,默認 2 x 128M = 256M),會觸發memstore刷新。
3. Region Server級別限制:當一個Region Server中所有Memstore的大小總和達到了上限(hbase.regionserver.global.memstore.upperLimit * hbase_heapsize,默認 40%的JVM內存使用量),會觸發部分Memstore刷新。Flush順序是按照Memstore由大到小執行,先Flush Memstore最大的Region,再執行次大的,直至總體Memstore內存使用量低于閾值(hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize,默認38%的JVM內存使用量)。
4. 當一個Region Server中HLog數量達到上限(可通過參數hbase.regionserver.max.logs配置)時,系統會選取最早的一個 HLog對應的一個或多個Region進行flush
5. HBase定期刷新Memstore:默認周期為1小時,確保Memstore不會長時間沒有持久化。為避免所有的MemStore在同一時間都進行flush導致的問題,定期的flush操作有20000左右的隨機延時。
6. 手動執行flush:用戶可以通過shell命令 flush ‘tablename’或者flush ‘region name’分別對一個表或者一個Region進行flush。
Memstore Flush流程
為了減少flush過程對讀寫的影響,HBase采用了類似于兩階段提交的方式,將整個flush過程分為三個階段:
- prepare階段:遍歷當前Region中的所有Memstore,將Memstore中當前數據集kvset做一個快照snapshot,然后再新建一個新的kvset。后期的所有寫入操作都會寫入新的kvset中,而整個flush階段讀操作會首先分別遍歷kvset和snapshot,如果查找不到再會到HFile中查找。prepare階段需要加一把updateLock對寫請求阻塞,結束之后會釋放該鎖。因為此階段沒有任何費時操作,因此持鎖時間很短。
- flush階段:遍歷所有Memstore,將prepare階段生成的snapshot持久化為臨時文件,臨時文件會統一放到目錄.tmp下。這個過程因為涉及到磁盤IO操作,因此相對比較耗時。
- commit階段:遍歷所有的Memstore,將flush階段生成的臨時文件移到指定的ColumnFamily目錄下,針對HFile生成對應的storefile和Reader,把storefile添加到HStore的storefiles列表中,最后再清空prepare階段生成的snapshot。
上述flush流程可以通過日志信息查看:
/******* prepare階段 ********/
2016-02-04 03:32:41,516 INFO [MemStoreFlusher.1] regionserver.HRegion: Started memstore flush for sentry_sgroup1_data,{\xD4\x00\x00\x01|\x00\x00\x03\x82\x00\x00\x00?\x06\xDA`\x13\xCAE\xD3C\xA3:_1\xD6\x99:\x88\x7F\xAA_\xD6[L\xF0\x92\xA6\xFB^\xC7\xA4\xC7\xD7\x8Fv\xCAT\xD2\xAF,1452217805884.572ddf0e8cf0b11aee2273a95bd07879., current region memstore size 128.9 M
/******* flush階段 ********/
2016-02-04 03:32:42,423 INFO [MemStoreFlusher.1] regionserver.DefaultStoreFlusher: Flushed, sequenceid=1726212642, memsize=128.9 M, hasBloomFilter=true, into tmp file hdfs://hbase1/hbase/data/default/sentry_sgroup1_data/572ddf0e8cf0b11aee2273a95bd07879/.tmp/021a430940244993a9450dccdfdcb91d
/******* commit階段 ********/
2016-02-04 03:32:42,464 INFO [MemStoreFlusher.1] regionserver.HStore: Added hdfs://hbase1/hbase/data/default/sentry_sgroup1_data/572ddf0e8cf0b11aee2273a95bd07879/d/021a430940244993a9450dccdfdcb91d, entries=643656, sequenceid=1726212642, filesize=7.1 M
其中第二階段flush又細分為兩個階段
- append階段:memstore中keyvalue首先會寫入到HFile中數據塊
- finalize階段:修改HFlie中meta元數據塊,索引數據塊以及Trailer數據塊等
append流程
具體keyvalue數據的append以及finalize過程在HFileWriterV2文件中,其中append流程可以大體表征為:
Block Write.png
a. 預檢查:檢查key的大小是否大于前一個key,如果大于則不符合HBase順序排列的原理,拋出異常;檢查value是否是null,如果為null也拋出異常
b. block是否寫滿:檢查當前Data Block是否已經寫滿,如果沒有寫滿就直接寫入keyvalue;否則就需要執行數據塊落盤以及索引塊修改操作;
c. 數據落盤并修改索引:如果DataBlock寫滿,首先將block塊寫入流;再生成一個leaf index entry,寫入leaf Index block;再檢查該leaf index block是否已經寫滿需要落盤,如果已經寫滿,就將該leaf index block寫入到輸出流,并且為索引樹根節點root index block新增一個索引,指向葉子節點(second-level index)
d. 生成一個新的block:重新reset輸出流,初始化startOffset為-1
e. 寫入keyvalue:將keyvalue以流的方式寫入輸出流,同時需要寫入memstore;除此之外,如果該key是當前block的第一個key,需要賦值給變量firstKeyInBlock
finalize階段
memstore中所有keyvalue都經過append階段輸出到HFile后,會執行一次finalize過程,主要更新HFile中meta元數據塊、索引數據塊以及Trailer數據塊,其中對索引數據塊的更新是我們關心的重點,此處詳細解析,上述append流程中c步驟’數據落盤并修改索引’會使得root index block不斷增多,當增大到一定程度之后就需要分裂,分裂示意圖如下圖所示:
Index Split.png
上圖所示,分裂前索引結構為second-level結構,圖中沒有畫出Data Blocks,根節點索引指向葉子節點索引塊。finalize階段系統會對Root Index Block進行大小檢查,如果大小大于規定的大小就需要進行分裂,圖中分裂過程實際上就是將原來的Root Index Block塊分割成4塊,每塊獨立形成中間節點InterMediate Index Block,系統再重新生成一個Root Index Block(圖中紅色部分),分別指向分割形成的4個interMediate Index Block。此時索引結構就變成了third-level結構。
Hfile文件格式
HFile的核心設計思想是(數據)分塊和(索引)分級
如上圖所示, HFile會被切分為多個大小相等的block塊,每個block的大小可以在創建表列簇的時候通過參數blocksize => ‘65535’進行指定,默認為64k,大號的Block有利于順序Scan,小號Block利于隨機查詢,因而需要權衡。而且所有block塊都擁有相同的數據結構,如圖左側所示,HBase將block塊抽象為一個統一的HFileBlock。HFileBlock支持兩種類型,一種類型不支持checksum,一種不支持。
HFile V2中,主要包括四個部分:
- Scanned Block(數據block,表示順序掃描HFile時所有的數據塊將會被讀取,包括Leaf Index Block和Bloom Block)
- Non-Scanned block(元數據block,表示在HFile順序掃描的時候數據不會被讀取,主要包括Meta Block和Intermediate Level Data Index Blocks兩部分)
- Load-on-open(這部分數據在HBase的region server啟動時需要加載到內存中,包括FileInfo、Bloom filter block、data block index和meta block index)
- trailer(文件尾,主要記錄了HFile的基本信息、各個部分的偏移值和尋址信息。)。
一個HFile文件包含了多種類型的HFileBlock塊,每種類型的HFileBlock主要包括兩部分:BlockHeader和BlockData。其中Header主要存儲block元數據,Data用來存儲具體數據。block元數據中最核心的字段是BlockType字段,用來標示該block塊的類型,HBase中定義了8種BlockType,每種BlockType對應的block都存儲不同的數據內容,有的存儲用戶數據,有的存儲索引數據,有的存儲meta元數據。對于任意一種類型的HFileBlock,都擁有相同結構的BlockHeader,但是BlockData結構卻不相同。下面通過一張表簡單羅列最核心的幾種BlockType:
-
Trailer Block
主要記錄了HFile的基本信息、各個部分的偏移值和尋址信息,下圖為Trailer內存和磁盤中的數據結構,其中只顯示了部分核心字段:
Trailer Block.png
HFile在讀取的時候首先會解析Trailer Block并加載到內存,然后再進一步加載LoadOnOpen區的數據,具體步驟如下:
- 首先加載version版本信息,HBase中version包含majorVersion和minorVersion兩部分,前者決定了HFile的主版本: V1、V2 還是V3;后者在主版本確定的基礎上決定是否支持一些微小修正,比如是否支持checksum等。不同的版本決定了使用不同的Reader對象對HFile進行讀取解析
- 根據Version信息獲取trailer的長度(不同version的trailer長度不同),再根據trailer長度加載整個HFileTrailer Block
- 最后加載load-on-open部分到內存中,起始偏移地址是trailer中的LoadOnOpenDataOffset字段,load-on-open部分的結束偏移量為HFile長度減去Trailer長度,load-on-open部分主要包括索引樹的根節點以及FileInfo兩個重要模塊,FileInfo是固定長度的塊,它紀錄了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY, COMPARATOR, MAX_SEQ_ID_KEY等;
-
Data Block
DataBlock是HBase中用戶數據存儲的最小單元。DataBlock中主要存儲用戶的KeyValue數據(KeyValue后面一般會跟一個timestamp,圖中未標出),而KeyValue結構是HBase存儲的核心,每個數據都是以KeyValue結構在HBase中進行存儲。KeyValue結構在內存和磁盤中可以表示為:
Data Block.png
每個KeyValue都由4個部分構成,分別為key length,value length,key和value。其中key value和value length是兩個固定長度的數值,而key是一個復雜的結構,首先是rowkey的長度,接著是rowkey,然后是ColumnFamily的長度,再是ColumnFamily,最后是時間戳和KeyType(keytype有四種類型,分別是Put、Delete、 DeleteColumn和DeleteFamily),value就沒有那么復雜,就是一串純粹的二進制數據。
-
BloomFilter Metadata Block & Bloom Block
BloomFilter對于HBase的隨機讀性能至關重要,對于get操作以及部分scan操作可以剔除掉不會用到的HFile文件,減少實際IO次數,提高隨機讀性能。在此簡單地介紹一下Bloom Filter的工作原理,Bloom Filter使用位數組來實現過濾,初始狀態下位數組每一位都為0,如下圖所示:
BloomFilter-1.png
假如此時有一個集合S = {x1, x2, … xn},Bloom Filter使用k個獨立的hash函數,分別將集合中的每一個元素映射到{1,…,m}的范圍。對于任何一個元素,被映射到的數字作為對應的位數組的索引,該位會被置為1。比如元素x1被hash函數映射到數字8,那么位數組的第8位就會被置為1。下圖中集合S只有兩個元素x和y,分別被3個hash函數進行映射,映射到的位置分別為(0,2,6)和(4,7,10),對應的位會被置為1:
BloomFilter-2.png
現在假如要判斷另一個元素是否是在此集合中,只需要被這3個hash函數進行映射,查看對應的位置是否有0存在,如果有的話,表示此元素肯定不存在于這個集合,否則有可能存在。下圖所示就表示z肯定不在集合{x,y}中:
BloomFilter-3.png
HBase中每個HFile都有對應的位數組,KeyValue在寫入HFile時會先經過幾個hash函數的映射,映射后將對應的數組位改為1,get請求進來之后再進行hash映射,如果在對應數組位上存在0,說明該get請求查詢的數據肯定不在該HFile中。
HFile中的位數組就是上述Bloom Block中存儲的值,可以想象,一個HFile文件越大,里面存儲的KeyValue值越多,位數組就會相應越大。一旦太大就不適合直接加載到內存了,因此HFile V2在設計上將位數組進行了拆分,拆成了多個獨立的位數組(根據Key進行拆分,一部分連續的Key使用一個位數組)。這樣一個HFile中就會包含多個位數組,根據Key進行查詢,首先會定位到具體的某個位數組,只需要加載此位數組到內存進行過濾即可,減少了內存開支。
在結構上每個位數組對應HFile中一個Bloom Block,為了方便根據Key定位具體需要加載哪個位數組,HFile V2又設計了對應的索引Bloom Index Block,對應的內存和邏輯結構圖如下:
Bloom Index Block.png
Bloom Index Block結構中totalByteSize表示位數組的bit數,numChunks表示Bloom Block的個數,hashCount表示hash函數的個數,hashType表示hash函數的類型,totalKeyCount表示bloom filter當前已經包含的key的數目,totalMaxKeys表示bloom filter當前最多包含的key的數目, Bloom Index Entry對應每一個bloom filter block的索引條目,作為索引分別指向"scanned block section" 部分的Bloom Block,Bloom Block中就存儲了對應的位數組。
Bloom Index Entry的結構見上圖左邊所示,BlockOffset表示對應Bloom Block在HFile中的偏移量,FirstKey表示對應BloomBlock的第一個Key。根據上文所說,一次get請求進來,首先會根據key在所有的索引條目中進行二分查找,查找到對應的Bloom Index Entry,就可以定位到該key對應的位數組,加載到內存進行過濾判斷。 Index Block
HFile V1的時候,在數據塊索引很大時,很難全部load到內存。假設每個數據塊使用默認大小64KB,每個索引項64Byte,這樣如果每臺及其上存放了60TB的數據,那索引數據就得有60G,所以內存的占用還是很高的。此外,由于直到加載完所有塊索引數據之后,才能認為region啟動完成,因此這樣的塊索引大小會明顯地拖慢region的啟動速度。所以,將這些索引以樹狀結構進行組織,只讓頂層索引常駐內存,其他索引按需讀取并通過LRU cache進行緩存,這樣就不用全部加載到內存了。
HFile中索引結構根據索引層級的不同分為兩種:single-level和mutil-level,前者表示單層索引,后者表示多級索引,一般為兩級或三級。HFile V1版本中只有single-level一種索引結構,V2版本中引入多級索引。
HFile V2版本Index Block則分為兩類:Root Index Block和NonRoot Index Block,其中NonRoot Index Block又分為Intermediate Index Block和Leaf Index Block兩種。HFile中索引結構類似于一棵樹,Root Index Block表示索引數根節點,記錄每個塊首個key及其索引,Intermediate Index Block表示中間節點,記錄每個塊最后的key及其索引,Leaf Index block表示葉子節點,葉子節點直接指向實際數據塊。隨著dateblock數量的不斷增多,(root_index-->intermediate_index-->leaf_index-->data_block), 索引的層級會逐漸增多。
HFile中除了Data Block需要索引之外,上面提到的Bloom Block也需要索引,Bloom Block的索引結構實際上就是采用了single-level結構,是一種Root Index Block。
Root Index Block
Root Index Block表示索引樹根節點索引塊,可以作為bloom的直接索引,也可以作為data索引的根索引。而且對于single-level和mutil-level兩種索引結構對應的Root Index Block略有不同,這里以mutil-level的Root Index Block索引結構為例進行分析,在內存和磁盤中的格式如下圖所示:
其中Index Entry表示具體的索引對象,每個索引對象由3個字段組成,Block Offset表示索引指向數據塊的偏移量,BlockDataSize表示索引指向數據塊在磁盤上的大小,BlockKey表示索引指向數據塊中的第一個key。除此之外,還有另外3個字段用來記錄MidKey的相關信息,MidKey表示HFile所有Data Block中中間的一個Data Block,用于在對HFile進行split操作時,快速定位HFile的中間位置。需要注意的是single-level索引結構和mutil-level結構相比,就只缺少MidKey這三個字段。
Root Index Block會在HFile解析的時候直接加載到內存中,此處需要注意在Trailer Block中有一個字段為dataIndexCount,就表示此處Index Entry的個數。因為Index Entry并不定長,只有知道Entry的個數才能正確的將所有Index Entry加載到內存。
NonRoot Index Block
當HFile中Data Block越來越多,single-level結構的索引已經不足以支撐所有數據都加載到內存,需要分化為mutil-level結構。mutil-level結構中NonRoot Index Block作為中間層節點或者葉子節點存在,無論是中間節點還是葉子節點,其都擁有相同的結構,如下圖所示:
和Root Index Block相同,NonRoot Index Block中最核心的字段也是Index Entry,用于指向葉子節點塊或者數據塊。不同的是,NonRoot Index Block結構中增加了block塊的內部索引entry Offset字段,entry Offset表示index Entry在該block中的相對偏移量(相對于第一個index Entry),用于實現block內的二分查找。所有非根節點索引塊,包括Intermediate index block和leaf index block,在其內部定位一個key的具體索引并不是通過遍歷實現,而是使用二分查找算法,這樣可以更加高效快速地定位到待查找key。
在HFile V2中數據完整索引流程:
- 先在內存中對HFile的root索引進行二分查找,如果支持多級索引,則定位到leaf index/intermediate index,如果是單級索引,則定位到數據塊data block;
- 如果支持多級索引,則會從cache/hdfs中讀取leaf/intermediate index chunk,在leaf/intermediate chunk根據key值進行二分查找(leaf/intermediate index chunk支持二分查找),找到對應的data block。
- 從cache/hdfs中讀取數據塊;
- 在數據塊中遍歷查找對應的數據。
圖中紅線表示一次查詢的索引過程(HBase中相關類為HFileBlockIndex和HFileReaderV2),基本流程可以表示為:
- 用戶輸入rowkey為fb,在root index block中通過二分查找定位到fb在’a’和’m’之間,因此需要訪問索引’a’指向的中間節點。因為root index block常駐內存,所以這個過程很快。
- 將索引’a’指向的中間節點索引塊加載到內存,然后通過二分查找定位到fb在index ‘d’和’h’之間,接下來訪問索引’d’指向的葉子節點。
- 同理,將索引’d’指向的中間節點索引塊加載到內存,一樣通過二分查找定位找到fb在index ‘f’和’g’之間,最后需要訪問索引’f’指向的數據塊節點。
- 將索引’f’指向的數據塊加載到內存,通過遍歷的方式找到對應的keyvalue。
上述流程中因為中間節點、葉子節點和數據塊都需要加載到內存,所以io次數正常為3次。但是實際上HBase為block提供了緩存機制,可以將頻繁使用的block緩存在內存中,可以進一步加快實際讀取過程。所以,在HBase中,通常一次隨機讀請求最多會產生3次io,如果數據量小(只有一層索引),數據已經緩存到了內存,就不會產生io。
HBase表誤刪恢復
hdfs的回收站機制
在hdfs上有一個回收站的設置,可以將刪除的數據移動到回收站目錄/user/$<username>/.Trash/中,設置回收站的相關參數如下:
- fs.trash.interval=360
以分鐘為單位的垃圾回收時間,垃圾站中數據超過此時間,會被刪除。如果是0,垃圾回收機制關閉。可以配置在服務器端和客戶端。如果在服務器端配置trash無效,會檢查客戶端配置。如果服務器端配置有效,客戶端配置會忽略。也就是說,Server端的值優先于Client。如有同名文件被刪除,會給文件順序編號,例如:a.txt,a.txt(1) - fs.trash.checkpoint.interval=0
以分鐘為單位的垃圾回收檢查間隔。應該小于或等于fs.trash.interval,如果是0,值等同于fs.trash.interval。該值只在服務器端設置。
如果disable+drop誤刪了hbase表數據,數據不會放到回收站中,hbase有自己的一套刪除策略。
HBase的數據主要存儲在分布式文件系統HFile和HLog兩類文件中。Compaction操作會將合并完的不用的小Hfile移動到<.archive>文件夾,并設置ttl過期時間。HLog文件在數據完全flush到hfile中時便會過期,被移動到.oldlog(oldWALs)文件夾中。
HMaster上的定時線程HFileCleaner/LogCleaner周期性掃描.archive目錄和.oldlog目錄, 判斷目錄下的HFile或者HLog是否可以被刪除,如果可以,就直接刪除文件。
關于hfile文件和hlog文件的過期時間,其中涉及到兩個參數,如下:
(1)hbase.master.logcleaner.ttl
HLog在.oldlogdir目錄中生存的最長時間,過期則被Master的線程清理,默認是600000(ms);
(2)hbase.master.hfilecleaner.plugins
HFile的清理插件列表,逗號分隔,被HFileService調用,可以自定義,默認org.apache.hadoop.hbase.master.cleaner.TimeToLiveHFileCleaner。
默認hfile的失效時間是5分鐘(300000ms)。由于一般的hadoop平臺默認都沒有對該參數的設置,可以在配置選項中添加對hbase.master.hfilecleaner.ttl的設置。
實際在測試的過程中,刪除一個hbase表,在hbase的hdfs目錄下的archive文件夾中,會立即發現刪除表的所有region數據(不包含regioninfo、tabledesc等元數據文件),超時5分鐘所有region(hfile)數據被刪除。
恢復步奏:
-
搶救數據
保證在刪除表之后的5分鐘之內將hdfs目錄/apps/hbase/data/archive/文件夾下的相關數據拷貝一份到另外一個安全目錄下。 - 新建與刪除表同名和同列族的表
- 將搶救下來的region數據拷貝到hbase表對應的目錄下
hadoop fs -cp /apps/hbase/data/archive/data/default/member/0705a8ce0ead4618839b4c9cf9977fa5 /apps/hbase/data/data/default/member/
-
hbase元數據修復
因為被刪除的數據文件夾中并沒有包含.regioninfo文件,需要進行元數據修復
hbase hbck -repair #一次可能不成功,多試幾次。
hbase元數據損壞
Hbase的一些啟動必要的文件放置在hdfs上,由于人為刪除了hdfs的數據塊文件,這些文件塊恰好包含了hbase的文件數據,所以導致hbase啟動失敗。
- 停止hbase服務
- 查看hdfs的文件健康狀態: hdfs fsck / | egrep -v '^.+$' | grep -v replica | grep -v Replica
- 根據輸出列出的所有損壞的文件塊,由于已被刪除無法恢復,所以清理hdfs中損壞或缺失的數據塊。hdfs fsck -delete 或者 hdfs fs -rm /test/xxx.txt ...
- 再次復查hdfs的文件健康狀態, 結果顯示HEALTHY。
- 備份hbase,hadoop fs -mv /apps/hbase /apps/hbasebak
- 登錄 zookeeper ,/usr/lib/zookeeper/zkCli.sh 刪除其中的hbase目錄 rmr /hbase
- 啟動hbase服務,確認所有相關服務都正常
hbase的行鎖與多版本并發控制(MVCC)
http://my.oschina.net/u/189445/blog/597226