HFile是參照谷歌的SSTable存儲格式進行設計的。全部的數據記錄都是通過它來完畢持久化,其內部主要採用分塊的方式進行存儲,如圖所看到的:

每一個HFile內部包括多種不同類型的塊結構,這些塊結構從邏輯上來講可歸並為兩類。分別用於數據存儲和數據索引(簡稱數據塊和索引塊),當中數據塊包括:
(1) DATA_BLOCK:存儲表格數據
(2) BLOOM_CHUNK:存儲布隆過濾器的位數組信息
(3) META_BLOCK:存儲元數據信息
(4) FILE_INFO:存儲HFile文件信息
索引塊包括:
表格數據索引塊(ROOT_INDEX、INTERMEDIATE_INDEX、LEAF_INDEX)
在早期的HFile版本號中(version-1),表格數據是採用單層索引結構進行存儲的。這樣當數據量上升到一定規模時,索引數據便會消耗大量內存,導致的結果是Region載入效率低下(A region is not considered opened until all of its block index data is loaded)。
因此在version-2版本號中。索引數據採用多層結構進行存儲,載入HFile時僅僅將根索引(ROOT_INDEX)數據載入內存,中間索引(INTERMEDIATE_INDEX)和葉子索引(LEAF_INDEX)在讀取數據時按需載入,從而提高了Region的載入效率。
元數據索引塊(META_INDEX)
新版本號的元數據索引依舊是單層結構,通過它來獲取元數據塊信息。
布隆索引信息塊(BLOOM_META)
通過索引信息來遍歷要檢索的數據記錄是通過哪一個BLOOM_CHUNK進行映射處理的。
從存儲的角度來看,這些數據塊會划分到不同的區域進行存儲。
Trailer區域
該區域位於文件的最底部。HFile主要通過它來實現相關數據的定位功能,因此須要最先載入,其數據內容是採用protobuf進行序列化處理的,protocol聲明例如以下:
message FileTrailerProto { optional uint64 file_info_offset = 1;
optional uint64 load_on_open_data_offset = 2;
optional uint64 uncompressed_data_index_size = 3;
optional uint64 total_uncompressed_bytes = 4;
optional uint32 data_index_count = 5;
optional uint32 meta_index_count = 6;
optional uint64 entry_count = 7;
optional uint32 num_data_index_levels = 8;
optional uint64 first_data_block_offset = 9;
optional uint64 last_data_block_offset = 10;
optional string comparator_class_name = 11; optional uint32 compression_codec = 12; optional bytes encryption_key = 13; }
在代碼層面上Trailer是通過FixedFileTrailer類來封裝的,可通過其readFromStream方法用來讀取指定HFile的Trailer信息。
Load-on-open區域
HFile被載入之后。位於該區域中的數據將會被載入內存,該區域的起始位置通過Trailer來定位(通過其load_on_open_data_offset屬性)。從該位置起依次保存的數據信息為:根索引快、元數據索引塊、文件信息塊以及布隆索引塊。
Scanned-Block區域
在運行HFile順序掃描時,位於該區域中的全部塊信息都須要被載入,包括:表格數據塊、布隆數據塊和葉子索引塊(后兩者稱之為InlineBlock)。
Non-Scanned-Block區域
在運行HFile順序掃描時,位於該區域中的存儲塊可不被載入。包括:元數據塊和中間索引塊。
每一個Block塊是由3部分信息組成的。各自是:header信息、data信息以及用於data校驗的checksum信息。
不同類型的block僅僅是在data信息的存儲結構上存在差異,而header信息和checksum信息存儲結構基本一致。
header主要用於存儲每一個Block塊的元數據信息
這些信息包括:
(1)blockType:塊類型。HFile一共對外聲明了10種不同類型的Block。各自是:DATA(表格數據塊)、META(元數據塊)、BLOOM_CHUNK(布隆數據塊)、FILE_INFO(文件信息塊)、TRAILER、LEAF_INDEX(葉子索引塊)、INTERMEDIATE_INDEX(中間索引塊)、ROOT_INDEX(根索引快)、BLOOM_META(布隆索引塊)、和META_INDEX(元數據索引塊)。
(2)onDiskSizeWithoutHeader:data信息與checksum信息所占用的磁盤空間大小;
(3)onDiskDataSizeWithHeader:data信息與header信息所占用的磁盤空間大小。
(4)uncompressedSizeWithoutHeader:每一個block塊在完畢解壓縮之后的大小(不包括header和checksum占用的空間);
(5)prevBlockOffset:距離上一個同類型block塊的存儲偏移量大小。
在v2版本號中,header的長度為固定的33字節。
data主要用於封裝每一個block塊的核心數據內容
假設是根索引塊其數據內容例如以下:
主要包括多條索引實體信息(索引實體的個數記錄在Trailer中)以及midKey相關信息。當中每條索引實體信息是由3部分數據組成的。分別為:
(1)Offset:索引指向的Block塊在文件里的偏移量位置;
(2)DataSize:索引指向的Block塊所占用的磁盤空間大小(在HFile中的長度);
(3)Key:假設索引指向的是表格數據塊(DATA_BLOCK)。該值為目標數據塊中第一條數據記錄的rowkey值(0.95版本號之前是這種,之后的版本號參考HBASE-7845);假設索引指向的是其它索引塊,該值為目標索引塊中第一條索引實體的blockKey值。
而midKey信息主要用於定位HFile的中間位置,以便於對該HFile運行split拆分處理,其數據內容相同由3部分信息組成,分別為:
(1)midLeafBlockOffset:midKey所屬葉子索引塊在HFile中的偏移量位置。
(2)midLeafBlockOnDiskSize:midKey所屬葉子索引塊的大小(在HFile中的長度)。
(3)midKeyEntry:midKey在其所屬索引塊中的偏移量位置。
假設是非根索引塊其數據內容例如以下:
相同包括多條索引實體信息。但不包括midKey信息。除此之外還包括了索引實體的數量信息以及每條索引實體相對於首個索引實體的偏移量位置。
假設是表格數據塊其數據內容為多條KeyValue記錄,每條KeyValue的存儲結構可參考Memstore組件實現章節。
假設是元數據索引塊其數據內容同葉子索引塊相似。僅僅只是索引實體引向的是META數據塊。
假設是布隆數據塊其數據內容為布隆過濾器的位數組信息。
假設是布隆索引塊其數據內容例如以下:
同其它索引塊相似,包括多條索引實體信息,每條索引實體引向布隆數據塊(BLOOM_CHUNK)。除此之外還包括與布隆過濾器相關的元數據信息。包括:
(1)version:布隆過濾器版本號,在新版本號HBase中布隆過濾器通過CompoundBloomFilter類來實現,其相應的版本號號為3。
(2)totalByteSize:全部布隆數據塊占用的磁盤空間總大小;
(3)hashCount:元素映射過程中所使用的hash函數個數。
(4)hashType:元素映射過程中所採用的hash函數類型(通過hbase.hash.type屬性進行聲明)。
(5)totalKeyCount:全部布隆數據塊中已映射的元素數量;
(6)totalMaxKeys:在滿足指定誤報率的情況下(默覺得百分之中的一個),全部布隆數據塊可以映射的元素總量。
(7)numChunks:眼下已有布隆數據塊的數量;
(8)comparator:所映射元素的排序比較類,默覺得org.apache.hadoop.hbase.KeyValue.RawBytesComparator
假設是文件信息塊其數據內容採用protobuf進行序列化,相關protocol聲明例如以下:
message FileInfoProto { repeated BytesBytesPair map_entry = 1; // Map of name/values }
checksum信息用於校驗data數據是否正確
數據塊的讀取操作主要是通過FSReader類的readBlockData方法來實現的。在運行數據讀取操作之前。須要首先知道目標數據塊在HFile中的偏移量位置。有一些數據塊的偏移量信息是可通過Trailer進行定位的。如:
根索引塊(ROOT_INDEX)的偏移量信息可通過Trailer的load_on_open_data_offset屬性來定位,在知道了根索引塊的存儲信息之后,便可通過它來定位全部DATA_BLOCK在HFile中的偏移量位置;
首個DATA_BLOCK的偏移量信息可通過Trailer的first_data_block_offset屬性來定位。
在不知道目標數據塊大小的情況下須要對HFile運行兩次查詢才干讀取到終於想要的HFileBlock數據。第一次查詢主要是為了讀取目標Block的header信息,由於header具有固定的長度(HFileV2版本號為33字節)。因此在知道目標Block的偏移量之后,便可通過讀取指定長度的數據來將header獲取。
獲取到header之后便可通過其onDiskSizeWithoutHeader屬性來得知目標數據塊的總大小。
totalSize = headerSize + onDiskSizeWithoutHeader
然后再次從Block的偏移量處讀取長度為totalSize字節的數據。以此來構造完整的HFileBlock實體。
由以上邏輯來看,假設在讀取數據塊之前。可以事先知道該數據塊的大小。那么便可省去header的查詢過程。從而有效減少IO次數。
為此,HBase採用的做法是在讀取指定Block數據的同一時候。將下一個Block的header也一並讀取出來(通過讀取totalSize + headerSize長度的數據),並通過ThreadLocal將該header進行緩存。
這樣假設當前線程所訪問的數據是通過兩個連續的Block進行存儲的,那么針對第二個Block的訪問僅僅需運行一次IO就可以。
獲取到HFileBlock實體之后,可通過其getByteStream方法來獲取內部數據的輸入流信息,在依據不同的塊類型來選擇相應的API進行信息讀取:
(1)假設block為根索引塊。其信息內容可通過BlockIndexReader進行讀取,通過其readMultiLevelIndexRoot方法;
(2)假設為元數據索引塊。相同採用BlockIndexReader進行讀取,通過其readRootIndex方法;
(3)假設為非根索引塊,可通過BlockIndexReader的locateNonRootIndexEntry方法來將數據指針定位到目標block的索引位置上。從而對目標block的偏移量、大小進行讀取;
(4)假設為文件信息塊,通過FileInfo類的read方法進行讀取。
(5)假設為布隆索引塊,通過HFile.Reader實體的getGeneralBloomFilterMetadata方法進行讀取。
(6)假設為布隆數據塊,通過該HFileBlock實體的getBufferWithoutHeader方法來獲取布隆數據塊的位數組信息(參考CompoundBloomFilter類的實現)。
Block數據在寫入HFile之前是暫存於內存中的。通過字節數組進行存儲,當其數據量大小達到指定閥值之后,在開始向HFile進行寫入。
寫入成功后,須要再次開啟一個全新的Block來接收新的數據記錄。該邏輯通過HFileBlock.Writer類的startWriting方法來封裝。方法運行后。會首先開啟ByteArrayOutputStream輸出流實例,然后在將其包裝成DataOutputStream對象,用於向目標字節數組寫入要加入的Block實體信息。
在HFile.Writer內部。不同類型的數據塊是通過不同的Writer進行寫入的,其內部封裝了3種不同類型的子Writer(這些Writer共用一個FSDataOutputStream用於向HFile寫入Block數據),分別例如以下:
HFileBlock.Writer
通過該Writer完畢表格數據塊(DataBlock)向HFile的寫入邏輯。大致流程例如以下:
每當運行HFile.Writer類的append方法進行加入數據時。會檢測當前DataBlock的大小是否已經超過目標閥值。假設沒有,直接將數據寫入DataBlock,否則須要進行例如以下處理:
將當前DataBlock持久化寫入HFile
寫入之前須要首先生成目標數據塊的header和checksum信息。當中checksum信息可通過ChecksumUtil的generateChecksums方法進行獲取。而header信息可通過putHeader方法來生成。
生成當前DataBlock的索引信息
索引信息是由索引key。數據塊在HFile中的偏移量位置和數據塊的總大小3部分信息組成的,當中索引key可通過CellComparator.getMidpoint方法進行獲取。方法會試圖返回一條數據記錄可以滿足例如以下約束條件:
(1)索引key在排序上大於上一個DataBlock的最后一條記錄。
(2)索引key在排序上小於當前DataBlock的第一條記錄;
(3)索引key的size是最小的。
經過這樣處理之后可以總體減少索引塊的數據量大小,從而節省了內存空間的使用,並提高了載入效率。
將索引信息寫入索引塊
通過HFileBlockIndex.BlockIndexWriter的addEntry方法。
推斷是否有必要將InlineBlock進行持久化
InlineBlock包括葉子索引塊和布隆數據塊。它們的持久化邏輯分別通過BlockIndexWriter和CompoundBloomFilterWriter來完畢。
開啟新的DataBlock進行數據寫入,同一時候將老的數據塊退役
假設集群開啟了hbase.rs.cacheblocksonwrite配置,須要將老數據塊緩存至BlockCache中。
HFileBlockIndex.BlockIndexWriter
通過該Writer完畢索引數據塊(IndexBlock)向HFile的寫入邏輯。
在HFile內部。索引數據是分層級進行存儲的,包括根索引塊、中間索引塊和葉子索引塊。
當中葉子索引塊又稱之為InlineBlock,由於它會穿插在DataBlock之間進行存儲。同DataBlock相似。IndexBlock一開始也是緩存在內存里的。每當DataBlock寫入HFile之后。都會向當前葉子索引塊加入一條索引實體信息。
假設葉子索引塊的大小超過hfile.index.block.max.size限制,便開始向HFile進行寫入。寫入格式為:索引實體個數、每條索引實體相對於塊起始位置的偏移量信息。以及每條索引實體的具體信息(參考Block塊結構)。
這主要是葉子索引塊的寫入邏輯。而根索引塊和中間索引塊的寫入則主要在HFile.Writer關閉的時候進行。通過BlockIndexWriter的writeIndexBlocks方法。
在HFile內部,每一個索引塊是通過BlockIndexChunk對象進行封裝的,其對內聲明了例如以下數據結構:
(1)blockKeys,封裝每一條索引所指向的Block中第一條記錄的key值;
(2)blockOffsets,封裝每一條索引所指向的Block在HFile中的偏移量位置;
(3)onDiskDataSizes,封裝每一條索引所指向的Block在HFile中的長度。
除此之外。根索引塊還比較特殊。其對內聲明了numSubEntriesAt集合,集合類型為List<Long>,每當有葉子索引塊寫入HFile之后都會向該集合加入一條實體信息,實體的index為當前葉子索引塊的個數。value為索引實體總數。這樣。通過numSubEntriesAt集合便能確定midKey(中間索引)處在哪個葉子索引塊上,在通過blockKeys、blockOffsets和onDiskDataSizes便可以獲取最后的midkey信息。
然后將其作為根索引塊的一部分寫入HFile。並通過FixedFileTrailer來標記根索引塊的寫入位置。
須要注意的是根索引塊的大小也是受上限約束的,假設其大小大於hfile.index.block.max.size參數閥值(默覺得128kb),須要將其拆分成多個中間索引塊,然后在對這些中間索引塊創建根索引,以此來減少根索引塊的大小,具體邏輯可參考BlockIndexWriter類的writeIntermediateLevel方法實現。
CompoundBloomFilterWriter
通過該Writer完畢布隆數據向HFile的寫入邏輯。
布隆數據在HFile內部相同是分塊進行存儲的,每一個數據塊通過ByteBloomFilter類來封裝,負責存儲指定區間的數據集映射信息(參考布隆過濾器實現章節)。
每當運行HFile.Writer的append方法向DataBlock加入KeyValue數據之前。都要調用ByteBloomFilter的add方法來生成該KeyValue的布隆映射信息,為了滿足目標容錯率,每一個ByteBloomFilter實體可以映射的KeyValue數量是受上限約束的,假設達到目標上限值須要將其持久化到HFile中進行存儲,然后開啟新的ByteBloomFilter實例來接管之前的邏輯。
每當布隆數據塊寫入成功之后。都會運行BlockIndexWriter的addEntry方法來創建一條布隆索引實體,實體的key值為布隆數據塊所映射的第一條KeyValue的key值。
同葉子索引塊一樣,布隆數據塊也被稱之為InlineBlock,在寫入DataBlock的同一時候會對該類型的數據塊進行穿插寫入。這主要是布隆數據塊的寫入邏輯,而布隆索引塊主要是在HFile.Writer關閉的時候進行創建的。通過CompoundBloomFilterWriter.MetaWriter的write方法。將布隆索引數據連同meta信息一同寫入HFile。