HBase最佳實踐

本文致力於從架構原理、集羣部署、性能優化與使用技巧等方面,闡述在如何基於HBase構建 容納大規模數據、支撐高併發、毫秒響應、穩定高效的OLTP實時系統

一、架構原理

1.1 基本架構

在這裏插入圖片描述
從上層往下可以看到HBase架構中的角色分配爲:

  • Client
  • Zookeeper
  • HMaster
  • RegionServer
  • HDFS

Client

Client是執行查詢、寫入等對HBase表數據進行增刪改查的使用方,可以是使用HBase Client API編寫的程序,也可以是其他開發好的HBase客戶端應用。

Zookeeper

同HDFS一樣,HBase使用Zookeeper作爲集羣協調與管理系統。

在HBase中其主要的功能與職責爲:

  • 存儲整個集羣HMaster與RegionServer的運行狀態
  • 實現HMaster的故障恢復與自動切換
  • 爲Client提供元數據表的存儲信息

HMaster、RegionServer啓動之後將會在Zookeeper上註冊並創建節點(/hbasae/master 與 /hbase/rs/*),同時 Zookeeper 通過Heartbeat的心跳機制來維護與監控節點狀態,一旦節點丟失心跳,則認爲該節點宕機或者下線,將清除該節點在Zookeeper中的註冊信息。

當Zookeeper中任一RegionServer節點狀態發生變化時,HMaster都會收到通知,並作出相應處理,例如RegionServer宕機,HMaster重新分配Regions至其他RegionServer以保證集羣整體可用性。

當HMaster宕機時(Zookeeper監測到心跳超時),Zookeeper中的 /hbasae/master 節點將會消失,同時Zookeeper通知其他備用HMaster節點,重新創建 /hbasae/master 並轉化爲active master。

協調過程示意圖如下:
在這裏插入圖片描述
除了作爲集羣中的協調者,Zookeeper還爲Client提供了 hbase:meta 表的存儲信息

客戶端要訪問HBase中的數據,只需要知道Zookeeper集羣的連接信息,訪問步驟如下:

  • 客戶端將從Zookeeper(/hbase/meta-region-server)獲得 hbase:meta 表存儲在哪個RegionServer,緩存該位置信息
  • 查詢該RegionServer上的 hbase:meta 表數據,查找要操作的 rowkey所在的Region存儲在哪個RegionServer中,緩存該位置信息
  • 在具體的RegionServer上根據rowkey檢索該Region數據

可以看到,客戶端操作數據過程並不需要HMaster的參與,通過Zookeeper間接訪問RegionServer來操作數據。

第一次請求將會產生3次RPC,之後使用相同的rowkey時客戶端將直接使用緩存下來的位置信息,直接訪問RegionServer,直至緩存失效(Region失效、遷移等原因)。

通過Zookeeper的讀寫流程如下:

在這裏插入圖片描述

hbase:meta 表

hbase:meta 表存儲了集羣中所有Region的位置信息

表結構如下:

在這裏插入圖片描述

  • Rowkey格式:tableName,regionStartKey,regionId
    • 第一個region的regionStartKey爲空
    • 示例:ns1:testTable,xxxxreigonid
  • 只有一個列族info,包含三個列:
    • regioninfo:RegionInfo的proto序列化格式,包含regionId,tableName,startKey,endKey,offline,split,replicaId等信息
    • server:RegionServer對應的server:port
    • serverstartcode:RegionServer的啓動時間戳

簡單總結Zookeeper在HBase集羣中的作用如下:

  • 對於服務端,是實現集羣協調與控制的重要依賴
  • 對於客戶端,是查詢與操作數據必不可少的一部分

HMaster

在這裏插入圖片描述
HBase整體架構中HMaster的功能與職責如下:

  • 管理RegionServer,監聽其狀態,保證集羣負載均衡且高可用
  • 管理Region,如新Region的分配、RegionServer宕機時該節點Region的分配與遷移
  • 接收客戶端的DDL操作,如創建與刪除表、列簇等信息
  • 權限控制

如我們前面所說的,HMaster 通過 Zookeeper 實現對集羣中各個 RegionServer 的監控與管理,在 RegionServer 發生故障時可以發現節點宕機轉移 Region 至其他節點,以保證服務的可用性

但是HBase的故障轉移並不是無感知的,相反故障轉移過程中,可能會直接影響到線上請求的穩定性,造成段時間內的大量延遲。

在分佈式系統的 CAP定理 中(Consistency一致性、Availability可用性、Partition tolerance分區容錯性),分佈式數據庫基本特性都會實現P,但是不同的數據庫對於A和C各有取捨。如HBase選擇了C,而通過Zookeeper這種方式來輔助實現A(雖然會有一定缺陷),而Cassandra選擇了A,通過其他輔助措施實現了C,各有優劣。

對於HBase集羣來說,HMaster是一個內部管理者,除了DDL操作並不對外(客戶端)開放,因而HMaster的負載是比較低的。

造成HMaster壓力大的情況可能是集羣中存在多個(兩個或者三個以上)HMaster,備用的Master會定期與Active Master通信以獲取最新的狀態信息,以保證故障切換時自身的數據狀態是最新的,因而Active Master可能會收到大量來自備用Master的數據請求。

RegionServer

RegionServer在HBase集羣中的功能與職責:

  • 根據HMaster的region分配請求,存放和管理Region
  • 接受客戶端的讀寫請求,檢索與寫入數據,產生大量IO

一個RegionServer中存儲並管理者多個Region,是HBase集羣中真正 存儲數據、接受讀寫請求 的地方,是HBase架構中最核心、同時也是最複雜的部分。

RegionServer內部結構圖如下:
在這裏插入圖片描述

BlockCache

BlockCache爲RegionServer中的 讀緩存,一個RegionServer共用一個BlockCache。

RegionServer處理客戶端讀請求的過程:

1. 在BlockCache中查詢是否命中緩存
2. 緩存未命中則定位到存儲該數據的Region
3. 檢索Region Memstore中是否有所需要的數據
4. Memstore中未查得,則檢索Hfiles
5. 任一過程查詢成功則將數據返回給客戶端並緩存至BlockCache

BlockCache有兩種實現方式,有不同的應用場景,各有優劣:

  • On-Heap的LRUBlockCache
    • 優點:直接中Java堆內內存獲取,響應速度快
    • 缺陷:容易受GC影響,響應延遲不穩定,特別是在堆內存巨大的情況下
    • 適用於:寫多讀少型、小內存等場景
  • Off-Heap的BucketCache
    • 優點:無GC影響,延遲穩定
    • 缺陷:從堆外內存獲取數據,性能略差於堆內內存
    • 適用於:讀多寫少型、大內存等場景

我們將在「性能優化」一節中具體討論如何判斷應該使用哪種內存模式。

WAL

全稱 Write Ahead Log ,是 RegionServer 中的預寫日誌。

所有寫入數據默認情況下都會先寫入WAL中,以保證RegionServer宕機重啓之後可以通過WAL來恢復數據,一個RegionServer中共用一個WAL。

RegionServer的寫流程如下:

1. 將數據寫入WAL中
2. 根據TableName、Rowkey和ColumnFamily將數據寫入對應的Memstore中
3. Memstore通過特定算法將內存中的數據刷寫成Storefile寫入磁盤,並標記WAL sequence值
4. Storefile定期合小文件

WAL會通過 日誌滾動 的操作定期對日誌文件進行清理(已寫入HFile中的數據可以清除),對應HDFS上的存儲路徑爲 /hbase/WALs/${HRegionServer_Name}

Region

一個Table由一個或者多個Region組成,一個Region中可以看成是Table按行切分且有序的數據塊,每個Region都有自身的StartKey、EndKey。

一個Region由一個或者多個Store組成,每個Store存儲該Table對應Region中一個列簇的數據,相同列簇的列存儲在同一個Store中。

同一個Table的Region會分佈在集羣中不同的RegionServer上以實現讀寫請求的負載均衡。故,一個RegionServer中將會存儲來自不同Table的N多個Region。

Store、Region與Table的關係可以表述如下:多個Store(列簇)組成Region,多個Region(行數據塊)組成完整的Table。

其中,Store由Memstore(內存)、StoreFile(磁盤)兩部分組成。

在RegionServer中,Memstore可以看成指定Table、Region、Store的寫緩存(正如BlockCache小節中所述,Memstore還承載了一些讀緩存的功能),以RowKey、Column Family、Column、Timestamp進行排序。如下圖所示:
在這裏插入圖片描述
寫請求到RegionServer之後並沒有立刻寫入磁盤中,而是先寫入內存中的Memstore(內存中數據丟失問題可以通過回放WAL解決)以提升寫入性能。

Region中的Memstore會根據特定算法將內存中的數據將會刷寫到磁盤形成Storefile文件,因爲數據在Memstore中爲已排序,順序寫入磁盤性能高、速度快。

在這種 Log-Structured Merge Tree架構模式隨機寫入 HBase擁有相當高的性能。

Memstore刷磁盤形成的StoreFile 以HFile格式存儲HBase的KV數據 於HDFS之上。

HDFS

HDFS爲HBase提供底層存儲系統,通過HDFS的高可用、高可靠等特性,保障了HBase的數據安全、容災與備份

1.2 寫數據 與 Memstore Flush

對於客戶端來說,將請求發送到需要寫入的RegionServer中,等待RegionServer寫入WAL、Memstore之後即返回寫入成功的ack信號。

對於RegionServer來說,寫入的數據還需要經過一系列的處理步驟。

首先我們知道Memstore是在內存中的,將數據放在內存中可以得到優異的讀寫性能,但是同樣也會帶來麻煩:

  • 內存中的數據如何防止斷電丟失
  • 將數據存儲於內存中的代價是高昂的,空間總是有限的

對於第一個問題,雖然可以通過WAL機制在重啓的時候進行數據回放,但是對於第二個問題,則必須將內存中的數據持久化到磁盤中

在不同情況下,RegionServer通過不同級別的刷寫策略對Memstore中的數據進行持久化,根據觸發刷寫動作的時機以及影響範圍,可以分爲不同的幾個級別:

  • **Memstore級別:**Region中任意一個MemStore達到了 hbase.hregion.memstore.flush.size 控制的上限(默認128MB),會觸發Memstore的flush。
  • **Region級別:**Region中Memstore大小之和達到了 hbase.hregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size 控制的上限(默認 2 * 128M = 256M),會觸發Memstore的flush。
  • **RegionServer級別:**Region Server中所有Region的Memstore大小總和達到了 hbase.regionserver.global.memstore.upperLimit * hbase_heapsize 控制的上限(默認0.4,即RegionServer 40%的JVM內存),將會按Memstore由大到小進行flush,直至總體Memstore內存使用量低於 hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize 控制的下限(默認0.38, 即RegionServer 38%的JVM內存)。
  • **RegionServer中HLog數量達到上限:**將會選取最早的 HLog對應的一個或多個Region進行flush(通過參數hbase.regionserver.maxlogs配置)。
  • **HBase定期flush:**確保Memstore不會長時間沒有持久化,默認週期爲1小時。爲避免所有的MemStore在同一時間都進行flush導致的問題,定期的flush操作有20000左右的隨機延時。
  • **手動執行flush:**用戶可以通過shell命令 flush ‘tablename’或者flush ‘region name’分別對一個表或者一個Region進行flush。

Memstore刷寫時會阻塞線上的請求響應,由此可以看到,不同級別的刷寫對線上的請求會造成不同程度影響的延遲:

  • 對於Memstore與Region級別的刷寫,速度是比較快的,並不會對線上造成太大影響
  • 對於RegionServer級別的刷寫,將會阻塞發送到該RegionServer上的所有請求,直至Memstore刷寫完畢,會產生較大影響

所以在Memstore的刷寫方面,需要儘量避免出現RegionServer級別的刷寫動作。

數據在經過Memstore刷寫到磁盤時,對應的會寫入WAL sequence的相關信息,已經持久化到磁盤的數據就沒有必要通過WAL記錄的必要

RegionServer會根據這個sequence值對WAL日誌進行滾動清理,防止WAL日誌數量太多,RegionServer啓動時加載太多數據信息。

同樣,在Memstore的刷寫策略中可以看到,爲了防止WAL日誌數量太多,達到指定閾值之後將會選擇WAL記錄中最早的一個或者多個Region進行刷寫。

1.3 讀數據 與 Bloom Filter

經過前文的瞭解,我們現在可以知道HBase中一條數據完整的讀取操作流程中,Client會和Zookeeper、RegionServer等發生多次交互請求。

基於HBase的架構,一條數據可能存在RegionServer中的三個不同位置:

  • 對於剛讀取過的數據,將會被緩存到BlockCache
  • 對於剛寫入的數據,其存在Memstore
  • 對於之前已經從Memstore刷寫到磁盤的,其存在於HFiles

RegionServer接收到的一條數據查詢請求,只需要從以上三個地方檢索到數據即可,在HBase中的檢索順序依次是:BlockCache -> Memstore -> HFiles

其中,BlockCache、Memstore都是直接在內存中進行高性能的數據檢索

而HFiles則是真正存儲在HDFS上的數據:

  • 檢索HFiles時會產生真實磁盤的IO操作
  • Memstore不停刷寫的過程中,將會產生大量的HFile

如何在大量的HFile中快速找到所需要的數據呢?

爲了提高檢索HFiles的性能,HBase支持使用 Bloom Fliter 對HFiles進行快讀定位

Bloom Filter(布隆過濾器)是一種數據結構,常用於大規模數據查詢場景,其能夠快速判斷一個元素一定不在集合中,或者可能在集合中

Bloom Filter由 一個長度爲m的位數組k個哈希函數 組成。

其工作原理如下:

  1. 原始集合寫入一個元素時,Bloom Filter同時將該元素 經過k個哈希函數映射成k個數字,並以這些數字爲下標,將 位數組 中對應下標的元素標記爲1
  2. 當需要判斷一個元素是否存在於原始集合中,只需要將該元素經過同樣的 k個哈希函數得到k個數字
    • 取 位數組 中對應下標的元素,如果都爲1,則表示元素可能存在
    • 如果存在其中一個元素爲0,則該元素不可能存在於原始集合中
  3. 因爲哈希碰撞問題,不同的元素經過相同的哈希函數之後可能得到相同的值
    • 對於集合外的一個元素,如果經過 k個函數得到的k個數字,對應位數組中的元素都爲1,可能是該元素存在於集合中
    • 也有可能是集合中的其他元素”碰巧“讓這些下標對應的元素都標記爲1,所以只能說其可能存在
  4. 對於集合中的不同元素,如果 經過k個函數得到的k個數字中,任意一個重複
    • 位數組 中對應下標的元素會被覆蓋,此時該下標的元素不能被刪除(即歸零)
    • 刪除可能會導致其他多個元素在Bloom Filter表示不“存在”

由此可見,Bloom Filter中:

  • 位數組的長度m越大,誤差率越小,而存儲代價越大

  • 哈希函數的個數k越多,誤差率越小,而性能越低
    在這裏插入圖片描述
    HBase中支持使用以下兩種Bloom Filter:

  • ROW:基於 Rowkey 創建的Bloom Filter

  • ROWCOL:基於 Rowkey+Column 創建的Bloom Filter

兩者的區別僅僅是:是否使用列信息作爲Bloom Filter的條件

使用ROWCOL時,可以讓指定列的查詢更快,因爲其通過Rowkey與列信息來過濾不存在數據的HFile,但是相應的,產生的Bloom Filter數據會更加龐大

而只通過Rowkey進行檢索的查詢,即使指定了ROWCOL也不會有其他效果,因爲沒有攜帶列信息。

通過Bloom Filter(如果有的話)快速定位到當前的Rowkey數據存儲於哪個HFile之後(或者不存在直接返回),通過HFile攜帶的 Data Block Index 等元數據信息可快速定位到具體的數據塊起始位置,讀取並返回(加載到緩存中)。

這就是Bloom Filter在HBase檢索數據的應用場景:

  1. 高效判斷key是否存在
  2. 高效定位key所在的HFile

當然,如果沒有指定創建Bloom Filter,RegionServer將會花費比較多的力氣一個個檢索HFile來判斷數據是否存在。

1.4 HFile存儲格式

通過Bloom Filter快速定位到需要檢索的數據所在的HFile之後的操作自然是從HFile中讀出數據並返回。

據我們所知,HFile是HDFS上的文件(或大或小都有可能),現在HBase面臨的一個問題就是如何在HFile中 快速檢索獲得指定數據

HBase隨機查詢的高性能很大程度上取決於底層HFile的存儲格式,所以這個問題可以轉化爲 HFile的存儲格式該如何設計,才能滿足HBase 快速檢索 的需求。

生成一個HFile

Memstore內存中的數據在刷寫到磁盤時,將會進行以下操作:

  • 會先現在內存中創建 空的Data Block數據塊 包含 預留的Header空間。而後,將Memstore中的KVs一個個順序寫滿該Block(一般默認大小爲64KB)。
  • 如果指定了壓縮或者加密算法,Block數據寫滿之後將會對整個數據區做相應的壓縮或者加密處理。
  • 隨後在預留的Header區寫入該Block的元數據信息,如 壓縮前後大小、上一個block的offset、checksum 等。
  • 內存中的準備工作完成之後,通過HFile Writer輸出流將數據寫入到HDFS中,形成磁盤中的Data Block。
  • 爲輸出的Data Block生成一條索引數據,包括 {startkey、offset、size} 信息,該索引數據會被暫時記錄在內存中的Block Index Chunk中。

至此,已經完成了第一個Data Block的寫入工作,Memstore中的 KVs 數據將會按照這個過程不斷進行 寫入內存中的Data Block -> 輸出到HDFS -> 生成索引數據保存到內存中的Block Index Chunk 流程。

值得一提的是,如果啓用了Bloom Filter,那麼 Bloom Filter Data(位圖數據)Bloom元數據(哈希函數與個數等) 將會和 KVs 數據一樣被處理:寫入內存中的Block -> 輸出到HDFS Bloom Data Block -> 生成索引數據保存到相對應的內存區域中

由此我們可以知道,HFile寫入過程中,Data Block 和 Bloom Data Block 是交叉存在的

隨着輸出的Data Block越來越多,內存中的索引數據Block Index Chunk也會越來越大。

達到一定大小之後(默認128KB)將會經過類似Data Block的輸出流程寫入到HDFS中,形成 Leaf Index Block (和Data Block一樣,Leaf Index Block也有對應的Header區保留該Block的元數據信息)。

同樣的,也會生成一條該 Leaf Index Block 對應的索引記錄,保存在內存中的 Root Block Index Chunk

Root Index -> Leaf Data Block -> Data Block 的索引關係類似 B+樹 的結構。得益於多層索引,HBase可以在不讀取整個文件的情況下查找數據。

隨着內存中最後一個 Data Block、Leaf Index Block 寫入到HDFS,形成 HFile 的 Scanned Block Section

Root Block Index Chunk 也會從內存中寫入HDFS,形成 HFile 的 Load-On-Open Section 的一部分。

至此,一個完整的HFile已經生成,如下圖所示:
在這裏插入圖片描述

檢索HFile

生成HFile之後該如何使用呢?

HFile的索引數據(包括 Bloom Filter索引和數據索引信息)會在 Region Open 的時候被加載到讀緩存中,之後數據檢索經過以下過程:

  • 所有的讀請求,如果讀緩存和Memstore中不存在,那麼將會檢索HFile索引
  • 通過Bloom Filter索引(如果有設置Bloom Filter的話)檢索Bloom Data以 快速定位HFile是否存在 所需數據
  • 定位到數據可能存在的HFile之後,讀取該HFile的 三層索引數據,檢索數據是否存在
  • 存在則根據索引中的 元數據 找到具體的 Data Block 讀入內存,取出所需的KV數據

可以看到,在HFile的數據檢索過程中,一次讀請求只有 真正確認數據存在 且 需要讀取硬盤數據的時候纔會 執行硬盤查詢操作

同時,得益於 分層索引分塊存儲,在Region Open加載索引數據的時候,再也不必和老版本(0.9甚至更早,HFile只有一層數據索引並且統一存儲)一樣加載所有索引數據到內存中導致啓動緩慢甚至卡機等問題。
在這裏插入圖片描述

1.5 HFile Compaction

Bloom Filter解決了如何在大量的HFile中快速定位數據所在的HFile文件,雖然有了Bloom Filter的幫助大大提升了檢索效率,但是對於RegionServer來說 要檢索的HFile數量並沒有減少

爲了再次提高HFile的檢索效率,同時避免大量小文件的產生造成性能低下,RegionServer會通過 Compaction機制 對HFile進行合併操作

常見的Compaction觸發方式有:

  • Memstore Flush檢測條件執行
  • RegionServer定期檢查執行
  • 用戶手動觸發執行

Minor Compaction

Minor Compaction 只執行簡單的文件合併操作,選取較小的HFiles,將其中的數據順序寫入新的HFile後,替換老的HFiles。

但是如何在衆多HFiles中選擇本次Minor Compaction要合併的文件卻有不少講究:

  • 首先排除掉文件大小 大於 hbase.hstore.compaction.max.size 值的HFile
  • 將HFiles按照 文件年齡排序(older to younger),並從older file開始選擇
  • 如果該文件大小 小於 hbase.hstore.compaction.min 則加入Minor Compaction中
  • 如果該文件大小 小於 後續hbase.hstore.compaction.max 個HFile大小之和 * hbase.hstore.compaction.ratio,則將該文件加入Minor Compaction中
  • 掃描過程中,如果需要合併的HFile文件數 達到 hbase.hstore.compaction.max(默認爲10) 則開始合併過程
  • 掃描結束後,如果需要合併的HFile的文件數 大於 hbase.hstore.compaction.min(默認爲3) 則開始合併過程
  • 通過 hbase.offpeak.start.hour、hbase.offpeak.end.hour 設置高峯、非高峯時期,使 hbase.hstore.compaction.ratio的值在不同時期靈活變化(高峯值1.2、非高峯值5)

可以看到,Minor Compaction不會合並過大的HFile,合併的HFile數量也有嚴格的限制,以避免產生太大的IO操作,Minor Compaction經常在Memstore Flush後觸發,但不會對線上讀寫請求造成太大延遲影響。
在這裏插入圖片描述

Major Compaction

相對於Minor Compaction 只合並選擇的一部分HFile合併、合併時只簡單合併數據文件的特點,Major Compaction則將會把Store中的所有HFile合併成一個大文件,將會產生較大的IO操作

同時將會清理三類無意義數據:被刪除的數據、TTL過期數據、版本號超過設定版本號的數據,Region Split過程中產生的Reference文件也會在此時被清理。

Major Compaction定期執行的條件由以下兩個參數控制:

  • hbase.hregion.majorcompaction:默認7天
  • hbase.hregion.majorcompaction.jitter:默認爲0.2

集羣中各個RegionServer將會在 hbase.hregion.majorcompaction ± hbase.hregion.majorcompaction * hbase.hregion.majorcompaction.jitter 的區間浮動進行Major Compaction,以避免過多RegionServer同時進行,造成較大影響。

Major Compaction 執行時機觸發之後,簡單來說如果當前Store中HFile的最早更新時間早於某個時間值,就會執行Major Compaction,該時間值爲 hbase.hregion.majorcompaction * hbase.hregion.majorcompaction.jitter

手動觸發的情況下將會直接執行Compaction。
在這裏插入圖片描述

Compaction的優缺點

HBase通過Compaction機制使底層HFile文件數保持在一個穩定的範圍,減少一次讀請求產生的IO次數、文件Seek次數,確保HFiles文件檢索效率,從而實現高效處理線上請求。

如果沒有Compaction機制,隨着Memstore刷寫的數據越來越多,HFile文件數量將會持續上漲,一次讀請求生產的IO操作、Seek文件的次數將會越來越多,反饋到線上就是讀請求延遲越來越大

然而,在Compaction執行過程中,不可避免的仍然會對線上造成影響。

  • 對於Major Compaction來說,合併過程將會佔用大量帶寬、IO資源,此時線上的讀延遲將會增大
  • 對於Minor Compaction來說,如果Memstore寫入的數據量太多,刷寫越來越頻繁超出了HFile合併的速度
    • 即使不停地在合併,但是HFile文件仍然越來越多,讀延遲也會越來越大
    • HBase通過 hbase.hstore.blockingStoreFiles(默認7) 來控制Store中的HFile數量
    • 超過配置值時,將會堵塞Memstore Flush阻塞flush操作 ,阻塞超時時間爲 hbase.hstore.blockingWaitTime
    • 阻塞Memstore Flush操作將會使Memstore的內存佔用率越來越高,可能導致完全無法寫入

簡而言之,Compaction機制保證了HBase的讀請求一直保持低延遲狀態,但付出的代價是Compaction執行期間大量的讀延遲毛刺和一定的寫阻塞(寫入量巨大的情況下)。

1.6 Region Split

HBase通過 LSM-Tree架構提供了高性能的隨機寫,通過緩存、Bloom Filter、HFile與Compaction等機制提供了高性能的隨機讀

至此,HBase已經具備了作爲一個高性能讀寫數據庫的基本條件。如果HBase僅僅到此爲止的話,那麼其也只是個在架構上和傳統數據庫有所區別的數據庫而已,作爲一個高性能讀寫的分佈式數據庫來說,其擁有近乎可以無限擴展的特性

支持HBase進行自動擴展、負載均衡的是Region Split機制

Split策略與觸發條件

在HBase中,提供了多種Split策略,不同的策略觸發條件各不相同。
在這裏插入圖片描述
如上圖所示,不同版本中使用的默認策略在變化。

  • ConstantSizeRegionSplitPolicy
    • 固定值策略,閾值默認大小 hbase.hregion.max.filesize
    • 優點:簡單實現
    • 缺陷:考慮片面,小表不切分、大表切分成很多Region,線上使用弊端多
  • IncreasingToUpperBoundRegionSplitPolicy
    • 非固定閾值
      • 計算公式 min(R^2 * memstore.flush.size, region.split.size)
      • R爲Region所在的Table在當前RegionServer上Region的個數
      • 最大大小 hbase.hregion.max.filesize
    • 優點:自動適應大小表,對於Region個數多的閾值大,Region個數少的閾值小
    • 缺陷:對於小表來說會產生很多小region
  • SteppingSplitPolicy:
    • 非固定閾值
      • 如果Region個數爲1,則閾值爲 memstore.flush.size * 2
      • 否則爲 region.split.size
    • 優點:對大小表更加友好,小表不會一直產生小Region
    • 缺點:控制力度比較粗

可以看到,不同的切分策略其實只是在尋找切分Region時的閾值,不同的策略對閾值有不同的定義

切分點

切分閾值確認完之後,首先要做的是尋找待切分Region的切分點。

HBase對Region的切分點定義如下:

  • Region中最大的Store中,最大的HFile中心的block中,首個Rowkey
  • 如果最大的HFile只有一個block,那麼不切分(沒有middle key)

得到切分點之後,核心的切分流程分爲 prepare - execute - rollback 三個階段

prepare階段

在內存中初始化兩個子Region(HRegionInfo對象),準備進行切分操作。

execute階段

在這裏插入圖片描述
execute階段執行流程較爲複雜,具體實施步驟爲:

  1. RegionServer在Zookeeper上的 /hbase/region-in-transition 節點中標記該Region狀態爲SPLITTING
  2. HMaster監聽到Zookeeper節點發生變化,在內存中修改此Region狀態爲RIT
  3. 在該Region的存儲路徑下創建臨時文件夾 .split
  4. 父Region close,flush所有數據到磁盤中,停止所有寫入請求
  5. 在父Region的 .split文件夾中生成兩個子Region文件夾,並寫入reference文件
    • reference是一個特殊的文件,體現在其文件名與文件內容上
    • 文件名組成:RegionHFile.{父Region對應切分點所在的HFile文件}.{父Region}
    • 文件內容:[splitkey]切分點rowkey[top?]true/false,true爲top上半部分,false爲bottom下半部分
    • 根據reference文件名,可以快速找到對應的父Region、其中的HFile文件、HFile切分點,從而確認該子Region的數據範圍
    • 數據範圍確認完畢之後進行正常的數據檢索流程(此時仍然檢索父Region的數據
  6. 將子Region的目錄拷貝到HBase根目錄下,形成新的Region
  7. 父Regin通知修改 hbase:meta 表後下線,不再提供服務
    • 此時並沒有刪除父Region數據,僅在表中標記split列、offline列爲true,並記錄兩個子region
  8. 兩個子Region上線服務
  9. 通知 hbase:meta 表標記兩個子Region正式提供服務

rollback階段

如果execute階段出現異常,則執行rollback操作,保證Region切分整個過程是具備事務性、原子性的,要麼切分成功、要麼回到未切分的狀態。

region切分是一個複雜的過程,涉及到父region切分、子region生成、region下線與上線、zk狀態修改、元數據狀態修改、master內存狀態修改 等多個子步驟,回滾程序會根據當前進展到哪個子階段清理對應的垃圾數據

爲了實現事務性,HBase設計了使用**狀態機(SplitTransaction類)**來保存切分過程中的每個子步驟狀態。這樣一來一旦出現異常,系統可以根據當前所處的狀態決定是否回滾,以及如何回滾。

但是目前實現中,中間狀態是存儲在內存中,因此一旦在切分過程中RegionServer宕機或者關閉,重啓之後將無法恢復到切分前的狀態。即Region切分處於中間狀態的情況,也就是RIT

由於Region切分的子階段很多,不同階段解決RIT的處理方式也不一樣,需要通過hbck工具進行具體查看並分析解決方案。

好消息是HBase2.0之後提出了新的分佈式事務框架Procedure V2,將會使用HLog存儲事務中間狀態,從而保證事務處理中宕機重啓後可以進行回滾或者繼續處理,從而減少RIT問題產生。

父Region清理

從以上過程中我們可以看到,Region的切分過程並不會父Region的數據到子Region中,只是在子Region中創建了reference文件,故Region切分過程是很快的。

只有進行Major Compaction時纔會真正(順便)將數據切分到子Region中,將HFile中的kv順序讀出、寫入新的HFile文件。

RegionServer將會定期檢查 hbase:meta 表中的split和offline爲true的Region,對應的子Region是否存在reference文件,如果不存在則刪除父Region數據。

負載均衡

Region切分完畢之後,RegionServer上將會存在更多的Region塊,爲了避免RegionServer熱點,使請求負載均衡到集羣各個節點上,HMaster將會把一個或者多個子Region移動到其他RegionServer上。

移動過程中,如果當前RegionServer繁忙,HMaster將只會修改Region的元數據信息至其他節點,而Region數據仍然保留在當前節點中,直至下一次Major Compaction時進行數據移動。
在這裏插入圖片描述
至此,我們已經揭開了HBase架構與原理的大部分神祕面紗,在後續做集羣規劃、性能優化與實際應用中,爲什麼這麼調整以及爲什麼這麼操作 都將一一映射到HBase的實現原理上。

如果你希望瞭解HBase的更多細節,可以參考《HBase權威指南》。

二、集羣部署

經過冗長的理論初步瞭解過HBase架構與工作原理之後,搭建HBase集羣是使用HBase的第一個步驟。

需要注意的是,HBase集羣一旦部署使用,再想對其作出調整需要付出慘痛代價(線上環境中),所以如何部署HBase集羣是使用的第一個關鍵步驟。

2.1 集羣物理架構

硬件混合型+軟件混合型集羣

硬件混合型 指的是該集羣機器配置參差不齊,混搭結構

軟件混合型 指的是該集羣部署了一套類似CDH全家桶套餐

如以下的集羣狀況:

  • 集羣規模:30
  • 部署服務:HBase、Spark、Hive、Impala、Kafka、Zookeeper、Flume、HDFS、Yarn等
  • 硬件情況:內存、CPU、磁盤等參差不齊,有高配有低配,混搭結構

這個集羣不管是規模、還是服務部署方式相信都是很多都有公司的「標準」配置。

那麼這樣的集羣有什麼問題呢?

如果僅僅HBase是一個非「線上」的系統,或者充當一個歷史冷數據存儲的大數據庫,這樣的集羣其實一點問題也沒有,因爲對其沒有任何苛刻的性能要求。

但是如果希望HBase作爲一個線上能夠承載海量併發、實時響應的系統,這個集羣隨着使用時間的增加很快就會崩潰。

硬件混合型 來說,一直以來Hadoop都是以宣稱能夠用低廉、老舊的機器撐起一片天。

這確實是Hadoop的一個大優勢,然而前提是作爲離線系統使用。

離線系統的定義,即跑批的系統,如:Spark、Hive、MapReduce等,沒有很強的時間要求,顯著的吞吐量大,延遲高

因爲沒有實時性要求,幾臺拖拉機跑着也沒有問題,只要最後能出結果並且結果正確就OK。

那麼在我們現在的場景中,對HBase的定義已經不是一個離線系統,而是一個實時系統

對於一個硬性要求很高的實時系統來說,如果其中幾臺老機器拖了後腿也會引起線上響應的延遲

統一高配硬件+軟件混合型集羣

既然硬件拖後腿,那麼硬件升級自然是水到渠成。

現在我們有全新的高配硬件可以使用,參考如下:

  • 集羣規模:30
  • 部署服務:HBase、Spark、Hive、Impala、Kafka、Zookeeper、Flume、HDFS、Yarn等
  • 硬件情況:內存、CPU、磁盤統一高配置

這樣的集羣可能還會存在什麼問題呢?

軟件混合型 來說,離線任務最大的特點就是吞吐量特別高,瞬間讀寫的數據量可以把IO直接撐到10G/s,最主要的影響因素就是大型離線任務帶動高IO將會影響HBase的響應性能

如果僅止步於此,那麼線上的表現僅僅爲短暫延遲,真正令人窒息的操作是,如果離線任務再把CPU撐爆,RegionServer節點可能會直接宕機,造成嚴重的生產影響

存在的另外一種情況是,離線任務大量讀寫磁盤、讀寫HDFS,導致HBase IO連接異常也會造成RegionServer異常(HBase日誌反應HDFS connection timeout,HDFS日誌反應IO Exception),造成線上故障。

根據觀測,集羣磁盤IO到4G以上、集羣網絡IO 8G以上、HDFS IO 5G以上任意符合一個條件,線上將會有延遲反應。

因爲離線任務運行太過強勢導致RegionServer宕機無法解決,那麼能採取的策略只能是重新調整離線任務的執行使用資源、執行順序等,限制離線計算能力來滿足線上的需求。同時還要限制集羣的CPU的使用率,可能出現某臺機器CPU打滿後整個機器假死致服務異常,造成線上故障。

軟、硬件獨立的HBase集羣

簡而言之,無論是硬件混合型還是軟件混合型集羣,其可能因爲各種原因帶來的延遲影響,對於一個高性能要求的HBase來說,都是無法忍受的。

所以在集羣規劃初始就應該考慮到種種情況,最好使用獨立的集羣部署HBase。

參考如下一組集羣規模配置:

  • 集羣規模:15+5(RS+ZK)
  • 部署服務:HBase、HDFS(另5臺虛擬Zookeeper)
  • 硬件情況:除虛擬機外,物理機統一高配置

雖然從可用節點上上來看比之前的參考配置少了一半,但是從集羣部署模式上看,最大程度保證HBase的穩定性,從根本上分離了軟硬件對HBase所帶來的影響,將會擁有比之前兩組集羣配置 更穩定的響應和更高的性能

其他硬件推薦

  • 網卡:網卡是容易產生瓶頸的地方,有條件建議使用雙萬兆網卡
  • 磁盤:沒有特殊要求,空間越大越好,轉速越高越好
  • 內存:不需要大容量內存,建議32-128G(詳見下文)
  • CPU:CPU核數越多越好,HBase本身壓縮數據、合併HFile等都需要CPU資源。
  • 電源:建議雙電源冗餘

另外值得注意的是,Zookeeper節點建議設置5個節點,5個節點能保證Leader快速選舉,並且最多可以允許2個節點宕機的情況下正常使用。

硬件上可以選擇使用虛擬機,因爲zk節點本身消耗資源並不大,不需要高配機器。但是5個虛擬節點不能在一個物理機上,防止物理機宕機影響所有zk節點。

2.2 安裝與部署

以CDH集羣爲例安裝HBase。

使用自動化腳本工具進行安裝操作:

# 獲取安裝腳本,上傳相關安裝軟件包至服務器(JDK、MySQL、CM、CDH等)
yum install -y git
git clone https://github.com/chubbyjiang/cdh-deploy-robot.git
cd cdh-deploy-robot

# 編輯節點主機名
vi hosts
# 修改安裝配置項
vi deploy-robot.cnf
# 執行
sh deploy-robot.sh install_all

安裝腳本將會執行 配置SSH免密登錄、安裝軟件、操作系統優化、Java等開發環境初始化、MySQL安裝、CM服務安裝、操作系統性能測試等過程。

腳本操作說明見:CDH集羣自動化部署工具

等待cloudera-scm-server進程起來後,在瀏覽器輸入 ip:7180 進入CM管理界面部署HDFS、HBase組件即可。

三、性能優化

HBase集羣部署完畢運行起來之後,看起來一切順利,但是所有東西都處於**「初始狀態」**中。

我們需要根據軟硬件環境,針對性地對HBase進行 調優設置,以確保其能夠以最完美的狀態運行在當前集羣環境中,儘可能發揮硬件的優勢。

爲了方便後續配置項計算說明,假設我們可用的集羣硬件狀況如下:

  • 總內存:256G
  • 總硬盤:1.8T * 12 = 21.6T
  • 可分配內存:256 * 0.75 = 192G
  • HBase可用內存空間:192 * 0.8 = 153G(20%留給HDFS等其他進程)
  • 可用硬盤空間:21.6T * 0.85 = 18.36T

3.1 Region規劃

對於Region的大小,HBase官方文檔推薦單個在10G-30G之間,單臺RegionServer的數量控制在20-300之間(當然,這僅僅是參考值)。

Region過大過小都會有不良影響:

  • 過大的Region
    • 優點:遷移速度快、減少總RPC請求
    • 缺點:compaction的時候資源消耗非常大、可能會有數據分散不均衡的問題
  • 過小的Region
    • 優點:集羣負載平衡、HFile比較少compaction影響小
    • 缺點:遷移或者balance效率低、頻繁flush導致頻繁的compaction、維護開銷大

規劃Region的大小與數量時可以參考以下算法:

0. 計算HBase可用磁盤空間(單臺RegionServer)
1. 設置region最大與最小閾值,region的大小在此區間選擇,如10-30G
2. 設置最佳region數(這是一個經驗值),如單臺RegionServer 200個
3. 從region最小值開始,計算 HBase可用磁盤空間 / (region_size * hdfs副本數) = region個數
4. 得到的region個數如果 > 200,則增大region_size(step可設置爲5G),繼續計算直至找到region個數最接近200的region_size大小
5. region大小建議不小於10G

當前可用磁盤空間爲18T,選擇的region大小範圍爲10-30G,最佳region個數爲300。

那麼最接近 最佳Region個數300的 region_size 值爲30G。

得到以下配置項:

  • hbase.hregion.max.filesize=30G
  • 單節點最多可存儲的Region個數約爲300

3.2 內存規劃

我們知道RegionServer中的BlockCache有兩種實現方式:

  • LRUBlockCache:On-Heap
  • BucketCache:Off-Heap

這兩種模式的詳細說明可以參考 CDH官方文檔

爲HBase選擇合適的 內存模式 以及根據 內存模式 計算相關配置項是調優中的重要步驟。

首先我們可以根據可用內存大小來判斷使用哪種內存模式。

先看 超小內存(假設8G以下)超大內存(假設128G以上) 兩種極端情況:

對於超小內存來說,即使可以使用BucketCache來利用堆外內存,但是使用堆外內存的主要目的是避免GC時不穩定的影響,堆外內存的效率是要比堆內內存低的。由於內存總體較小,即使讀寫緩存都在堆內內存中,GC時也不會造成太大影響,所以可以直接選擇LRUBlockCache

對於超大內存來說,在超大內存上使用LRUBlockCache將會出現我們所擔憂的情況:GC時對線上造成很不穩定的延遲影響。這種場景下,應該儘量利用堆外內存作爲讀緩存,減小堆內內存的壓力,所以可以直接選擇BucketCache

在兩邊的極端情況下,我們可以根據內存大小選擇合適的內存模式,那麼如果內存大小在合理、正常的範圍內該如何選擇呢?

此時我們應該主要關注業務應用的類型

當業務主要爲寫多讀少型應用時,寫緩存利用率高,應該使用LRUBlockCache儘量提高堆內寫緩存的使用率

當業務主要爲寫少讀多型應用時,讀緩存利用率高(通常也意味着需要穩定的低延遲響應),應該使用BucketCache儘量提高堆外讀緩存的使用率

對於不明確或者多種類型混合的業務應用,建議使用BucketCache保證讀請求的穩定性同時,堆內寫緩存效率並不會很低

當前HBase可使用的內存高達153G,故將選擇BucketCache的內存模型來配置HBase,該模式下能夠最大化利用內存,減少GC影響,對線上的實時服務較爲有利。

得到配置項:

  • hbase.bucketcache.ioengine=offheap: 使用堆外緩存

確認使用的內存模式之後,接下來將通過計算確認 JavaHeap、對外讀緩存、堆內寫緩存、LRU元數據 等內存空間具體的大小。

內存與磁盤比

討論具體配置之前,我們從 HBase集羣規劃 引入一個Disk / JavaHeap Ratio的概念來幫助我們設置內存相關的參數。

理論上我們假設 最優 情況下 硬盤維度下的Region個數JavaHeap維度下的Region個數 相等。

相應的計算公式爲:

  • 硬盤容量維度下Region個數: DiskSize / (RegionSize * ReplicationFactor)
  • JavaHeap維度下Region個數: JavaHeap * HeapFractionForMemstore / (MemstoreSize / 2 )

其中:

  • RegionSize:Region大小,配置項:hbase.hregion.max.filesize
  • ReplicationFactor:HDFS的副本數,配置項:dfs.replication
  • HeapFractionForMemstore:JavaHeap寫緩存大小,即RegionServer內存中Memstore的總大小,配置項:hbase.regionserver.global.memstore.lowerLimit
  • MemstoreSize:Memstore刷寫大小,配置項:hbase.hregion.memstore.flush.size

現在我們已知條件 硬盤維度和JavaHeap維度相等,求 1 bytes的JavaHeap大小需要搭配多大的硬盤大小

已知:

DiskSize / (RegionSize * ReplicationFactor) = JavaHeap * HeapFractionForMemstore / (MemstoreSize / 2 )

求:

DiskSize / JavaHeap

進行簡單的交換運算可得:

DiskSize / JavaHeap = RegionSize / MemstoreSize * ReplicationFactor * HeapFractionForMemstore * 2

以HBase的默認配置爲例:

  • RegionSize: 10G
  • MemstoreSize: 128M
  • ReplicationFactor: 3
  • HeapFractionForMemstore: 0.4

計算:

10G / 128M * 3 * 0.4 * 2 = 192

即理想狀態下 RegionServer上 1 bytes的Java內存大小需要搭配192bytes的硬盤大小最合適

套用到當前集羣中,HBase可用內存爲152G,在LRUBlockCache模式下,對應的硬盤空間需要爲153G * 192 = 29T,這顯然是比較不合理的。

在BucketCache模式下,當前 JavaHeap、HeapFractionForMemstore 等值還未確定,我們會根據這個 計算關係和已知條件 對可用內存進行規劃和調整,以滿足合理的 內存/磁盤比

已知條件:

  • 內存模式:BucketCache
  • 可用內存大小:153G
  • 可用硬盤大小:18T
  • Region大小:30G
  • ReplicationFactor:3

未知變量:

  • JavaHeap
  • MemstoreSize
  • HeapFractionForMemstore

內存佈局

在計算位置變量的具體值之前,我們有必要了解一下當前使用的內存模式中對應的內存佈局。

BucketCache模式下,RegionServer的內存劃分如下圖:
在這裏插入圖片描述
簡化版:
在這裏插入圖片描述

寫緩存

從架構原理中我們知道,Memstore有4種級別的Flush,需要我們關注的是 Memstore、Region和RegionServer級別的刷寫。

其中Memstore和Region級別的刷寫並不會對線上造成太大影響,但是需要控制其閾值和刷寫頻次來進一步提高性能

而RegionServer級別的刷寫將會阻塞請求直至刷寫完成,對線上影響巨大,需要儘量避免

得到以下配置項:

  • hbase.hregion.memstore.flush.size=256M: 控制的Memstore大小默認值爲128M,太過頻繁的刷寫會導致IO繁忙,刷新隊列阻塞等。
    設置太高也有壞處,可能會較爲頻繁的觸發RegionServer級別的Flush,這裏設置爲256M。
  • hbase.hregion.memstore.block.multiplier=3: 控制的Region flush上限默認值爲2,意味着一個Region中最大同時存儲的Memstore大小爲2 * MemstoreSize ,如果一個表的列族過多將頻繁觸發,該值視情況調整。

現在我們設置兩個 經驗值變量

  • RegionServer總內存中JavaHeap的佔比=0.35
  • JavaHeap最大大小=56G:超出此值表示GC有風險

計算得JavaHeap的大小爲 153 * 0.35 = 53.55 ,沒有超出預期的最大JavaHeap。如果超過最大期望值,則使用最大期望值代替得JavaHeap大小爲53G

現在JavaHeap、MemstoreSize已知,可以得到唯一的位置變量 HeapFractionForMemstore 的值爲 0.48 。

得到以下配置項:

  • RegionServer JavaHeap堆棧大小: 53G
  • hbase.regionserver.global.memstore.upperLimit=0.58: 整個RS中Memstore最大比例,比lower大5-15%
  • hbase.regionserver.global.memstore.lowerLimit=0.48: 整個RS中Memstore最小比例

寫緩存大小爲 53 * 0.48 = 25.44G

讀緩存配置

當前內存信息如下:

  • A 總可用內存:153G
  • J JavaHeap大小:53G
    • W 寫緩存大小:25.44G
    • R1 LRU緩存大小:?
  • R2 BucketCache堆外緩存大小:153 - 53 = 100G

因爲讀緩存由 堆內的LRU元數據堆外的數據緩存 組成,兩部分佔比一般爲 1:9(經驗值) 。

而對於總體的堆內內存,存在以下限制,如果超出此限制則應該調低比例:

LRUBlockCache + MemStore < 80% * JVM_HEAP
即 LRUBlockCache + 25.44 < 53 * 0.8

可得R1的最大值爲16.96G

總讀緩存:R = R1 + R2
R1:R2 = 1:9
R1 = 11G < 16G
R = 111G

配置堆外緩存涉及到的相關參數如下:

  • hbase.bucketcache.size=111 * 1024M: 堆外緩存大小,單位爲M
  • hbase.bucketcache.percentage.in.combinedcache=0.9: 堆外讀緩存所佔比例,剩餘爲堆內元數據緩存大小
  • hfile.block.cache.size=0.15: 校驗項,+upperLimit需要小於0.8

現在,我們再來計算 Disk / JavaHeap Ratio 的值,檢查JavaHeap內存與磁盤的大小是否合理:

RegionSize / MemstoreSize * ReplicationFactor * HeapFractionForMemstore * 2
30 * 1024 / 256 * 3 * 0.48 * 2 = 345.6
53G * 345.6 = 18T <= 18T

至此,已得到HBase中內存相關的重要參數:

  • RegionServer JavaHeap堆棧大小: 53G
  • hbase.hregion.max.filesize=30G
  • hbase.bucketcache.ioengine=offheap
  • hbase.hregion.memstore.flush.size=256M
  • hbase.hregion.memstore.block.multiplier=3
  • hbase.regionserver.global.memstore.upperLimit=0.58
  • hbase.regionserver.global.memstore.lowerLimit=0.48
  • hbase.bucketcache.size=111 * 1024M
  • hbase.bucketcache.percentage.in.combinedcache=0.9
  • hfile.block.cache.size=0.15

3.3 合併與切分

HFile合併

Compaction過程中,比較常見的優化措施是:

  • Major Compaction
    • 停止自動執行
    • 增大其處理線程數
  • Minor Compaction
    • 增加Memstore Flush大小
    • 增加Region中最大同時存儲的Memstore數量

配置項如下:

# 關閉major compaction,定時在業務低谷執行,每週一次
hbase.hregion.majorcompaction=0
# 提高compaction的處理閾值
hbase.hstore.compactionThreshold=6
# 提高major compaction處理線程數
hbase.regionserver.thread.compaction.large=5
# 提高阻塞memstore flush的hfile文件數閾值
hbase.hstore.blockingStoreFiles=100

hbase.hregion.memstore.flush.size=256M
hbase.hregion.memstore.block.multiplier=3:

Major Compaction 腳本

關閉自動compaction之後手動執行腳本的代碼示例:

#!/bin/bash

if [ $# -lt 1 ]
then
    echo "Usage: <table key>"
    exit 1
fi

TMP_FILE=tmp_tables
TABLES_FILE=tables.txt
key=$1

echo "list" | hbase shell > $TMP_FILE
sleep 2

sed '1,6d' $TMP_FILE | tac | sed '1,2d' | tac | grep $key > $TABLES_FILE
sleep 2

for table in $(cat $TABLES_FILE); do
  date=`date "+%Y%m%d %H:%M:%S"`
        echo "major_compact '$table'" | hbase shell
  echo "'$date' major_compact '$table'" >> /tmp/hbase-major-compact.log
        sleep 5
done

rm -rf $TMP_FILE
rm -rf $TABLES_FILE

echo "" >> /tmp/hbase-major-compact.log

Region切分

在架構原理中我們知道,Region多有種切分策略,在Region切分時將會有短暫時間內的Region下線無服務,Region切分完成之後的Major Compaction中,將會移動父Region的數據到子Region中,HMaster爲了集羣整體的負載均衡可能會將子Region分配到其他RegionServer節點。

從以上描述中可以看到,Region的切分行爲其實是會對線上的服務請求帶來一定影響的。

Region切分設置中,使用默認配置一般不會有太大問題,但是有沒有 保證數據表負載均衡的情況下,Region不進行切分行爲?

有一種解決方案是使用 預分區 + 固定值切分策略 可以一定程度上通過預估數據表數量以及Region個數,從而在一段時間內抑制Region不產生切分。

假設我們可以合理的預判到一個表的當前總數據量爲150G,每日增量爲1G,當前Region大小爲30G

那麼我們建表的時候至少要設定 (150 + 1 * 360) / 30 = 17 個分區,如此一來一年內(360天)該表的數據增長都會落到17個Region中而不再切分。

當然對於一個不斷增長的表,除非時間段設置的非常長,否則總有發生切分的一天。如果無限制的延長時間段則會在一開始就產生大量的空Region,這對HBase是極其不友好的,所以時間段是一個需要合理控制的閾值。

在hbase-site.xml中配置Region切分策略爲ConstantSizeRegionSplitPolicy:

hbase.regionserver.region.split.policy=org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy

3.4 響應優化

HBase服務端

高併發情況下,如果HBase服務端處理線程數不夠,應用層將會收到HBase服務端拋出的無法創建新線程的異常從而導致應用層線程阻塞。

可以釋放調整HBase服務端配置以提升處理性能:

#  Master處理客戶端請求最大線程數   
hbase.master.handler.count=256
# RS處理客戶端請求最大線程數,如果該值設置過大則會佔用過多的內存,導致頻繁的GC,或者出現OutOfMemory
hbase.regionserver.handler.count=256
# 客戶端緩存大小,默認爲2M  
hbase.client.write.buffer=8M
# scan緩存一次獲取數據的條數,太大也會產生OOM
hbase.client.scanner.caching=100

另外,以下兩項中,默認設置下超時太久、重試次數太多,一旦應用層連接不上HBse服務端將會進行近乎無限的重試,長連接無法釋放,新請求不斷進來,從而導致線程堆積應用假死等,影響比較嚴重,可以適當減少:

hbase.client.retries.number=3   
hbase.rpc.timeout=10000  

HDFS

適當增加處理線程等設置:

dfs.datanode.handler.count=64
dfs.datanode.max.transfer.threads=12288
dfs.namenode.handler.count=256
dfs.namenode.service.handler.count=256

同時,對於HDFS的存儲設置也可以做以下優化:

# 可以配置多個,擁有多個元數據備份
dfs.name.dir
# 配置多個磁盤與路徑,提高並行讀寫能力
dfs.data.dir
# dn同時處理文件的上限,默認爲256,可以提高到8192
dfs.datanode.max.xcievers

應用層(客戶端)

之前我們說到,HBase爲了保證CP,在A的實現上做了一定的妥協,導致HBase出現故障並轉移的過程中會有較大的影響。

對於應用服務層來說,保證服務的 穩定性 是最重要的,爲了避免HBase可能產生的問題,應用層應該採用 讀寫分離 的模式來最大程度保證自身穩定性。

應用層讀寫分離

可靠的應用層應使用 讀寫分離 的模式提高響應效率與可用性:

  • 讀寫應用應該分別屬於 不同的服務實例 ,避免牽一髮而動全身
  • 對於寫入服務,數據異步寫入redis或者kafka隊列,由下游消費者同步至HBase,響應性能十分優異
    • 需要處理數據寫入失敗的事務處理與重寫機制
  • 對於讀取服務,如果一個RS掛了,一次讀請求經過若干重試和超時可能會持續幾十秒甚至更久,由於和寫入服務分離可以做到互不影響
    • 最好使用緩存層來環節RS宕機問題,對於至關重要的數據先查緩存再查HBase(見下文)

在應用層的 代碼 中,同樣有需要注意的小TIPS:

  • 如果在Spring中將HBaseAdmin配置爲Bean加載,則需配置爲懶加載,避免在啓動時鏈接HMaster失敗導致啓動失敗,從而無法進行一些降級操作。
  • scanner使用後及時關閉,避免浪費客戶端和服務器的內存
  • 查詢時指定列簇或者指定要查詢的列限定掃描範圍
  • Put請求可以關閉WAL,但是優化不大

最後,可以適當調整一下 連接池 設置:

# 配置文件加載爲全局共享,可提升tps
setInt(“hbase.hconnection.threads.max”, 512); 
setInt(“hbase.hconnection.threads.core”, 64); 

3.5 使用緩存層

即使我們經過大量的準備、調優與設置,在真實使用場景中,隨着HBase中承載的數據量越來越大、請求越來越多、併發越來越大,HBase不可避免的會有一些**「毛刺」問題**。

如果你現在已經通過HBase解決了大部分的線上數據存儲與訪問問題,但是有一小部分的數據需要提供最快速的響應、最低的延遲,由於HBase承載的東西太多,總是有延遲比較高的響應,此時需要怎麼解決?

其實,對所有數據庫軟件來說都會存在這樣的場景。於是,類似關係型數據庫中的數據庫拆分等策略也是可以應用到HBase上的。

或者是將最關鍵、最熱點的數據使用 獨立的HBase集羣 來處理,或者是使用諸如 Redis等更高性能的緩存軟件,其核心思想就是 將最關鍵的業務數據獨立存儲以提供最優質的服務,這個服務統稱爲緩存層。

3.6 其他配置

hbase-env.sh 的 HBase 客戶端環境高級配置代碼段

配置了G1垃圾回收器和其他相關屬性:

-XX:+UseG1GC 
-XX:InitiatingHeapOccupancyPercent=65 
-XX:-ResizePLAB 
-XX:MaxGCPauseMillis=90  
-XX:+UnlockDiagnosticVMOptions 
-XX:+G1SummarizeConcMark 
-XX:+ParallelRefProcEnabled 
-XX:G1HeapRegionSize=32m 
-XX:G1HeapWastePercent=20 
-XX:ConcGCThreads=4 
-XX:ParallelGCThreads=16  
-XX:MaxTenuringThreshold=1 
-XX:G1MixedGCCountTarget=64 
-XX:+UnlockExperimentalVMOptions 
-XX:G1NewSizePercent=2 
-XX:G1OldCSetRegionThresholdPercent=5

hbase-site.xml 的 RegionServer 高級配置代碼段(安全閥)

手動split region配置

<property><name>hbase.regionserver.wal.codec</name><value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec</value></property><property><name>hbase.region.server.rpc.scheduler.factory.class</name><value>org.apache.hadoop.hbase.ipc.PhoenixRpcSchedulerFactory</value><description>Factory to create the Phoenix RPC Scheduler that uses separate queues for index and metadata updates</description></property><property><name>hbase.rpc.controllerfactory.class</name><value>org.apache.hadoop.hbase.ipc.controller.ServerRpcControllerFactory</value><description>Factory to create the Phoenix RPC Scheduler that uses separate queues for index and metadata updates</description></property><property><name>hbase.regionserver.thread.compaction.large</name><value>5</value></property><property><name>hbase.regionserver.region.split.policy</name><value>org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy</value></property>

四、使用技巧

4.1 建表規約

Rowkey規範

  • 如無特殊情況,長度應控制在64字節內
  • 充分分析業務需求後確認需要查詢的維度字段。
  • get請求,則rowkey散列處理
  • scan請求,rowkey前綴維度散列後,後續維度依照查詢順序或者權重拼接(視具體情況決定是否散列處理)。
    • 各個字段都保持相同長度以支持左對齊的部分鍵掃描。
    • scan形式的數據表中,需要提前統計單個scan可掃描出的最大數量

列簇規範

  • 如無特殊情況,一個表中只有一個列簇,統一使用info命名。
  • 如果需要1以上的列簇,則原則上一次請求的數據不可跨列簇存儲,多不超過3個列簇。
  • 示例:NAME =>‘info’

壓縮

  • 統一使用SNAPPY壓縮
  • 示例:COMPRESSION => ‘SNAPPY’

版本

  • 默認版本數爲3,前期存儲空間緊張的情況下設置爲1。
  • 示例:VERSIONS => 1

布隆過濾器

  • 視情情況使用,主要針對get查詢提高性能
  • kv示例:BLOOMFILTER => ‘ROW’,根據rowkey中的信息生成布隆過濾器數據
  • kv+col示例:BLOOMFILTER => ‘ROWCOL’,根據rowkey+列信息生成布隆過濾器,針對get+指定列名的查詢,產生的過濾器文件會比ROW大。

預分區

  • 預分區需要通過評估整體表數據量來確認,當前hbase集羣region塊大小爲30G。
    • 歷史大增量小的數據:給定的預分區數足夠支撐該表永遠(或者相當長的時間內)不split,即更新的所有數據將進入已存在的region中,以減少split與compaction造成的影響。
    • 歷史小增量大的數據:預分區個數需滿足歷史數據等分存儲,並支撐未來一段時間內(一個月以上)的增量數據
  • 預分區區間計算:屬性相同的表中隨機取出部分樣本數據(rowkey維度字段)。將樣本轉換成rowkey之後排序,並以樣本個數/預分區個數爲步長,取預分區個數個rowkey組成預分區區間。

預分區代碼示例:

/**
    * hbase region預分區工具
    *
    * @param filePath    樣本文件路徑
    * @param numOfSPlits 預分區個數
    **/
  def rowkeySplitedArr(filePath: String, numOfSPlits: Int) = {
    val file = Source.fromFile(filePath).getLines()
    val res = file.map {
      line =>
        val arr = line.split("_")
        val card = arr(0)
        val name = arr(1)
        MathUtil.MD5Encrypt32(card) + MathUtil.MD5Encrypt32(card)
    }.toList.sorted
    val count = res.length / numOfSPlits
    var str = ""
    for (i <- 0 until numOfSPlits) {
      str += s"\'${res(i * count)}\',"
    }
    println(str.substring(0, str.length - 1))
  }

4.2 客戶端使用

服務端配置完成之後,如何更好的使用HBase集羣也需要花點心思測試與調整。

以Spark作爲HBase讀寫客戶端爲例。

查詢場景

批量查詢

Spark有對應的API可以批量讀取HBase數據,但是使用過程比較繁瑣,這裏安利一個小組件Spark DB Connector,批量讀取HBase的代碼可以這麼簡單:

val rdd = sc.fromHBase[(String, String, String)]("mytable")
      .select("col1", "col2")
      .inColumnFamily("columnFamily")
      .withStartRow("startRow")
      .withEndRow("endRow")

done!

實時查詢

以流式計算爲例,Spark Streaming中,我們要實時查詢HBase只能通過HBase Client API(沒有隊友提供服務的情況下)。

那麼HBase Connection每條數據創建一次肯定是不允許的,效率太低,對服務壓力比較大,並且ZK的連接數會暴增影響服務。
比較可行的方案是每個批次創建一個鏈接(類似foreachPartiton中每個分區創建一個鏈接,分區中數據共享鏈接)。但是這種方案也會造成部分連接浪費、效率低下等。

如果可以做到一個Streaming中所有批次、所有數據始終複用一個連接池是最理想的狀態。
Spark中提供了Broadcast這個重要工具可以幫我們實現這個想法,只要將創建的HBase Connection廣播出去所有節點就都能複用,但是真實運行代碼時你會發現HBase Connection是不可序列化的對象,無法廣播。。。

其實利用scala的lazy關鍵字可以繞個彎子來實現:

//實例化該對象,並廣播使用
class HBaseSink(zhHost: String, confFile: String) extends Serializable {
  //延遲加載特性
  lazy val connection = {
    val hbaseConf = HBaseConfiguration.create()
    hbaseConf.set(HConstants.ZOOKEEPER_QUORUM, zhHost)
    hbaseConf.addResource(confFile)
    val conn = ConnectionFactory.createConnection(hbaseConf)
    sys.addShutdownHook {
      conn.close()
    }
    conn
  }
}

在Driver程序中實例化該對象並廣播,在各個節點中取廣播變量的value進行使用。

廣播變量只在具體調用value的時候纔會去創建對象並copy到各個節點,而這個時候被序列化的對象其實是外層的HBaseSink,當在各個節點上具體調用connection進行操作的時候,Connection纔會被真正創建(在當前節點上),從而繞過了HBase Connection無法序列化的情況(同理也可以推導RedisSink、MySQLSink等)。

這樣一來,一個Streaming Job將會使用同一個數據庫連接池,在Structured Streaming中的foreachWrite也可以直接應用。

寫入場景

批量寫入

同理安利組件

rdd.toHBase("mytable")
      .insert("col1", "col2")
      .inColumnFamily("columnFamily")
      .save()

這裏邊其實對HBase Client的Put接口包裝了一層,但是當線上有大量實時請求,同時線下又有大量數據需要更新時,直接這麼寫會對線上的服務造成衝擊,具體表現可能爲持續一段時間的短暫延遲,嚴重的甚至可能會把RS節點整掛。

大量寫入的數據帶來具體大GC開銷,整個RS的活動都被阻塞了,當ZK來監測心跳時發現無響應就將該節點列入宕機名單,而GC完成後RS發現自己“被死亡”了,那麼就乾脆自殺,這就是HBase的“朱麗葉死亡”。

這種場景下,使用bulkload是最安全、快速的,唯一的缺點是帶來的IO比較高。
大批量寫入更新的操作,建議使用bulkload工具來實現。

實時寫入

理同實時查詢,可以使用創建的Connection做任何操作。

結束語

我們從HBase的架構原理出發,接觸了HBase大部分的核心知識點。

理論基礎決定上層建築,有了對HBase的總體認知,在後續的集羣部署、性能優化以及實際應用中都能夠比較遊刃有餘。

知其然而之所以然,保持對技術原理的探索,不僅能學習到其中許多令人驚歎的設計與操作,最重要的是能夠真正在業務應用中充分發揮其應有的性能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章