HBase 架構原理-數據讀取流程解析

和寫流程相比,HBase讀數據是一個更加複雜的操作流程,這主要基於兩個方面的原因:

  • 其一是因爲整個HBase存儲引擎基於LSM-Like樹實現,因此一次範圍查詢可能會涉及多個分片、多塊緩存甚至多個數據存儲文件;
  • 其二是因爲HBase中更新操作以及刪除操作實現都很簡單,更新操作並沒有更新原有數據,而是使用時間戳屬性實現了多版本。刪除操作也並沒有真正刪除原有數據,只是插入了一條打上”deleted”標籤的數據,而真正的數據刪除發生在系統異步執行Major_Compact的時候。

很顯然,這種實現套路大大簡化了數據更新、刪除流程,但是對於數據讀取來說卻意味着套上了層層枷鎖,讀取過程需要根據版本進行過濾,同時對已經標記刪除的數據也要進行過濾。

總之,把這麼複雜的事情講明白並不是一件簡單的事情,爲了更加條理化地分析整個查詢過程,接下來筆者會用兩篇文章來講解整個過程,首篇文章主要會從框架的角度粗粒度地分析scan的整體流程,並不會涉及太多的細節實現。大多數看客通過首篇文章基本就可以初步瞭解scan的工作思路;爲了能夠從細節理清楚整個scan流程,接着第二篇文章將會在第一篇的基礎上引入更多的實現細節以及HBase對於scan所做的基礎優化。因爲理解問題可能會有紕漏,希望可以一起探討交流,歡迎拍磚~

Client-Server交互邏輯

運維開發了很長一段時間HBase,經常有業務同學諮詢爲什麼客戶端配置文件中沒有配置RegionServer的地址信息,這裏針對這種疑問簡單的做下解釋,客戶端與HBase系統的交互階段主要有如下幾個步驟:

  1. 客戶端首先會根據配置文件中zookeeper地址連接zookeeper,並讀取/<hbase-rootdir>/meta-region-server節點信息,該節點信息存儲HBase元數據(hbase:meta)表所在的RegionServer地址以及訪問端口等信息。用戶可以通過zookeeper命令(get /<hbase-rootdir>/meta-region-server)查看該節點信息。
  2. 根據hbase:meta所在RegionServer的訪問信息,客戶端會將該元數據表加載到本地並進行緩存。然後在表中確定待檢索rowkey所在的RegionServer信息。
  3. 根據數據所在RegionServer的訪問信息,客戶端會向該RegionServer發送真正的數據讀取請求。服務器端接收到該請求之後需要進行復雜的處理,具體的處理流程將會是這個專題的重點。

通過上述對客戶端以及HBase系統的交互分析,可以基本明確兩點:

  1. 客戶端只需要配置zookeeper的訪問地址以及根目錄,就可以進行正常的讀寫請求。不需要配置集羣的RegionServer地址列表。
  2. 客戶端會將hbase:meta元數據表緩存在本地,因此上述步驟中前兩步只會在客戶端第一次請求的時候發生,之後所有請求都直接從緩存中加載元數據。如果集羣發生某些變化導致hbase:meta元數據更改,客戶端再根據本地元數據表請求的時候就會發生異常,此時客戶端需要重新加載一份最新的元數據表到本地。

-----------------此處應有華麗麗的分隔線----------------

RegionServer接收到客戶端的get/scan請求之後,先後做了兩件事情:構建scanner體系(實際上就是做一些scan前的準備工作),在此體系基礎上一行一行檢索。舉個不太合適但易於理解的例子,scan數據就和開發商蓋房一樣,也是分成兩步:組建施工隊體系,明確每個工人的職責;一層一層蓋樓。

構建scanner體系-組建施工隊

scanner體系的核心在於三層scanner:
RegionScanner、StoreScanner以及StoreFileScanner。

三者是層級的關係,

一個RegionScanner由多個StoreScanner構成,一張表由多個列族組成,就有多少個StoreScanner負責該列族的數據掃描。

一個StoreScanner又是由多個StoreFileScanner組成。

每個Store的數據由內存中的MemStore和磁盤上的StoreFile文件組成。
相對應的,StoreScanner對象會僱傭一個MemStoreScanner和N個StoreFileScanner來進行實際的數據讀取,每個StoreFile文件對應一個StoreFileScanner。

注意:StoreFileScanner和MemstoreScanner是整個scan的最終執行者。

對應於建樓項目,一棟樓通常由好幾個單元樓構成(每個單元樓對應於一個Store),每個單元樓會請一個監工(StoreScanner)負責該單元樓的建造。而監工一般不做具體的事情,他負責招募很多工人(StoreFileScanner),這些工人才是建樓的主體。下圖是整個構建流程圖:

  1. RegionScanner會根據列族構建StoreScanner,有多少列族就構建多少StoreScanner,用於負責該列族的數據檢索

    1.1 構建StoreFileScanner:每個StoreScanner會爲當前該Store中每個HFile構造一個StoreFileScanner,用於實際執行對應文件的檢索。同時會爲對應Memstore構造一個MemstoreScanner,用於執行該Store中Memstore的數據檢索。該步驟對應於監工在人才市場招募建樓所需的各種類型工匠。

    1.2 過濾淘汰StoreFileScanner:根據Time Range以及RowKey Range對StoreFileScanner以及MemstoreScanner進行過濾,淘汰肯定不存在待檢索結果的Scanner。上圖中StoreFile3因爲檢查RowKeyRange不存在待檢索Rowkey所以被淘汰。該步驟針對具體的建樓方案,裁撤掉部分不需要的工匠,比如這棟樓不需要地暖安裝,對應的工匠就可以撤掉。

    1.3 Seek rowkey:所有StoreFileScanner開始做準備工作,在負責的HFile中定位到滿足條件的起始Row。工匠也開始準備自己的建造工具,建造材料,找到自己的工作地點,等待一聲命下。就像所有重要項目的準備工作都很核心一樣,Seek過程(此處略過Lazy Seek優化)也是一個很核心的步驟,它主要包含下面三步:

  • 定位Block Offset:在Blockcache中讀取該HFile的索引樹結構,根據索引樹檢索對應RowKey所在的Block Offset和Block Size
  • Load Block:根據BlockOffset首先在BlockCache中查找Data Block,如果不在緩存,再在HFile中加載
  • Seek Key:在Data Block內部通過二分查找的方式定位具體的RowKey

整體流程細節參見《HBase原理-探索HFile索引機制》,文中詳細說明了HFile索引結構以及如何通過索引結構定位具體的Block以及RowKey

1.4 StoreFileScanner合併構建最小堆:將該Store中所有StoreFileScanner和MemstoreScanner合併形成一個heap(最小堆),所謂heap是一個優先級隊列,隊列中元素是所有scanner,排序規則按照scanner seek到的keyvalue大小由小到大進行排序。這裏需要重點關注三個問題,首先爲什麼這些Scanner需要由小到大排序,其次keyvalue是什麼樣的結構,最後,keyvalue誰大誰小是如何確定的:

  • 爲什麼這些Scanner需要由小到大排序?

最直接的解釋是scan的結果需要由小到大輸出給用戶,當然,這並不全面,最合理的解釋是隻有由小到大排序才能使得scan效率最高。舉個簡單的例子,HBase支持數據多版本,假設用戶只想獲取最新版本,那隻需要將這些數據由最新到最舊進行排序,然後取隊首元素返回就可以。那麼,如果不排序,就只能遍歷所有元素,查看符不符合用戶查詢條件。這就是排隊的意義。

工匠們也需要排序,先做地板的排前面,做牆體的次之,最後是做門窗戶的。做牆體的內部還需要再排序,做內牆的排前面,做外牆的排後面,這樣,假如設計師臨時決定不做外牆的話,就可以直接跳過外牆部分工作。很顯然,如果不排序的話,是沒辦法臨時做決定的,因爲這部分工作已經可能做掉了。

  • HBase中KeyValue是什麼樣的結構?

HBase中KeyValue並不是簡單的KV數據對,而是一個具有複雜元素的結構體,其中Key由RowKey,ColumnFamily,Qualifier ,TimeStamp,KeyType等多部分組成,Value是一個簡單的二進制數據。Key中元素KeyType表示該KeyValue的類型,取值分別爲Put/Delete/Delete Column/Delete Family等。KeyValue可以表示爲如下圖所示:

瞭解了KeyValue的邏輯結構後,我們不妨再進一步從原理的角度想想HBase的開發者們爲什麼如此對其設計。這個就得從HBase所支持的數據操作說起了,HBase支持四種主要的數據操作,分別是Get/Scan/Put/Delete,其中Get和Scan代表數據查詢,Put操作代表數據插入或更新(如果Put的RowKey不存在則爲插入操作、否則爲更新操作),特別需要注意的是HBase中更新操作並不是直接覆蓋修改原數據,而是生成新的數據,新數據和原數據具有不同的版本(時間戳);Delete操作執行數據刪除,和數據更新操作相同,HBase執行數據刪除並不會馬上將數據從數據庫中永久刪除,而只是生成一條刪除記錄,最後在系統執行文件合併的時候再統一刪除。

HBase中更新刪除操作並不直接操作原數據,而是生成一個新紀錄,那問題來了,如何知道一條記錄到底是插入操作還是更新操作亦或是刪除操作呢?這正是KeyType和Timestamp的用武之地。上文中提到KeyType取值爲分別爲Put/Delete/Delete Column/Delete Family四種,如果KeyType取值爲Put,表示該條記錄爲插入或者更新操作,而無論是插入或者更新,都可以使用版本號(Timestamp)對記錄進行選擇;如果KeyType爲Delete,表示該條記錄爲整行刪除操作;相應的KeyType爲Delete Column和Delete Family分別表示刪除某行某列以及某行某列族操作;

  • 不同KeyValue之間如何進行大小比較?

上文提到KeyValue中Key由RowKey,ColumnFamily,Qualifier ,TimeStamp,KeyType等5部分組成,HBase設定Key大小首先比較RowKey,RowKey越小Key就越小;RowKey如果相同就看CF,CF越小Key越小;CF如果相同看Qualifier,Qualifier越小Key越小;Qualifier如果相同再看Timestamp,Timestamp越大表示時間越新,對應的Key越小。如果Timestamp還相同,就看KeyType,KeyType按照DeleteFamily -> DeleteColumn -> Delete -> Put 順序依次對應的Key越來越大。

2. StoreScanner合併構建最小堆:上文討論的是一個監工如何構建自己的工匠師團隊以及工匠師如何做準備工作、排序工作。實際上,監工也需要進行排序,比如一單元的監工排前面,二單元的監工排之後… StoreScanner一樣,列族小的StoreScanner排前面,列族大的StoreScanner排後面。

scan查詢-層層建樓

構建Scanner體系是爲了更好地執行scan查詢,就像組建工匠師團隊就是爲了蓋房子一樣。scan查詢總是一行一行查詢的,先查第一行的所有數據,再查第二行的所有數據,但每一行的查詢流程卻沒有什麼本質區別。蓋房子也一樣,無論是蓋8層還是蓋18層,都需要一層一層往上蓋,而且每一層的蓋法並沒有什麼區別。所以實際上我們只需要關注其中一行數據是如何查詢的就可以。

對於一行數據的查詢,又可以分解爲多個列族的查詢,比如RowKey=row1的一行數據查詢,首先查詢列族1上該行的數據集合,再查詢列族2裏該行的數據集合。同樣是蓋第一層房子,先蓋一單元的一層,再改二單元的一層,蓋完之後纔算一層蓋完,接着開始蓋第二層。所以我們也只需要關注某一行某個列族的數據是如何查詢的就可以。

還記得Scanner體系構建的最終結果是一個由StoreFileScanner和MemstoreScanner組成的heap(最小堆)麼,這裏就派上用場了。下圖是一張表的邏輯視圖,該表有兩個列族cf1和cf2(我們只關注cf1),cf1只有一個列name,表中有5行數據,其中每個cell基本都有多個版本。cf1的數據假如實際存儲在三個區域,memstore中有r2和r4的最新數據,hfile1中是最早的數據。現在需要查詢RowKey=r2的數據,按照上文的理論對應的Scanner指向就如圖所示:

這三個Scanner組成的heap爲<MemstoreScanner,StoreFileScanner2, StoreFileScanner1>,Scanner由小到大排列。查詢的時候首先pop出heap的堆頂元素,即MemstoreScanner,得到keyvalue = r2:cf1:name:v3:name23的數據,拿到這個keyvalue之後,需要進行如下判定:

  1. 檢查該KeyValue的KeyType是否是Deleted/DeletedCol等,如果是就直接忽略該列所有其他版本,跳到下列(列族)
  2. 檢查該KeyValue的Timestamp是否在用戶設定的Timestamp Range範圍,如果不在該範圍,忽略
  3. 檢查該KeyValue是否滿足用戶設置的各種filter過濾器,如果不滿足,忽略
  4. 檢查該KeyValue是否滿足用戶查詢中設定的版本數,比如用戶只查詢最新版本,則忽略該cell的其他版本;反正如果用戶查詢所有版本,則還需要查詢該cell的其他版本。

現在假設用戶查詢所有版本而且該keyvalue檢查通過,此時當前的堆頂元素需要執行next方法去檢索下一個值,並重新組織最小堆。即圖中MemstoreScanner將會指向r4,重新組織最小堆之後最小堆將會變爲<StoreFileScanner2, StoreFileScanner1, MemstoreScanner>,堆頂元素變爲StoreFileScanner2,得到keyvalue=r2:cf1:name:v2:name22,進行一系列判定,再next,再重新組織最小堆…

不斷重複這個過程,直至一行數據全部被檢索得到。繼續下一行…

----------------此處應有華麗麗的分隔符----------------

本文從框架層面對HBase讀取流程進行了詳細的解析,文中並沒有針對細節進行深入分析,一方面是擔心個人能力有限,引入太多細節會讓文章難於理解,另一方面是大多數看官可能對細節並不關心,下篇文章筆者會揪出來一些比較重要的細節和大家一起交流~

文章最後,貼出來一個一些朋友諮詢的問題:Memstore在flush的時候會不會將Blockcache中的數據update?如果不update的話不就會產生髒讀,讀到以前的老數據?

這個問題大家可以思考思考,我們下一篇文章再見~

範欣欣,網易杭州研究院技術專家。負責網易內部Hadoop&HBase等組件內核開發運維工作,擅長大數據領域架構設計,性能優化以及問題診斷。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章