本文首先對 HBase 作簡單的介紹,包括其總體架構、依賴組件、核心服務類的相關解析。再重點介紹 HBase 讀取數據的流程分析,並根據此流程介紹如何在客戶端以及服務端優化性能,同時結合有贊線上 HBase 集羣的實際應用狀況,將理論和實踐結合,但願能給讀者帶來啓發。如文章有紕漏請在下面留言,咱們共同探討共同窗習。html
HBase 是一個分佈式,可擴展,面向列的適合存儲海量數據的數據庫,其最主要的功能是解決海量數據下的實時隨機讀寫的問題。 一般 HBase 依賴 HDFS 作爲底層分佈式文件系統,本文以此作前提並展開,詳細介紹 HBase 的架構,讀路徑以及優化實踐。node
HBase是一個 Master/Slave 架構的分佈式數據庫,內部主要有 Master, RegionServer 兩個核心服務,依賴 HDFS 作底層存儲,依賴 zookeeper 作一致性等協調工做。sql
首先給出架構圖以下數據庫
架構淺析: HBase 數據存儲基於 LSM 架構,數據先順序寫入 HLog,默認狀況下 RegionServer 只有一個 Hlog 實例,以後再寫入 HRegion 的 MemStore 之中。 HRegion 是一張 HBase 表的一塊數據連續的區域,數據按照 rowkey 字典序排列,RegionServer 管理這些 HRegion 。當MemStore達到閾值時觸發flush操做,刷寫爲一個 HFile 文件,衆多 HFile 文件會週期性進行 major, minor compaction 合併成大文件。全部 HFile 與日誌文件都存儲在HDFS之上。客戶端讀取數據有兩種方式, Get 與 Scan。 Get 是一種隨機點查的方式,根據 rowkey 返回一行數據,也能夠在構造 Get 對象的時候傳入一個 rowkey 列表,這樣一次 RPC 請求能夠返回多條數據。Get 對象能夠設置列與 filter,只獲取特定 rowkey 下的指定列的數據、Scan 是範圍查詢,經過指定 Scan 對象的 startRow 與 endRow 來肯定一次掃描的數據範圍,獲取該區間的全部數據。
一次由客戶端發起的完成的讀流程,能夠分爲兩個階段。第一個階段是客戶端如何將請求發送到正確的 RegionServer 上,第二階段是 RegionServer 如何處理讀取請求。apache
HRegion 是管理一張表一塊連續數據區間的組件,而表是由多個 HRegion 組成,同時這些 HRegion 會在 RegionServer 上提供讀寫服務。因此客戶端發送請求到指定的 RegionServer 上就須要知道 HRegion 的元信息,這些元信息保存在 hbase:meta 這張系統表以內,這張表也在某一個 RegionServer 上提供服務,而這個信息相當重要,是全部客戶端定位 HRegion 的基礎所在,因此這個映射信息是存儲在 zookeeper 上面。 客戶端獲取 HRegion 元信息流程圖以下: 後端
咱們以單條 rowkey 的 Get 請求爲例,當用戶初始化到 zookeeper 的鏈接以後,併發送一個 Get 請求時,須要先定位這條 rowkey 的 HRegion 地址。若是該地址不在緩存之中,就須要請求 zookeeper (箭頭1),詢問 meta 表的地址。在獲取到 meta 表地址以後去讀取 meta 表的數據來根據 rowkey 定位到該 rowkey 屬於的 HRegion 信息和 RegionServer 的地址(箭頭2),緩存該地址併發 Get 請求點對點發送到對應的 RegionServer(箭頭3),至此,客戶端定位發送請求的流程走通。首先在 RegionServer 端,將 Get 請求當作特殊的一次 Scan 請求處理,其 startRow 和 StopRow 是同樣的,因此介紹 Scan 請求的處理就能夠明白 Get 請求的處理流程了。緩存
讓咱們回顧一下 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;
複製代碼
咱們知道數據在內存以及 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:colA,然後 ScannerA 的指針移動到下一個 KeyValue rowA:cf:colB,堆中的 Scanners 排序不變;若是 scan 的參數更加複雜,條件也會發生變化,好比指定 scan 返回 Raw 數據的時候,打了刪除標記的數據也要被返回,這部分就再也不詳細展開,至此讀流程基本解析完成,固然本文介紹的仍是很粗略,有興趣的同窗能夠本身研究這一部分源碼。架構
在介紹讀流程以後,咱們再結合有贊業務上的實踐來介紹如何優化讀請求,既然談到優化,就要先知道哪些點可會影響讀請求的性能,咱們依舊從客戶端和服務端兩個方面來深刻了解優化的方法。
HBase 讀數據共有兩種方式,Get 與 Scan。
在通用層面,在客戶端與服務端建連須要與 zookeeper 通訊,再經過 meta 表定位到 region 信息,因此在初次讀取 HBase 的時候 rt 都會比較高,避免這個狀況就須要客戶端針對表來作預熱,簡單的預熱能夠經過獲取 table 全部的 region 信息,再對每個 region 發送一個 Scan 或者 Get 請求,這樣就會緩存 region 的地址;
rowkey 是否存在讀寫熱點,若出現熱點則失去分佈式系統帶來的優點,全部請求都只落到一個或幾個 HRegion 上,那麼請求效率必定不會高; 讀寫佔比是如何的。若是寫重讀輕,瀏覽服務端 RegionServer 日誌發現不少 MVCC STUCK 這樣的字樣,那麼會由於 MVCC 機制由於寫 Sync 到 WAL 不及時而阻塞讀,這部分機制比較複雜,考慮以後分享給你們,這裏不詳細展開。
相對於客戶端,服務端優化可作的比較多,首先咱們列出有哪些點會影響服務端處理讀請求。
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要求極高,爲了知足業務需求咱們作了以下措施。
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 。
該特性由 HDFS-2246 引入。咱們集羣的 RegionServer 與 DataNode 混布,這樣的好處是數據有本地化率的保證,數據第一個副本會優先寫本地的 Datanode。在不開啓短路讀的時候,即便讀取本地的 DataNode 節點上的數據,也須要發送RPC請求,通過層層處理最後返回數據,而短路讀的實現原理是客戶端向 DataNode 請求數據時,DataNode 會打開文件和校驗和文件,將兩個文件的描述符直接傳遞給客戶端,而不是將路徑傳遞給客戶端。客戶端收到兩個文件的描述符以後,直接打開文件讀取數據,該特性是經過 UNIX Domain Socket進程間通訊方式實現,流程圖如圖所示:
該特性內部實現比較複雜,設計到共享內存段經過 slot 放置副本的狀態與計數,這裏再也不詳細展開。開啓短路讀須要修改 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>
複製代碼
當咱們經過短路讀讀取本地數據由於磁盤抖動或其餘緣由讀取數據一段時間內沒有返回,去向其餘 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 比原生讀要快的次數。
HBase是一個CP系統,同一個region同一時刻只有一個regionserver提供讀寫服務,這保證了數據的一致性,即不存在多副本同步的問題。可是若是一臺regionserver發聲宕機的時候,系統須要必定的故障恢復時間deltaT, 這個deltaT時間內,region是不提供服務的。這個deltaT時間主要由宕機恢復中須要回放的log的數目決定。集羣複製原理圖以下圖所示:
HBase提供了HBase Replication機制,用來實現集羣間單方向的異步數據複製咱們線上部署了雙集羣,備集羣 SSD 分組和主集羣 SSD 分組有相同的配置。當主集羣由於磁盤,網絡,或者其餘業務突發流量影響致使某些 RegionServer 甚至集羣不可用的時候,就須要提供備集羣繼續提供服務,備集羣的數據可能會由於 HBase Replication 機制的延遲,相比主集羣的數據是滯後的,按照咱們集羣目前的規模統計,平均延遲在 100ms 之內。因此爲了達到高可用,粉絲業務能夠接受複製延遲,放棄了強一致性,選擇了最終一致性和高可用性,在初版採用的方案以下: 粉絲業務方不想感知到後端服務的狀態,也就是說在客戶端層面,他們只但願一個 Put 或者 Get 請求正常送達且返回預期的數據便可,那麼就須要高可用客戶端封裝一層降級,熔斷處理的邏輯,這裏咱們採用 Hystrix 作爲底層熔斷處理引擎,在引擎之上封裝了 HBase 的基本 API,用戶只須要配置主備機房的 ZK 地址便可,全部的降級熔斷邏輯最終封裝到 ha-hbase-client 中,原理相似圖9,這裏再也不贅述。應用冷啓動預熱不生效問題。該問題產生的背景在於應用初始化以後第一次訪問 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…