有贊HBase 技術實踐:讀流程解析與優化

本文首先對 HBase 做簡單的介紹,包括其整體架構、依賴組件、核心服務類的相關解析。

有贊HBase 技術實踐:讀流程解析與優化

本文首先對 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 技術實踐:讀流程解析與優化

圖 1 HBase 架構圖

架構淺析: HBase 數據存儲基於 LSM 架構,數據先順序寫入 HLog,默認情況下 RegionServer 只有一個 Hlog 實例,之後再寫入 HRegion 的 MemStore 之中。HRegion 是一張 HBase 表的一塊數據連續的區域,數據按照 rowkey 字典序排列,RegionServer 管理這些 HRegion 。當 MemStore 達到閾值時觸發 flush 操作,刷寫為一個 HFile 文件,眾多 HFile 文件會週期性進行 major, minor compaction 合併成大文件。所有 HFile 與日誌文件都存儲在 HDFS 之上。

至此,我們對 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 元信息流程圖如下:

有贊HBase 技術實踐:讀流程解析與優化

圖 2 客戶端尋址圖


我們以單條 rowkey 的 Get 請求為例,當用戶初始化到 zookeeper 的連接之後,併發送一個 Get 請求時,需要先定位這條 rowkey 的 HRegion 地址。如果該地址不在緩存之中,就需要請求 zookeeper (箭頭 1),詢問 meta 表的地址。在獲取到 meta 表地址之後去讀取 meta 表的數據來根據 rowkey 定位到該 rowkey 屬於的 HRegion 信息和 RegionServer 的地址 (箭頭 2),緩存該地址併發 Get 請求點對點發送到對應的 RegionServer(箭頭 3),至此,客戶端定位發送請求的流程走通。

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;/<keyvaluescanner>

// 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 組織邏輯視圖如下圖所示。

有贊HBase 技術實踐:讀流程解析與優化

圖 3 scanner 組織邏輯視圖 A

第一次 next 請求,將會返回 ScannerA 中的 rowA:cf:colA,而後 ScannerA 的指針移動到下一個 KeyValue rowA:cf:colB,堆中的 Scanners 排序不變;

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

有贊HBase 技術實踐:讀流程解析與優化

圖 4 scanner 組織邏輯視圖 B

scanner 內部數據完全檢索之後會 close 掉,或者 rowA 所有數據檢索完畢,則查詢下一條。默認情況下返回的數據需要經過 ScanQueryMatcher 過濾返回的數據需要滿足下面的條件:

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 不及時而阻塞讀,這部分機制比較複雜,考慮之後分享給大家,這裡不詳細展開。

Get 請求優化

將 Get 請求批量化,減少 rpc 次數,但如果一批次的 Get 數量過大,如果遇到磁盤毛刺或者 Split 毛刺,則 Get 會全部失敗(不會返回部分成功的結果),拋出異常。

指定列族,標識符。這樣可以服務端過濾掉很多無用的 scanner,減少 IO 次數,提高效率,該方法同樣適用於 Scan。

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 如下:

https://issues.apache.org/jira/browse/HBASE-14061?jql=text ~ "storage policy" 。

添加 SSD 磁盤之後,HDFS 集群存儲架構示意圖如圖 5 所示:

有贊HBase 技術實踐:讀流程解析與優化

圖 5 混合機型下 HDFS 集群存儲邏輯示意圖

理想的混合機型集群異構部署,對於 HBase 層面來看,文件存儲可選三種策略:HOT, ONE_SSD, ALL_SSD, 其中 ONE_SSD 存儲策略既可以把三個副本中的兩個存儲到便宜的 SATA 磁盤介質之上來減少 SSD 磁盤存儲成本的開銷,同時在數據讀取訪問本地 SSD 磁盤上的數據可以獲得理想的 RT ,是一個十分理想的存儲策略。HOT 存儲策略與不引入異構存儲時的存儲情況沒有區別,而 ALL_SSD 將所有副本都存儲到 SSD 磁盤上。 在有讚我們目前沒有這樣的理想混合機型,只有純 SATA 與 純 SSD 兩種大數據機型,這樣的機型對應的架構與之前會有所區別,存儲架構示意圖如圖 6 所示。

有贊HBase 技術實踐:讀流程解析與優化

圖 6 純 SATA 與純 SSD 機型 HDFS 集群存儲架構邏輯示意圖

基於這樣的場景,我們做了如下規劃:

將 SSD 機器規劃成獨立的組,分組的 RegionServer 配置 hbase.wal.storage.policy=ONE_SSD, 保證 wal 本身的本地化率;

將 SSD 分組內的表配置成 ONE_SSD 或者 ALL_SSD;

非 SSD 分組內的表存儲策略使用默認的 HOT

具體的配置策略如下:在 hdfs-site.xml 中修改

 <property>

<name>dfs.datanode.data.dir/<name>

<value>[SSD]file:/path/to/dfs/dn1/<value>

在 SSD 機型 的 RegionServer 中的 hbase-site.xml 中修改

<property>

<name>hbase.wal.storage.policy/<name>

<value>ONE_SSD/<value>

其中 ONE_SSD 也可以替代為 ALL_SSD。 SATA 機型的 RegionServer 則不需要修改或者改為 HOT。

4.2.2 HDFS 短路讀

開啟 HDFS 的短路讀模式。該特性由 HDFS-2246 引入。我們集群的 RegionServer 與 DataNode 混布,這樣的好處是數據有本地化率的保證,數據第一個副本會優先寫本地的 Datanode。在不開啟短路讀的時候,即使讀取本地的 DataNode 節點上的數據,也需要發送 RPC 請求,經過層層處理最後返回數據,而短路讀的實現原理是客戶端向 DataNode 請求數據時,DataNode 會打開文件和校驗和文件,將兩個文件的描述符直接傳遞給客戶端,而不是將路徑傳遞給客戶端。客戶端收到兩個文件的描述符之後,直接打開文件讀取數據,該特性是通過 UNIX Domain Socket 進程間通信方式實現,流程圖如圖 7 所示。

有贊HBase 技術實踐:讀流程解析與優化

圖 7 短路讀流程圖

該特性內部實現比較複雜,設計到共享內存段通過 slot 放置副本的狀態與計數,這裡不再詳細展開。

開啟短路讀需要修改 hdfs-site.xml 文件:

<property>

<name>dfs.client.read.shortcircuit/<name>

<value>true/<value>

<property>

<name>dfs.domain.socket.path/<name>

value>/var/run/hadoop/dn.socket

4.2.3 HDFS Hedged Read

開啟 Hedged Read 模式。當我們通過短路讀讀取本地數據因為磁盤抖動或其他原因讀取數據一段時間內沒有返回,去向其他 DataNode 發送相同的數據請求,先返回的數據為準,後到的數據拋棄,這也可以減少磁盤毛刺帶來的影響。默認該功能關閉,在 HBase 中使用此功能需要修改 hbase-site.xml

<property>

<name>dfs.client.hedged.read.threadpool.size/<name>

<value>50/<value>

<property>

<name>dfs.client.hedged.read.threshold.millis/<name>

<value>100/<value>

線程池大小可以與讀 handler 的數目相同,而超時閾值不適宜調整的太小,否則會對集群和客戶端都增加壓力。同時可以通過 Hadoop 監控查看 hedgedReadOps 與 hedgedReadOps 兩個指標項,查看啟用 Hedged read 的效果,前者表示發生了 Hedged read 的次數,後者表示 Hedged read 比原生讀要快的次數。

4.2.4 高可用讀

高可用讀。HBase 是一個 CP 系統,同一個 region 同一時刻只有一個 regionserver 提供讀寫服務,這保證了數據的一致性,即不存在多副本同步的問題。但是如果一臺 regionserver 發聲宕機的時候,系統需要一定的故障恢復時間 deltaT, 這個 deltaT 時間內,region 是不提供服務的。這個 deltaT 時間主要由宕機恢復中需要回放的 log 的數目決定。集群複製原理圖如下圖 8 所示。

有贊HBase 技術實踐:讀流程解析與優化

圖 8 HBase Replication 原理圖

HBase 提供了 HBase Replication 機制,用來實現集群間單方向的異步數據複製我們線上部署了雙集群,備集群 SSD 分組和主集群 SSD 分組有相同的配置。當主集群因為磁盤,網絡,或者其他業務突發流量影響導致某些 RegionServer 甚至集群不可用的時候,就需要提供備集群繼續提供服務,備集群的數據可能會因為 HBase Replication 機制的延遲,相比主集群的數據是滯後的,按照我們集群目前的規模統計,平均延遲在 100ms 以內。所以為了達到高可用,粉絲業務可以接受複製延遲,放棄了強一致性,選擇了最終一致性和高可用性,在第一版採用的方案如下:

有贊HBase 技術實踐:讀流程解析與優化

圖 9 客戶端高可用方案原理圖

粉絲業務方是不想感知到後端服務的狀態,也就是說在客戶端層面,他們只希望一個 Put 或者 Get 請求正常送達且返回預期的數據即可,那麼就需要高可用客戶端封裝一層降級,熔斷處理的邏輯,這裡我們採用 Hystrix 做為底層熔斷處理引擎,在引擎之上封裝了 HBase 的基本 API,用戶只需要配置主備機房的 ZK 地址即可,所有的降級熔斷邏輯最終封裝到 ha-hbase-client 中,原理類似圖 9,這裡不再贅述。

4.2.5 預熱失敗問題修復

應用冷啟動預熱不生效問題。該問題產生的背景在於應用初始化之後第一次訪問 HBase 讀取數據時候需要做尋址,具體流程見圖 2,這個過程涉及多次 RPC 請求,所以耗時較長。在緩存下所有的 Region 地址之後,客戶端與 RegionServer 就會做點對點通信,這樣 RT 就有所保證。所以我們會在應用啟動的時候做一次預熱操作,而預熱操作我們通常做法是調用方法 getAllRegionLocations。在 1.2.6 版本(後來經過筆者調研,1.3.x,以及 2.x 版本)getAllRegionLocations 存在 bug ,該方案預期返回所有的 Region locations 並且緩存這些 Region 地址,但實際上,該方法只會緩存 table 的第一個 Region, 筆者發現此問題之後反饋給社區,並提交了 patch 修復了此問題,issue 連接:https://issues.apache.org/jira/browse/HBASE-20697?filter=-2 。這樣通過調用修復 bug 之後的 getAllRegionLocations 方法,即可在應用啟動之後做好預熱,在應用第一次讀寫 HBase 時便不會產生 RT 毛刺。

粉絲業務主備超時時間都設置為 300ms。經過這些優化,其批量 Get 請求 99.99% 在 20ms 以內,99.9999% 在 400ms 以內。


總結

HBase 讀路徑相比寫路徑更加複雜,本文只是簡單介紹了核心思路。也正是因為這種複雜性,在考慮優化的時候需要深入瞭解其原理,且目光不能僅僅侷限於本身的服務組件,也要考慮其依賴的組件,是否也有可優化的點。

參考

http://www.nosqlnotes.com/technotes/hbase/hbase-read/

http://hbasefly.com/2016/11/11/

http://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/ShortCircuitLocalReads.html

https://www.cloudera.com/documentation/enterprise/5-6-x/topics/admin_hedged_reads.html


分享到:


相關文章: