一、前言
本文首先對 HBase 做簡單的介紹,包括其整體架構、依賴元件、核心服務類的相關解析。再重點介紹 HBase 讀取資料的流程分析,並根據此流程介紹如何在客戶端以及服務端優化效能,同時結合有贊線上 HBase 叢集的實際應用情況,將理論和實踐結合,希望能給讀者帶來啟發。如文章有紕漏請在下面留言,我們共同探討共同學習。
二、 HBase 簡介
HBase 是一個分散式,可擴充套件,面向列的適合儲存海量資料的資料庫,其最主要的功能是解決海量資料下的實時隨機讀寫的問題。 通常 HBase 依賴 HDFS 做為底層分散式檔案系統,本文以此做前提並展開,詳細介紹 HBase 的架構,讀路徑以及優化實踐。
2.1 HBase 關鍵程式
HBase是一個 Master/Slave 架構的分散式資料庫,內部主要有 Master, RegionServer 兩個核心服務,依賴 HDFS 做底層儲存,依賴 zookeeper 做一致性等協調工作。
- Master 是一個輕量級程式,負責所有 DDL 操作,負載均衡, region 資訊管理,並在當機恢復中起主導作用。
- RegionServer 管理 HRegion,與客戶端點對點通訊,負責實時資料的讀寫,。
- zookeeper 做 HMaster 選舉,關鍵資訊如 meta-region 地址,replication 進度,Regionserver 地址與埠等儲存。
2.2 HBase 架構
首先給出架構圖如下

至此,我們對 HBase 的關鍵元件和它的角色以及架構有了一個大體的認識,下面重點介紹下 HBase 的讀路徑。
三、讀路徑解析
客戶端讀取資料有兩種方式, Get 與 Scan。 Get 是一種隨機點查的方式,根據 rowkey 返回一行資料,也可以在構造 Get 物件的時候傳入一個 rowkey 列表,這樣一次 RPC 請求可以返回多條資料。Get 物件可以設定列與 filter,只獲取特定 rowkey 下的指定列的資料、Scan 是範圍查詢,通過指定 Scan 物件的 startRow 與 endRow 來確定一次掃描的資料範圍,獲取該區間的所有資料。
一次由客戶端發起的完成的讀流程,可以分為兩個階段。第一個階段是客戶端如何將請求傳送到正確的 RegionServer 上,第二階段是 RegionServer 如何處理讀取請求。
3.1 客戶端如何傳送請求到指定的 RegionServer
HRegion 是管理一張表一塊連續資料區間的元件,而表是由多個 HRegion 組成,同時這些 HRegion 會在 RegionServer 上提供讀寫服務。所以客戶端傳送請求到指定的 RegionServer 上就需要知道 HRegion 的元資訊,這些元資訊儲存在 hbase:meta 這張系統表之內,這張表也在某一個 RegionServer 上提供服務,而這個資訊至關重要,是所有客戶端定位 HRegion 的基礎所在,所以這個對映資訊是儲存在 zookeeper 上面。 客戶端獲取 HRegion 元資訊流程圖如下:

3.2 RegionServer 處理讀請求
首先在 RegionServer 端,將 Get 請求當做特殊的一次 Scan 請求處理,其 startRow 和 StopRow 是一樣的,所以介紹 Scan 請求的處理就可以明白 Get 請求的處理流程了。
3.2.1 資料組織
讓我們回顧一下 HBase 資料的組織架構,首先 Table 橫向切割為多個 HRegion ,按照一個列族的情況,每一個 HRegion 之中包含一個 MemStore 和多個 HFile 檔案, HFile 檔案設計比較複雜,這裡不詳細展開,使用者需要知道給定一個 rowkey 可以根據索引結合二分查詢可以迅速定位到對應的資料塊即可。結合這些背景資訊,我們可以把一個Read請求的處理轉化下面的問題:如何從一個 MemStore,多個 HFile
中獲取到使用者需要的正確的資料(預設情況下是最新版本,非刪除,沒有過期的資料。同時使用者可能會設定 filter ,指定返回條數等過濾條件)
在 RegionServer 內部,會把讀取可能涉及到的所有元件都初始化為對應的 scanner 物件,針對 Region 的讀取,封裝為一個 RegionScanner 物件,而一個列族對應一個 Store,對應封裝為 StoreScanner,在 Store 內部,MemStore 則封裝為 MemStoreScanner,每一個 HFile 都會封裝為 StoreFileScanner 。最後資料的查詢就會落在對 MemStoreScanner 和 StoreFileScanner 上的查詢之上。
這些 scanner 首先根據 scan 的 TimeRange 和 Rowkey Range 會過濾掉一些,剩下的 scanner 在 RegionServer 內部組成一個最小堆 KeyValueHeap,該資料結構核心一個 PriorityQueue 優先順序佇列,佇列裡按照 Scanner 指向的 KeyValue 排序。
// 用來組織所有的Scanner
protected PriorityQueue<KeyValueScanner> heap = null;
// PriorityQueue當前排在最前面的Scanner
protected KeyValueScanner current = null;
複製程式碼
3.2.2 資料過濾
我們知道資料在記憶體以及 HDFS 檔案中儲存著,為了讀取這些資料,RegionServer 構造了若干 Scanner 並組成了一個最小堆,那麼如何遍歷這個堆去過濾資料返回使用者想要的值呢。 我們假設 HRegion 有4個 Hfile,1個 MemStore,那麼最小堆內有4個 scanner 物件,我們以 scannerA-D 來代替這些 scanner 物件,同時假設我們需要查詢的 rowkey 為 rowA。每一個 scanner 內部有一個 current 指標,指向的是當前需要遍歷的 KeyValue,所以這時堆頂部的 scanner 物件的 current 指標指向的就是 rowA(rowA:cf:colA)這條資料。通過觸發 next() 呼叫,移動 current 指標,來遍歷所有 scanner 中的資料。scanner 組織邏輯檢視如下圖所示。

第二次 next 請求,返回 ScannerA 中的 rowA:cf:colB,ScannerA 的 current 指標移動到下一個 KeyValue rowB:cf:ColA,因為堆按照 KeyValue 排序可知 rowB 小於 rowA, 所以堆內部,scanner 順序發生改變,改變之後如下圖所示

- keyValue型別為put
- 列是Scanner指定的列
- 滿足filter過濾條件
- 最新的版本
- 未刪除的資料
如果 scan 的引數更加複雜,條件也會發生變化,比如指定 scan 返回 Raw 資料的時候,打了刪除標記的資料也要被返回,這部分就不再詳細展開,至此讀流程基本解析完成,當然本文介紹的還是很粗略,有興趣的同學可以自己研究這一部分原始碼。
四、讀優化
在介紹讀流程之後,我們再結合有贊業務上的實踐來介紹如何優化讀請求,既然談到優化,就要先知道哪些點可會影響讀請求的效能,我們依舊從客戶端和服務端兩個方面來深入瞭解優化的方法。
4.1客戶端層面
HBase 讀資料共有兩種方式,Get 與 Scan。
在通用層面,在客戶端與服務端建連需要與 zookeeper 通訊,再通過 meta 表定位到 region 資訊,所以在初次讀取 HBase 的時候 rt 都會比較高,避免這個情況就需要客戶端針對表來做預熱,簡單的預熱可以通過獲取 table 所有的 region 資訊,再對每一個 region 傳送一個 Scan 或者 Get 請求,這樣就會快取 region 的地址;
rowkey 是否存在讀寫熱點,若出現熱點則失去分散式系統帶來的優勢,所有請求都只落到一個或幾個 HRegion 上,那麼請求效率一定不會高;
讀寫佔比是如何的。如果寫重讀輕,瀏覽服務端 RegionServer 日誌發現很多 MVCC STUCK 這樣的字樣,那麼會因為 MVCC 機制因為寫 Sync 到 WAL 不及時而阻塞讀,這部分機制比較複雜,考慮之後分享給大家,這裡不詳細展開。
4.1.1請求優化
- 將 Get 請求批量化,減少 rpc 次數,但如果一批次的 Get 數量過大,如果遇到磁碟毛刺或者 Split 毛刺,則 Get 會全部失敗(不會返回部分成功的結果),丟擲異常。
- 指定列族,識別符號。這樣可以服務端過濾掉很多無用的 scanner,減少 IO 次數,提高效率,該方法同樣適用於 Scan。
4.1.2 Scan 請求優化
- 設定合理的 startRow 與 stopRow 。如果 scan 請求不設定這兩個值,而只設定 filter,則會做全表掃描。
- 設定合理的 caching 數目, scan.setCaching(100)。 因為 Scan 潛在會掃描大量資料,因此客戶端發起一次 Scan 請求,實際並不會一次就將所有資料載入到本地,而是分成多次 RPC 請求進行載入。預設值是100。使用者如果確實需要掃描海量資料,同時不做邏輯分頁處理,那麼可以將快取值設定到1000,減少 rpc 次數,提升處理效率。如果使用者需要快速,迭代地獲取資料,那麼將 caching 設定為50或者100就合理。
4.2 服務端優化
相對於客戶端,服務端優化可做的比較多,首先我們列出有哪些點會影響服務端處理讀請求。
- gc 毛刺
- 磁碟毛刺
- HFile 檔案數目
- 快取配置
- 本地化率
- Hedged Read 模式是否開啟
- 短路讀是否開啟
- 是否做高可用
gc 毛刺沒有很好的辦法避免,通常 HBase 的一次 Young gc 時間在 20~30ms 之內。磁碟毛刺發生是無法避免的,通常 SATA 盤讀 IOPS 在 150 左右,SSD 盤隨機讀在 30000 以上,所以儲存介質使用 SSD 可以提升吞吐,變向降低了毛刺的影響。HFile 檔案數目因為 flush 機制而增加,因 Compaction 機制減少,如果 HFile 數目過多,那麼一次查詢可能經過更多 IO ,讀延遲就會更大。這部分調優主要是優化 Compaction 相關配置,包括觸發閾值,Compaction 檔案大小閾值,一次參與的檔案數量等等,這裡不再詳細展開。讀快取可以設定為為 CombinedBlockCache,調整讀快取與 MemStore 佔比對讀請求優化同樣十分重要,這裡我們配置 hfile.block.cache.size 為 0.4,這部分內容又會比較艱深複雜,同樣不再展開。下面結合業務需求講下我們做的優化實踐。
我們的線上叢集搭建伊始,接入了比較重要的粉絲業務,該業務對RT要求極高,為了滿足業務需求我們做了如下措施。
4.2.1 異構儲存
HBase 資源隔離+異構儲存。SATA 磁碟的隨機 iops 能力,單次訪問的 RT,讀寫吞吐上都遠遠不如 SSD,那麼對RT極其敏感業務來說,SATA盤並不能勝任,所以我們需要HBase有支援SSD儲存介質的能力。
為了 HBase 可以支援異構儲存,首先在 HDFS 層面就需要做響應的支援,在 HDFS 2.6.x 以及之後的版本,提供了對SSD上儲存檔案的能力,換句話說在一個 HDFS 叢集上可以有SSD和SATA磁碟並存,對應到 HDFS 儲存格式為 [ssd] 與 [disk]。然而 HBase 1.2.6 上並不能對錶的列族和 RegionServer 的 WAL 上設定其儲存格式為 [ssd], 該功能在社群 HBase 2.0 版本之後才開放出來,所以我們從社群 backport 了對應的 patch ,打到了我們有讚自己的 HBase 版本之上。支援 [ssd] 的 社群issue 如下: issues.apache.org/jira/browse… 。
新增SSD磁碟之後,HDFS叢集儲存架構示意圖如圖所示:


<property>
<name>dfs.datanode.data.dir</name>
<value>[SSD]file:/path/to/dfs/dn1</value>
</property>
複製程式碼
在 SSD 機型 的 RegionServer 中的 hbase-site.xml 中修改
<property>
<name>hbase.wal.storage.policy</name>
<value>ONE_SSD</value>
</property>
複製程式碼
其中ONE_SSD 也可以替代為 ALL_SSD。 SATA 機型的 RegionServer 則不需要修改或者改為 HOT 。
4.2.2 HDFS短路讀
該特性由 HDFS-2246 引入。我們叢集的 RegionServer 與 DataNode 混布,這樣的好處是資料有本地化率的保證,資料第一個副本會優先寫本地的 Datanode。在不開啟短路讀的時候,即使讀取本地的 DataNode 節點上的資料,也需要傳送RPC請求,經過層層處理最後返回資料,而短路讀的實現原理是客戶端向 DataNode 請求資料時,DataNode 會開啟檔案和校驗和檔案,將兩個檔案的描述符直接傳遞給客戶端,而不是將路徑傳遞給客戶端。客戶端收到兩個檔案的描述符之後,直接開啟檔案讀取資料,該特性是通過 UNIX Domain Socket程式間通訊方式實現,流程圖如圖所示:

開啟短路讀需要修改 hdfs-site.xml 檔案
<property>
<name>dfs.client.read.shortcircuit</name>
<value>true</value>
</property>
<property>
<name>dfs.domain.socket.path</name>
value>/var/run/hadoop/dn.socket</value>
</property>
複製程式碼
4.2.3 HDFS Hedged read
當我們通過短路讀讀取本地資料因為磁碟抖動或其他原因讀取資料一段時間內沒有返回,去向其他 DataNode 傳送相同的資料請求,先返回的資料為準,後到的資料拋棄,這也可以減少磁碟毛刺帶來的影響。預設該功能關閉,在HBase中使用此功能需要修改 hbase-site.xml
<property>
<name>dfs.client.hedged.read.threadpool.size</name>
<value>50</value>
</property>
<property>
<name>dfs.client.hedged.read.threshold.millis</name>
<value>100</value>
</property>
複製程式碼
執行緒池大小可以與讀handler的數目相同,而超時閾值不適宜調整的太小,否則會對叢集和客戶端都增加壓力。同時可以通過 Hadoop 監控檢視 hedgedReadOps 與 hedgedReadOps 兩個指標項,檢視啟用 Hedged read 的效果,前者表示發生了 Hedged read 的次數,後者表示 Hedged read 比原生讀要快的次數。
4.2.4 高可用讀
HBase是一個CP系統,同一個region同一時刻只有一個regionserver提供讀寫服務,這保證了資料的一致性,即不存在多副本同步的問題。但是如果一臺regionserver發聲當機的時候,系統需要一定的故障恢復時間deltaT, 這個deltaT時間內,region是不提供服務的。這個deltaT時間主要由當機恢復中需要回放的log的數目決定。叢集複製原理圖如下圖所示:


4.2.5 預熱失敗問題修復
應用冷啟動預熱不生效問題。該問題產生的背景在於應用初始化之後第一次訪問 HBase 讀取資料時候需要做定址,具體流程見圖2,這個過程涉及多次 RPC 請求,所以耗時較長。在快取下所有的 Region 地址之後,客戶端與 RegionServer 就會做點對點通訊,這樣 RT 就有所保證。所以我們會在應用啟動的時候做一次預熱操作,而預熱操作我們通常做法是呼叫方法 getAllRegionLocations 。在1.2.6版本getAllRegionLocations 存在 bug(後來經過筆者調研,1.3.x,以及2.x版本也都有類似問題),該方案預期返回所有的 Region locations 並且快取這些 Region 地址,但實際上,該方法只會快取 table 的第一個 Region, 筆者發現此問題之後反饋給社群,並提交了 patch 修復了此問題,issue連線:issues.apache.org/jira/browse… 。這樣通過呼叫修復 bug 之後的 getAllRegionLocations 方法,即可在應用啟動之後做好預熱,在應用第一次讀寫HBase時便不會產生 RT 毛刺。
粉絲業務主備超時時間都設定為 300ms。經過這些優化,其批量 Get 請求 99.99% 在 20ms 以內,99.9999% 在 400ms 以內。
五、總結
HBase 讀路徑相比寫路徑更加複雜,本文只是簡單介紹了核心思路。也正是因為這種複雜性,在考慮優化的時候需要深入瞭解其原理,且目光不能僅僅侷限於本身的服務元件,也要考慮其依賴的元件,是否也有可優化的點。最後,本人能力有限,文中觀點難免存在紕漏,還望交流指正。
最後打個小廣告,有贊大資料團隊基礎設施團隊,主要負責有讚的資料平臺(DP), 實時計算(Storm, Spark Streaming, Flink),離線計算(HDFS,YARN,HIVE, SPARK SQL),線上儲存(HBase),實時 OLAP(Druid) 等數個技術產品,歡迎感興趣的小夥伴聯絡 zhaoyuan@youzan.com
參考
www.nosqlnotes.com/technotes/h…
hbasefly.com/2016/11/11/
hadoop.apache.org/docs/stable…
www.cloudera.com/documentati…