hbase讀寫流程分析
前言
最近被大佬問到一個問題,hbase查詢資料在最壞的場景下需要進行幾次rpc,當時就懵了..下面主要對client端程式碼進行分析。閱讀文章和看原始碼更配~
讀資料
流程總覽
1. 從zookeeper中獲取meta資訊,並通過meta資訊找到需要查詢的table的startkey所在的region資訊
2. 和該region所在的regionserver進行rpc互動獲取result
3. region server查詢memstore(memstore是是一個按key排序的樹形結構的緩衝區),如果有該rowkey,則直接返回,若沒有進入步驟4
4. 查詢blockcache,如果有該rowkey,則直接返回,若沒有進入步驟5
5. 查詢storefile,不管有沒有都直接返回
client程式碼分析
hbase讀資料除了直接操作hfile之外有3個入口,get(),batch()和scan(),get()相對而言就比較簡單,找到對應的regionserver然後發rpc即可,batch()採用單rpc多action的策略流程和get()類似,下面主要對scan涉及的核心介面進行分析。核心介面有以下幾個
Connection:負責和zk建立連線
Table:負責維護相關物件
ResultScanner:負責給使用者遍歷紓解
Caller:負責呼叫Callable
Callable:客戶端和hbase互動的主要介面
Connection
預設的聯結器是HConnectionImplementation,可以通過配置`hbase.client.connection.impl`修改。核心思路是基於zk的watcher,保持長連線,然後獲取hbase元資料
Table
table通過Connection.getTable()例項化,預設的實現是HTable。這個類比較簡單,只是維護了針對hbase一張表所用到的物件。主要關注遍歷的方法,通過HTable.getScanner()例項化一個新的ResultScanner,使用者通過ResultScanner迭代器遍歷獲取result資料。
Scanner
client提供了4種scanner,參考HTable.getScanner(),1. ClientScanner,讀取result的流程需要3次rpc,openScanner,next和closeScanner;2. 針對小量資料優化的ClientSmallScanner,和ClientScanner的區別在於,將openScanner,next和closeScanner合併到一個rpc執行,官方建議拉取的資料在64KB之內可以考慮用SmallScanner的方式;另外兩個是基於reversed配置,也就是倒序遍歷region,需要交換startkey和endkey的位置。ClientScanner是我們最常用的Scanner,也是預設的Scanner,下面對其進行分析
- 在初始化的時候通過nextScanner()方法,例項化一個新的Callable物件,並呼叫其call()方法
- next()方法,當使用者不斷的呼叫next()時,ClientScanner()會先從快取中找,是否還有result,如果還有那麼直接返回,如果快取中沒有result,那麼呼叫loadCache()方法
- loadCache()方法,呼叫Callable.call(),獲取result陣列。這裡的異常處理需要特別關注,如果是UnkonwnScannerException,那麼重試rpc直到本次scan超時,如果是OutOfOrderScannerNextException異常,scanner會重試rpc請求重複步驟3,如果已經重試過,那麼直接丟擲異常。重試的超時時間的配置`hbase.client.scanner.timeout.period`,預設是60s
- 拉取到result後,ClientScanner會進行合併,這是由於拉取到的result是部分的,不是完整的,說到底hbase是以Cell為最小單位進行儲存或者傳輸的,要封裝成result的話就需要進行合併。合併完之後將result快取在記憶體中,快取策略基於caching和maxResultSize,caching表示hbase client最多可以快取在記憶體多少條資料,也就是多少個result;maxResultSize表示hbase client最多可以快取多少記憶體大小的result,也就是控制result佔用堆的大小
- 判斷是否還需要再拉取result,這裡有兩種拉取判斷,一種是之前的region拉取失敗,轉而拉取其replica,另一種是呼叫rpc拉取下一組result。
- result達到記憶體限制或者數量(maxResultSize,caching)則返回
ScannerCallable
ClientScanne對應的Callable是ScannerCallable,也是最典型的Callable,下面對其核心方法進行分析
prepare()方法
- 核心功能是通過RPCRetryingCallerWithReadReplicas.getRegionLocations獲取待遍歷的table startkey的region,從而定位到region server。
核心call()方法
- 首次呼叫call(),client會發送一次開始rpc,高速region server本次scan開始了,此次rpc不獲取result,只生成scannerId,之後的rpc不需要再傳遞scan配置,這形成了一個會話的概念。
- 通過rpc controller獲取CellScanner,再轉換成Result陣列,這裡參考`ResponseConverter.getResults`。注意,這裡由於獲取的result是連續的,也就是說region server是有狀態的服務,client每次rpc都會帶上當前請求的序號,也就是nextCallSeq,這有的類似傳統資料庫中的分頁作用。當出現序號不匹配,region server會丟擲異常。
- 如果需要關閉,那麼向region server傳送close的rpc
總結
hbase-client的scan操作總體上可以看成是兩層迭代器,面向使用者的Scanner以及面向region server的Callable。Callable負責從regionserver中獲取result,主要解決,Scanner負責整合result提供給使用者。這樣做的思路很明顯,資料大小是肯定會大於記憶體的,通過迭代器介面,可以讓使用者處理完之前的result再拉取其他result,從而起到分頁的效果,這操作對使用者是透明的。如果需要詳細的scan日誌,可以通過配置`hbase.client.log.scanner.activity`來開啟開關,預設是false。
寫資料
流程總覽
- zookeeper中獲取meta資訊,並通過meta資訊找到需要查詢的table的startkey所在的region資訊(和讀資料相似)
- 將put快取在記憶體中,參考`BufferedMutatorImpl`,並計算其記憶體大小(計算方式會考慮java物件佔用的大小,參考ofollow,noindex">《java物件在記憶體的大小》 ),當超過`hbase.client.write.buffer`預設2097152位元組也就是2MB,client會將put 通過rpc交給region server
- region server接收資料後分別寫到HLog和MemStore上一份
- MemStore達到一個閾值後則把資料刷成一個StoreFile檔案。若MemStore中的資料有丟失,則可以從HLog上恢復
- 當多個StoreFile檔案達到一定的數量,會觸發Compact和Major Compaction操作,這裡不對compaction的細節做展開。
- 當Compact後,逐步形成越來越大的StoreFIle後,會觸發Split操作,把當前的StoreFile分成兩個,這裡相當於把一個大的region分割成兩個region,細節也不展開了。
總結
對於scan操作而言,拿ClientScanner來說,一次“完整rpc”過程包含3次rpc,open,result和close。如果失敗了,region不可用或者在split,那麼client會重試新的一次“完整rpc”,那麼就是6次rpc。其他操作會少一點,例如SmallClientScanner一次“完整rpc”只需要1次rpc,它把open,close整合到了一起。hbase在client還是花了不少心思的。
參考
HBase-1.3.1程式碼