1. 程式人生 > >HBase生產環境配置與使用優化

HBase生產環境配置與使用優化

HBase上線至今,承載了線上所有實時交易量,雖然大部分請求都能夠保證服務穩定(99.56%響應時間毫秒級),但是一旦HBase出現問題就是雞飛狗跳的災難。
從老機器到新叢集,從老機房到新機房,期間經歷過各種問題和生產故障,總結一番以備不時之需。

HBase使用定位:大規模資料+高併發+毫秒級響應的OLTP實時系統(資料庫)。

叢集部署架構

HBase叢集一旦部署使用,再想對其作出調整需要付出慘痛代價,所以如何部署HBase叢集是使用的第一個關鍵步驟。

以下是HBase叢集使用以來的部署架構變化以及對應的分析。

第一階段 硬體混合型+軟體混合型叢集

  • 叢集規模:20
  • 部署服務:HBase、Spark、Hive、Impala、Kafka、Zookeeper、Flume、HDFS、Yarn等
  • 硬體情況:記憶體、CPU、磁碟等參差不齊,有高配有低配,混搭結構

硬體混合型指的是該叢集機器配置參差不齊,混搭結構。
軟體混合型指的是該叢集部署了一套CDH全家桶套餐。

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

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

如果僅僅HBase是一個非“線上”的系統,或者充當一個歷史冷資料儲存的大資料庫,這樣的叢集其實一點問題也沒有,因為對其沒有任何苛刻的效能要求。

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

**先從硬體混合型來說,**一直以來Hadoop都是以宣稱能夠用低廉、老舊的機器撐起一片天。是的沒錯,這確實是Hadoop的一個大優勢。然而前提是作為離線系統使用。首先說明一下離線系統的定義,就是跑批的系統,Spark、Hive、MapReduce等等,這些都算,沒有很強的時間要求,顯著的吞吐量大,延遲高。因為沒有實時性要求,幾臺拖拉機跑著也沒有問題,只要最後能出結果並且結果正確就ok。

那麼我們現在對HBase的要求特別高,對它的定義已經不是一個離線系統而是一個實時系統了。對於一個硬性要求很高的實時系統來說,如果其中幾臺老機器拖了後腿也會引起線上響應的延遲。

**軟體混合型叢集對實時HBase來說影響也特別大,**離線任務最大的特點就是吞吐量特別高,瞬間讀寫的資料量可以把IO直接撐到10G/s,最主要的影響因素就是大型離線任務帶動高IO將會影響HBase的響應效能。如果只是這樣的話,線上的表現僅僅為短暫延遲,如果離線任務再把CPU撐爆,RegionServer節點可能會直接宕機掛掉,造成嚴重的生產影響。

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

硬體混合型+軟體混合型結合產生的化學反應簡直無法想象的酸爽。。。

第二階段 全新硬體+軟體混合型叢集

第二階段,重新採購了全新的高配機器,搭建了一個新叢集並從老叢集過渡過來,老叢集的舊機器淘汰不用(一般硬體使用年限就是4、5年)。但是受限於機器規模,沒有將軟體服務分開部署,仍然是軟體混合型叢集,只是在硬體上做了提升。

  • 叢集規模:30(後期加至40)
  • 部署服務:HBase、Spark、Hive、Impala、Kafka、Zookeeper、Flume、HDFS、Yarn等
  • 硬體情況:記憶體、CPU、磁碟統一高配置

這樣的叢集還有什麼問題呢?

仍然是前文說的軟體混合型叢集帶來的影響,主要是離線任務IO影響大,觀測下來,**叢集磁碟IO到4G以上、叢集網路IO 8G以上、HDFS IO 5G以上任意符合一個條件,線上將會有延遲反應。**因為離線任務執行太過強勢導致RegionServer宕機仍然無法解決,只能重新調整離線任務的執行使用資源、執行順序等,限制離線計算能力來滿足線上的需求。同時還要限制叢集的CPU的使用率,可能出現某臺機器CPU打滿後整個機器假死致服務異常,造成線上故障。

問既然老早就知道原因了,為啥這麼多機器了不分幾臺出來搭個獨立的HBase叢集?
前期新叢集能用的機器比較少,HBase中儲存的資料量非常大,只分幾個機器出來可能無法滿足。
而後期線上交易已經無法允許暫停遷移,只能支援現有叢集,現在看來早分離HBase是個明智的選擇,然而我們錯過了這個選擇。

第三階段 軟、硬體獨立的HBase叢集

目前處於規劃中的第三階段,從叢集部署模式上最大程度保證HBase的穩定性。

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

這裡已經從根本上分離了軟硬體對HBase所帶來的影響。

另外Zookeeper節點不建議只設置3臺,5個節點能保證快速選舉,可以使用虛擬機器,因為ZK節點本身消耗資源並不大,但是5個虛擬節點不能在一個物理機上,一旦物理機掛了相當於5個ZK全掛,會有單點問題,並且ZK節點不在一起可以解決跨網路訪問時,外部請求不到的問題。

其他硬體配置,叢集使用萬兆網絡卡(千兆對於大資料叢集來說實在太小,很容易打滿,影響較大),磁碟儘可能大,記憶體不用太高,一般128G就已經特別多了HBase本身對記憶體的需求並不是配的越大越好(詳見下文)。CPU核數越多越好,HBase本身壓縮資料、compaction執行緒等都是很吃CPU資源的。

Redis前置層

由於目前第二階段HBase仍然存在許多問題,不是很穩定,在第三階段投入使用之前,我們添加了一個Redis前置快取層(8臺共800G記憶體叢集),將HBase中最重要最熱點的資料寫入Redis中,Redis叢集異常應用層可直接穿透查詢HBase,這樣一來對於使用者來說我們的服務將會是一直穩定的(然而這僅僅也是理論穩定,後續仍然出現了故障,詳見下文)。

Redis作為HBase的前置快取存在,儲存的熱點資料量大概是HBase中的20%。至於如何保證Redis叢集的穩定又是另外一個話題了。

HBase配置優化

確定完硬體方面的部署結構,下一個關鍵步驟是對HBase的配置進行優化調整,儘可能發揮硬體的最大優勢。

先看一下具體的硬體配置:

  • 總記憶體:256G
  • 可分配記憶體:256 * 0.75 = 192G
  • 總硬碟:1.8T * 12 = 21.6T
  • 可用硬碟空間:21.6T * 0.85 = 18.36T

Region規劃

對於Region的大小,HBase官方文件推薦單個在10G-30G之間,單臺RegionServer的數量控制在20-200之間。

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

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

按照官方推薦的配置最多可以儲存的資料量大概為200 * 30G * 3= 18T。如果儲存的資料量超過18T,或多或少會有些效能問題。從Region規模這個角度講,當前單臺RegionServer能夠合理利用起來的硬碟容量上限基本為18T(已提出Sub-Region的概念來滿足超大硬碟的需求)。

視磁碟空間、機器數量而定,當前Region配置為:

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

Memstore刷寫配置

Memstore是Region中的一塊記憶體區域,隨著客戶端的寫入請求增大,將會產生flush的操作將資料內容寫入到磁碟。

解釋如何配置Memstore刷寫引數之前建議提前瞭解Memstore的刷寫機制,簡單總結HBase會在如下幾種情況下觸發flush操作:

  • **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的最小flush單元是Region而不是單個MemStore。一個Region中Memstore越多每次flush的開銷越大,即ColumnFamily控制越少越好,一般不超過3個。

Memstore我們主要關注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 ,如果一個表的列族過多將頻繁觸發,該值視情況調整。
  • hbase.regionserver.global.memstore.upperLimit: 控制著整個RegionServer中Memstore最大佔據的比例,一定程度上可以理解為RS記憶體中寫快取的大小。詳見下文。

記憶體規劃

首先HBase的記憶體模式可以分為兩種:

  • LRUBlockCache:適用於寫多讀少型
  • BucketCache:適用於寫少讀多型

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

我們將會選擇BucketCache的記憶體模型來配置HBase,該模式下能夠最大化利用記憶體,減少GC影響,對線上的實時服務較為有利。

討論具體配置之前,從HBase最佳實踐-叢集規劃引入一個Disk / JavaHeap Ratio的概念來幫助我們設定記憶體相關的引數。
前面說過,對於HBase來說,記憶體並不是分配的越大越好,記憶體給多了GC起來是個災難,記憶體大小和硬碟大小之間存在一定的關聯。

Disk / JavaHeap Ratio指的是一臺RegionServer上1bytes的Java記憶體大小需要搭配多大的硬碟大小最合理。

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

簡單公式解釋(詳細解釋見上鍊接):

  • 硬碟容量維度下Region個數: DiskSize / (RegionSize * ReplicationFactor)
  • JavaHeap維度下Region個: JavaHeap * HeapFractionForMemstore / (MemstoreSize / 2 )
  • 硬碟維度和Java Headp維度理論相等:DiskSize / (RegionSize *ReplicationFactor) = JavaHeap * HeapFractionForMemstore / (MemstoreSize / 2 ) => DiskSize / JavaHeap = RegionSize / MemstoreSize * ReplicationFactor * HeapFractionForMemstore * 2

以預設配置為例:

  • RegionSize: hbase.hregion.max.filesize=10G
  • MemstoreSize: hbase.hregion.memstore.flush.size=128M
  • ReplicationFactor: dfs.replication=3
  • HeapFractionForMemstore: hbase.regionserver.global.memstore.lowerLimit = 0.4

計算為:10G / 128M * 3 * 0.4 * 2 = 192,即RegionServer上1bytes的Java記憶體大小需要搭配192bytes的硬碟大小最合理。

叢集可用記憶體為192G,即對應的硬碟空間需要為192G * 192 = 36T,顯然這是很不合理的

由於我們能夠使用的硬碟只有18T,所以可以適當調小記憶體,並重新調整以上引數。

寫快取配置

現在根據公式和之前的配置:

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

  • DiskSize:18T
  • JavaHeap: ?
  • RegionSize:30G
  • MemstoreSize:256M
  • ReplicationFactor: 3
  • HeapFractionForMemstore :?

可得 JavaHeap * HeapFractionForMemstore 約等於 24,假設 HeapFractionForMemstore 在0.4-0.6之間波動取值,對應那麼 JavaHeap 的大小為60-40G。

這裡可以取0.6+40G的配置,因為JavaHeap越大,GC起來就越痛苦,我們可以將多餘的記憶體給到堆外讀快取BucketCache中,這樣就可以保證JavaHeap並沒有實際浪費。

  • RegionServer JavaHeap堆疊大小: 40G
  • hbase.regionserver.global.memstore.upperLimit=0.6: 整個RS中Memstore最大比例
  • hbase.regionserver.global.memstore.lowerLimit=0.55: 整個RS中Memstore最小比例

讀快取配置

BucketCache模式下HBase的記憶體佈局如圖所示:

image

該模式主要應用於線上讀多寫少型應用,整個RegionServer記憶體(Java程序記憶體)分為兩部分:JVM記憶體和堆外記憶體。

  • 讀快取CombinedBlockCache:LRUBlockCache + 堆外記憶體BucketCache,用於快取讀到的Block資料
    • LRUBlockCache:用於快取元資料Block
    • BucketCache:用於快取實際使用者資料Block
  • 寫快取MemStore:快取使用者寫入KeyValue資料
  • 其他部分用於RegionServer正常執行所必須的記憶體

當前記憶體資訊如下:

  • A 總可用記憶體:192G
  • D JavaHeap大小:40G
    • C 寫快取大小:24G
    • B1 LRU快取大小:?
  • B2 BucketCache堆外快取大小:?

B理論上可以將192-40=152G全部給到堆外快取,考慮到HDFS程序、其他服務以及預留記憶體,這裡只分配到72G。 HBase本身啟動時對引數會有校驗限制(詳見下文檢驗項)。

TIPS:任何軟體使用的硬體資源安全線是80%以下,一旦超出將會有無法預料的問題,這是個傳統的運維玄學。曾經在Redis前置層上應驗過,相同的資料量相同的寫入速度,Redis叢集的記憶體使用率達到了90%直接掛了。

B=B1+B2,B1和B2的比例視情況而定,這裡設為1:9。

配置堆外快取涉及到的相關引數如下:

  • hbase.bucketcache.size=96 * 1024M: 堆外快取大小,單位為M
  • hbase.bucketcache.ioengine=offheap: 使用堆外快取
  • hbase.bucketcache.percentage.in.combinedcache=0.9: 堆外讀快取所佔比例,剩餘為堆內元資料快取大小
  • hfile.block.cache.size=0.15: 校驗項,+upperLimit需要小於0.8

校驗項

  • LRUBlockCache + MemStore < 80% * JVM_HEAP -> (7.2+24)/40=0.78 <= 0.8
  • RegionSize / MemstoreSize * ReplicationFactor * HeapFractionForMemstore * 2 -> 30 * 1024 / 256 * 3 * 0.6 * 2 = 432 -> 40G * 432 = 17T <= 18T
  • hfile.block.cache.size + hbase.regionserver.global.memstore.upperLimit = 0.75 <= 0.8

上一張CDH官方圖便於理解offheap下HBase的記憶體模型:

image

其他HBase服務端配置

應用層響應配置

響應配置的優化能夠提升HBase服務端的處理效能,一般情況下預設配置都是無法滿足高併發需求的。

  • hbase.master.handler.count=256: Master處理客戶端請求最大執行緒數
  • hbase.regionserver.handler.count=256: RS處理客戶端請求最大執行緒數

說明:如果設定小了,高併發的情況下,應用層將會收到HBase服務端丟擲的無法建立新執行緒的異常從而導致應用層執行緒阻塞。

  • hbase.client.retries.number=3
  • hbase.rpc.timeout=5000

說明:預設值太大了,一旦應用層連線不上HBse服務端將會進行近乎無限的重試,從而導致執行緒堆積應用假死等,影響比較嚴重,可以適當減少。

  • hbase.hstore.blockingStoreFiles=100: storefile個數達到該值則block寫入

說明:線上該引數可以調大一些,不然hfile達到指定數量時就會block等到compact。

HDFS相關配置

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

配置彙總

  • RegionServer JavaHeap堆疊大小: 40G
  • hbase.hregion.max.filesize=30G
  • hbase.hregion.memstore.flush.size=256M
  • hbase.hregion.memstore.block.multiplier=3
  • hbase.regionserver.global.memstore.upperLimit=0.6
  • hbase.regionserver.global.memstore.lowerLimit=0.55
  • hbase.bucketcache.size=64 * 1024M
  • hbase.bucketcache.ioengine=offheap
  • hbase.bucketcache.percentage.in.combinedcache=0.9
  • hfile.block.cache.size=0.15
  • hbase.master.handler.count=256
  • hbase.regionserver.handler.count=256
  • hbase.client.retries.number=3
  • hbase.rpc.timeout=5000
  • hbase.hstore.blockingStoreFiles=100

這裡只給出相對比較重要的配置,其餘引數視情況參考文件說明。

應用層使用優化

服務端配置完成之後,如何更好的使用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-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>