在瞭解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中的各個結構的關係。
每個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值)。
舉個簡單的例子。
假設有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了。
以此類推。
當某個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 (歷史文章查閱非常方便)