【從零單排HBase 03】深入HBase讀寫

在瞭解HBase架構的基礎上,我們需要進一步學習HBase的讀寫過程,一方面是瞭解各個組件在整個讀寫過程中充當的角色,另一方面只有瞭解HBase的真實請求過程,才能爲後續的正確使用打下初步基礎,畢竟,除了會使用api,你還得知道怎麼能寫得更快,怎麼查得更快。

1.首次讀寫的基本過程

在上一篇 深入HBase架構(建議收藏)中已經做了介紹。這裏再重申一下。

這裏要解決的主要問題是,

client如何知道去那個region server執行自己的讀寫請求。

有一個特殊的HBase表,叫做META table,保存了集羣中各個region的位置。

而這個表的位置信息是保存在zookeeper中的。因此,當我們第一次訪問HBase集羣時,會做以下操作:

1)客戶端從zk中獲取保存meta table的位置信息,知道meta table保存在了哪個region server,並在客戶端緩存這個位置信息;

2)client會查詢這個保存meta table的特定的region server,查詢meta table信息,在table中獲取自己想要訪問的row key所在的region在哪個region server上。

3)客戶端直接訪問目標region server,獲取對應的row

這裏我們需要關注一定,在讀寫的過程中,客戶端實際上是不需要跟HMaster有任何交互的。這也是爲什麼我們在客戶端的配置中,連接地址是填寫的zookeeper的地址。
在這裏插入圖片描述
meta table信息都可以在client上進行緩存(apache的原生abase-client類的Connection的實現類中)。

2.寫請求

從上文我們知道了,client如何找到目標region server發起請求。

接下來,就是正式的寫操作了。

當client將寫請求發送到客戶端後,會執行以下流程。

(1)獲取行鎖: HBase中使用行鎖保證對同一行數據的更新都是互斥操作,用以保證更新的原子性。

(2)Append HLog:順序寫入HLog中,並執行sync。

(3)寫緩存memstore

(4)釋放行鎖

這裏需要重點關注WAL。

WAL(Write-Ahead Logging)是一種高效、高可靠的日誌機制。

基本原理就是在數據寫入時,通過先順序寫入日誌,然後再寫入緩存,等到緩存寫滿之後統一落盤。

爲什麼可以提高寫入性能和可靠性呢?

衆所周知,對於磁盤的寫入,順序寫性能是遠高於隨機寫的。因此,WAL將將一次隨機寫轉化爲了一次順序寫加一次內存寫,提高了性能。

至於可靠性,我們可以看到,因爲先寫日誌再寫緩存,即使發生宕機,緩存數據丟失,那麼我們也可以通過恢復日誌還原出丟失的數據。

另一方面,我們需要關注一下HBase中的各個結構的關係。
深入HBase讀寫
每個region server上只有一個HLog,但是有多個region。

每個HRegion裏面有多個HStore,每個HStore會有一個寫入緩存memstore,memstore是根據columnfamily來劃分。

因此,在一個寫入操作中,我們對任意一行的改變是落在memstore上,然後HBase並不會直接將數據落盤,而是先寫入緩存,等緩存滿足一定大小之後再一起落盤,生成新的HFile。

3.讀請求

HBase-client上的讀請求分爲 兩種,Get和Scan。

Get是一種隨機查詢的模式,根據給定的rowkey返回一行數據,雖然Get也支持輸入多個rowkey返回多個結果,但是本質上是多次隨機查詢。

具體rpc次數,取決於查詢list的數據分佈,如果都分佈在一個region server上,就是一次rpc,如果是分佈在3個rs,就是3次rpc,但是是併發請求和返回的,時間取決於最慢的那個。

Scan是一種批量查詢的模式,根據指定的startRow和endRow進行範圍掃描,獲取區間內的數據。

而對於hbase服務端來說,當一個Get請求過來後,還是會轉換爲一個特殊的scan請求,即startrow和endrow一致的Scan請求。所以,下文的介紹,就圍繞scan展開。

首先,我們要知道,HBase的寫入很快,是追加多版本的形式,刪除也很快,只是插入一條打上“deteled”標籤的數據。因此,hbase的讀操作比較複雜的,需要處理各種狀態和關係。

因爲Store是按照columfamily來劃分的,一張表由N個列族組成,就有N個StoreScanner負責該列族的數據掃描。

當client要查詢一個region,那麼就會有一個RegionScanne,這個regionscannerr會創建N個StoreScanner。

而一個store由多個storefile和一個memstore組成,

因此,StoreScanner對象會創建一個MemStoreScanner和多個StoreFileScanner進行實際數據的讀取。

這些scanner首先根據TimeRange和RowKey Range過濾掉一部分肯定無用的StoreFileScanner。

剩下的scanner組成一個最小堆KeyValueHeap。這個最小堆的實際數據結構是一個優先級隊列,隊列中所有元素是scanner,根據scanner指向的keyvalue進行排序(scanner類似遊標,每次查詢一個結果後,通過next下移找下一個kv值)。

舉個簡單的例子。
深入HBase讀寫
假設有4個scanner組成的優先級隊列,分佈標記爲ScannerA\B\C\D。

1)查詢的時候首先pop出heap的堆頂元素。

2)第一次pop出來的是scannerA。調用 next 請求,將會返回 ScannerA 中的 rowA:cf:colA,而後 ScannerA 的指針移動到下一個 KeyValue rowA:cf:colB;

3)重新組織堆中元素,堆中的 Scanners 排序不變;

4)第二次 pop出來的還是scannerA。調用next 請求,返回 ScannerA 中的 rowA:cf:colB,ScannerA 的 current 指針移動到下一個 KeyValue rowB:cf:ColA;

5)重新組織堆中元素,由於此時scannerA的指針指向了rowB,按照 KeyValue 排序可知 rowB 小於 rowA, 所以堆內部,scanner 順序發生改變,改變之後如下圖所示:

6)第三次pop出來的就是ScannerB了。

以此類推。
深入HBase讀寫
當某個scanner 內部數據完全檢索之後會就會被 close 掉,或者 rowA 所有數據檢索完畢,則查詢下一條。

默認情況下返回的數據需要經過 ScanQueryMatcher 過濾返回的數據需要滿足下面的條件:

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

至此,就是HBase大致上的讀寫流程。

我們經常聽說HBase數據讀取要讀Memstore、HFile和Blockcache,爲什麼我們這裏說Scanner只有StoreFileScanner和MemstoreScanner,而沒有BlockcacheScanner呢?

因爲HBase中數據僅獨立地存在於Memstore和StoreFile中,Blockcache作爲讀緩存,裏面有StoreFile中的部分熱點數據,因此,如果有數據存在於Blockcache中,那麼這些數據必然存在StoreFile中。因此使用MemstoreScanner和StoreFileScanner就可以覆蓋到所有數據。

而在實際的讀操作時,StoreFileScanner通過索引定位到待查找key所在的block之後,會先去查看該block是否存在於Blockcache中,如果存在,那麼就會去BlockCache中取出,避免IO,如果BlockCache中不存在,纔會再到對應的StoreFile中讀取。

看到這裏了,原創不易,點個贊吧,你最好看了~

知識碎片重新梳理,構建Java知識圖譜:https://github.com/saigu/JavaKnowledgeGraph (歷史文章查閱非常方便)

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