HBase原理--HBase讀取流程

和寫流程相比,HBase讀數據的流程更加複雜。主要基於兩個方面的緣由:一是由於HBase一次範圍查詢可能會涉及多個Region、多塊緩存甚至多個數據存儲文件;二是由於HBase中更新操做以及刪除操做的實現都很簡單,更新操做並無更新原有數據,而是使用時間戳屬性實現了多版本;刪除操做也並無真正刪除原有數據,只是插入了一條標記爲"deleted"標籤的數據,而真正的數據刪除發生在系統異步執行Major Compact的時候。很顯然,這種實現思路大大簡化了數據更新、刪除流程,可是對於數據讀取來講卻意味着套上了層層枷鎖:讀取過程須要根據版本進行過濾,對已經標記刪除的數據也要進行過濾。緩存

本節系統地將HBase讀取流程的各個環節串起來進行解讀。讀流程從頭至尾能夠分爲以下4個步驟:Client-Server讀取交互邏輯,Server端Scan框架體系,過濾淘汰不符合查詢條件的HFile,從HFile中讀取待查找Key。其中Client-Server交互邏輯主要介紹HBase客戶端在整個scan請求的過程當中是如何與服務器端進行交互的,理解這點對於使用HBase Scan API進行數據讀取很是重要。瞭解Server端Scan框架體系,從宏觀上介紹HBase RegionServer如何逐步處理一次scan請求。接下來的小節會對scan流程中的核心步驟進行更加深刻的分析。服務器

Client-Server讀取交互邏輯網絡

Client-Server通用交互邏輯在以前介紹寫入流程的時候已經作過解讀:Client首先會從ZooKeeper中獲取元數據hbase:meta表所在的RegionServer,而後根據待讀寫rowkey發送請求到元數據所在RegionServer,獲取數據所在的目標RegionServer和Region(並將這部分元數據信息緩存到本地),最後將請求進行封裝發送到目標RegionServer進行處理。框架

在通用交互邏輯的基礎上,數據讀取過程當中Client與Server的交互有不少須要關注的點。從API的角度看,HBase數據讀取能夠分爲get和scan兩類,get請求一般根據給定rowkey查找一行記錄,scan請求一般根據給定的startkey和stopkey查找多行知足條件的記錄。但從技術實現的角度來看,get請求也是一種scan請求(最簡單的scan請求,scan的條數爲1)。從這個角度講,全部讀取操做均可以認爲是一次scan操做。異步

HBase Client端與Server端的scan操做並無設計爲一次RPC請求,這是由於一次大規模的scan操做頗有可能就是一次全表掃描,掃描結果很是之大,經過一次RPC將大量掃描結果返回客戶端會帶來至少兩個很是嚴重的後果:函數

•大量數據傳輸會致使集羣網絡帶寬等系統資源短期被大量佔用,嚴重影響集羣中其餘業務。spa

•客戶端極可能由於內存沒法緩存這些數據而致使客戶端OOM。設計

實際上HBase會根據設置條件將一次大的scan操做拆分爲多個RPC請求,每一個RPC請求稱爲一次next請求,每次只返回規定數量的結果。下面是一段scan的客戶端示例代碼:code

public static void scan(){
    HTable table=... ;
    Scan scan=new Scan();
    scan.withStartRow(startRow)
        //設置檢索起始row
        .withStopRow(stopRow)
        //設置檢索結束row
        .setFamilyMap (Map<byte[],Set<byte[]>familyMap>)
        //設置檢索的列簇和對應列簇下的列集合
        .setTimeRange(minStamp,maxStamp)
        //設置檢索TimeRange
        .setMaxVersions(maxVersions)
        //設置檢索的最大版本號
        .setFilter(filter)
        //設置檢索過濾器
    scan.setMaxResultSize(10000);
    scan.setCacheing(500);
    scan.setBatch(100);
    ResultScanner rs=table.getScanner(scan);
    for (Result r : rs){
        for (KeyValue kv : r.raw()){
        ......
        }
    }
}

其中,for (Result r : rs)語句實際等價於Result r=rs.next()。每執行一次next()操做,客戶端先會從本地緩存中檢查是否有數據,若是有就直接返回給用戶,若是沒有就發起一次RPC請求到服務器端獲取,獲取成功以後緩存到本地。對象

單次RPC請求的數據條數由參數caching設定,默認爲Integer.MAX_VALUE。每次RPC請求獲取的數據都會緩存到客戶端,該值若是設置過大,可能會由於一次獲取到的數據量太大致使服務器端/客戶端內存OOM;而若是設置過小會致使一次大scan進行太屢次RPC,網絡成本高。

對於不少特殊業務有可能一張表中設置了大量(幾萬甚至幾十萬)的列,這樣一行數據的數據量就會很是大,爲了防止返回一行數據但數據量很大的狀況,客戶端能夠經過setBatch方法設置一次RPC請求的數據列數量。

另外,客戶端還能夠經過setMaxResultSize方法設置每次RPC請求返回的數據量大小(不是數據條數),默認是2G。

Server端Scan框架體系

從宏觀視角來看,一次scan可能會同時掃描一張表的多個Region,對於這種掃描,客戶端會根據hbase:meta元數據將掃描的起始區間[startKey, stopKey)進行切分,切分紅多個互相獨立的查詢子區間,每一個子區間對應一個Region。好比當前表有3個Region,Region的起始區間分別爲:["a", "c"),["c", "e"),["e","g"),客戶端設置scan的掃描區間爲["b", "f")。由於掃描區間明顯跨越了多個Region,須要進行切分,按照Region區間切分後的子區間爲["b", "c"),["c","e"),["e", "f ")。

HBase中每一個Region都是一個獨立的存儲引擎,所以客戶端能夠將每一個子區間請求分別發送給對應的Region進行處理。下文會聚焦於單個Region處理scan請求的核心流程。

RegionServer接收到客戶端的get/scan請求以後作了兩件事情:首先構建scanneriterator體系;而後執行next函數獲取KeyValue,並對其進行條件過濾。

1. 構建Scanner Iterator體系

Scanner的核心體系包括三層Scanner:RegionScanner,StoreScanner,MemStoreScanner和StoreFileScanner。三者是層級的關係:

•一個RegionScanner由多個StoreScanner構成。一張表由多少個列簇組成,就有多少個StoreScanner,每一個StoreScanner負責對應Store的數據查找。

•一個StoreScanner由MemStoreScanner和StoreFileScanner構成。每一個Store的數據由內存中的MemStore和磁盤上的StoreFile文件組成。相對應的,StoreScanner會爲當前該Store中每一個HFile構造一個StoreFileScanner,用於實際執行對應文件的檢索。同時,會爲對應MemStore構造一個MemStoreScanner,用於執行該Store中MemStore的數據檢索。

須要注意的是,RegionScanner以及StoreScanner並不負責實際查找操做,它們更多地承擔組織調度任務,負責KeyValue最終查找操做的是StoreFileScanner和MemStoreScanner。三層Scanner體系能夠用圖表示。

image.png
Scanner的三層體系

構造好三層Scanner體系以後準備工做並無完成,接下來還須要幾個很是核心的關鍵步驟,如圖所示。

image.png
Scanner工做流程

1)過濾淘汰部分不知足查詢條件的Scanner。StoreScanner爲每個HFile構造一個對應的StoreFileScanner,須要注意的事實是,並非每個HFile都包含用戶想要查找的KeyValue,相反,能夠經過一些查詢條件過濾掉不少確定不存在待查找KeyValue的HFile。主要過濾策略有:Time Range過濾、Rowkey Range過濾以及布隆過濾器,下圖中StoreFile3檢查未經過而被過濾淘汰。

2)每一個Scanner seek到startKey。這個步驟在每一個HFile文件中(或MemStore)中seek掃描起始點startKey。若是HFile中沒有找到starkKey,則seek下一個KeyValue地址。HFile中具體的seek過程比較複雜。

3)KeyValueScanner合併構建最小堆。將該Store中的全部StoreFileScanner和MemStoreScanner合併造成一個heap(最小堆),所謂heap其實是一個優先級隊列。在隊列中,按照Scanner排序規則將Scanner seek獲得的KeyValue由小到大進行排序。最小堆管理Scanner能夠保證取出來的KeyValue都是最小的,這樣依次不斷地pop就能夠由小到大獲取目標KeyValue集合,保證有序性。

2. 執行next函數獲取KeyValue並對其進行條件過濾
通過Scanner體系的構建,KeyValue此時已經能夠由小到大依次通過KeyValueScanner得到,但這些KeyValue是否知足用戶設定的TimeRange條件、版本號條件以及Filter條件還須要進一步的檢查。檢查規則以下:

1)檢查該KeyValue的KeyType是不是Deleted/DeletedColumn/DeleteFamily等,若是是,則直接忽略該列全部其餘版本,跳到下列(列簇)。

2)檢查該KeyValue的Timestamp是否在用戶設定的Timestamp Range範圍,若是不在該範圍,忽略。

3)檢查該KeyValue是否知足用戶設置的各類filter過濾器,若是不知足,忽略。

4)檢查該KeyValue是否知足用戶查詢中設定的版本數,好比用戶只查詢最新版本,則忽略該列的其餘版本;反之,若是用戶查詢全部版本,則還須要查詢該cell的其餘版本。

過濾淘汰不符合查詢條件的HFile

過濾StoreFile發生在圖中第3步,過濾手段主要有三種:根據KeyRange過濾,根據TimeRange過濾,根據布隆過濾器進行過濾。

1)根據KeyRange過濾:由於StoreFile中全部KeyValue數據都是有序排列的,因此若是待檢索row範圍[ startrow,stoprow ]與文件起始key範圍[ f irstkey,lastkey ]沒有交集,好比stoprow < f irstkey或者startrow > lastkey,就能夠過濾掉該StoreFile。

2)根據TimeRange過濾:StoreFile中元數據有一個關於該File的TimeRange屬性[ miniTimestamp, maxTimestamp ],若是待檢索的TimeRange與該文件時間範圍沒有交集,就能夠過濾掉該StoreFile;另外,若是該文件全部數據已通過期,也能夠過濾淘汰。

3)根據布隆過濾器進行過濾:系統根據待檢索的rowkey獲取對應的Bloom Block並加載到內存(一般狀況下,熱點Bloom Block會常駐內存的),再用hash函數對待檢索rowkey進行hash,根據hash後的結果在布隆過濾器數據中進行尋址,便可肯定待檢索rowkey是否必定不存在於該HFile。

從HFile中讀取待查找Key

在一個HFile文件中seek待查找的Key,該過程能夠分解爲4步操做,如圖所示。
image.png
HFile讀取待查Key流程

  1. 根據HFile索引樹定位目標Block

HRegionServer打開HFile時會將全部HFile的Trailer部分和Load-on-open部分加載到內存,Load-on-open部分有個很是重要的Block——Root Index Block,即索引樹的根節點。

一個Index Entry,由BlockKey、Block Offset、BlockDataSize三個字段組成。
image.png

BlockKey是整個Block的第一個rowkey,如Root Index Block中"a", "m", "o","u"都爲BlockKey。Block Offset表示該索引節點指向的Block在HFile的偏移量。

HFile索引樹索引在數據量不大的時候只有最上面一層,隨着數據量增大開始分裂爲多層,最多三層。

一次查詢的索引過程,基本流程能夠表示爲:

1)用戶輸入rowkey爲'fb',在Root Index Block中經過二分查找定位到'fb'在'a'和'm'之間,所以須要訪問索引'a'指向的中間節點。由於Root IndexBlock常駐內存,因此這個過程很快。

2)將索引'a'指向的中間節點索引塊加載到內存,而後經過二分查找定位到fb在index 'd'和'h'之間,接下來訪問索引'd'指向的葉子節點。

3)同理,將索引'd'指向的中間節點索引塊加載到內存,經過二分查找定位找到fb在index 'f'和'g'之間,最後須要訪問索引'f'指向的Data Block節點。

4)將索引'f'指向的Data Block加載到內存,經過遍歷的方式找到對應KeyValue。

上述流程中,Intermediate Index Block、Leaf Index Block以及Data Block都須要加載到內存,因此一次查詢的IO正常爲3次。可是實際上HBase爲Block提供了緩存機制,能夠將頻繁使用的Block緩存在內存中,以便進一步加快實際讀取過程。

2. BlockCache中檢索目標Block

從BlockCache中定位待查Block都很是簡單。Block緩存到BlockCache以後會構建一個Map,Map的Key是BlockKey,Value是Block在內存中的地址。其中BlockKey由兩部分構成——HFile名稱以及Block在HFile中的偏移量。BlockKey很顯然是全局惟一的。根據BlockKey能夠獲取該Block在BlockCache中內存位置,而後直接加載出該Block對象。若是在BlockCache中沒有找到待查Block,就須要在HDFS文件中查找。

3. HDFS文件中檢索目標Block

上文說到根據文件索引提供的Block Offset以及Block DataSize這兩個元素能夠在HDFS上讀取到對應的Data Block內容。這個階段HBase會下發命令給HDFS,HDFS執行真正的Data Block查找工做,如圖所示。
image.png
HDFS文件檢索Block

整個流程涉及4個組件:HBase、NameNode、DataNode以及磁盤。其中HBase模塊作的事情上文已經作過了說明,須要特別說明的是FSDataInputStream這個輸入流,HBase會在加載HFile的時候爲每一個HFile新建一個從HDFS讀取數據的輸入流——FSDataInputStream,以後全部對該HFile的讀取操做都會使用這個文件級別的InputStream進行操做。

使用FSDataInputStream讀取HFile中的數據塊,命令下發到HDFS,首先會聯繫NameNode組件。NameNode組件會作兩件事情:

•找到屬於這個HFile的全部HDFSBlock列表,確認待查找數據在哪一個HDFSBlock上。衆所周知,HDFS會將一個給定文件切分爲多個大小等於128M的Data Block,NameNode上會存儲數據文件與這些HDFSBlock的對應關係。

•確認定位到的HDFSBlock在哪些DataNode上,選擇一個最優DataNode返回給客戶端。HDFS將文件切分紅多個HDFSBlock以後,採起必定的策略按照三副本原則將其分佈在集羣的不一樣節點,實現數據的高可靠存儲。HDFSBlock與DataNode的對應關係存儲在NameNode。

NameNode告知HBase能夠去特定DataNode上訪問特定HDFSBlock,以後,HBase會再聯繫對應DataNode。DataNode首先找到指定HDFSBlock,seek到指定偏移量,並從磁盤讀出指定大小的數據返回。

DataNode讀取數據其實是向磁盤發送讀取指令,磁盤接收到讀取指令以後會移動磁頭到給定位置,讀取出完整的64K數據返回。

4. 從Block中讀取待查找KeyValue
HFile Block由KeyValue(由小到大依次存儲)構成,但這些KeyValue並非固定長度的,只能遍歷掃描查找。

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

相關文章
相關標籤/搜索