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的記憶體佈局如圖所示:
該模式主要應用於線上讀多寫少型應用,整個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的記憶體模型:
其他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>