HBase比較高階的調優指南

JVM調優

內存調優

一般安裝好的HBase集羣,默認配置是給Master和RegionServer 1G的內存,而Memstore默認佔0.4,也就是400MB。顯然RegionServer給的1G真的太少了。

export HBASE_MASTER_OPTS="$HBASE_MASTER_OPTS -Xms2g -Xmx2g"
export HBASE_REGIONSERVER_OPTS="$HBASE_REGIONSERVER_OPTS -Xms8g -Xmx8g"

這裏只是舉例,並不是所有的集羣都是這麼配置。
==要牢記至少留10%的內存給操作系統來進行必要的操作==

如何給出一個合理的JVM 內存大小設置,舉一個ambari官方提供的例子吧。

比如你現在有一臺16GB的機器,上面有MapReduce服務、 RegionServer和DataNode(這三位一般都是裝在一起的),那麼建議按 照如下配置設置內存:

  • 2GB:留給系統進程。

  • 8GB:MapReduce服務。平均每1GB分配6個Map slots + 2個Reduce slots。

  • 4GB:HBase的RegionServer服務

  • 1GB:TaskTracker

  • 1GB:DataNode

如果同時運行MapReduce的話,RegionServer將是除了MapReduce以外使用內存最大的服務。如果沒有MapReduce的話,RegionServer可以調整到大概一半的服務器內存。

Full GC調優

由於數據都是在RegionServer裏面的,Master只是做一些管理操作,所以一般內存問題都出在RegionServer上。

JVM提供了4種GC回收器:

  • 串行回收器(SerialGC)。

  • 並行回收器(ParallelGC),主要針對年輕帶進行優化(JDK 8 默認策略)。

  • 併發回收器(ConcMarkSweepGC,簡稱CMS),主要針對年老代進行優化。

  • G1GC回收器,主要針對大內存(32GB以上才叫大內存)進行優化。

一般會採取兩種組合方案

  1. ParallelGC和CMS的組合方案

    export HBASE_REGIONSERVER_OPTS="$HBASE_REGIONSERVER_OPTS -Xms8g -Xmx8g -XX:+UseParNewGC -XX:+UseConMarkSweepGC"

  2. G1GC方案

    export HBASE_REGIONSERVER_OPTS="$HBASE_REGIONSERVER_OPTS -Xms8g -Xmx8g -XX:+UseG1GC -XX:MaxGCPauseMillis=100"

怎麼選擇呢?

一般內存很大(32~64G)的時候,纔會去考慮用G1GC方案。
如果你的內存小於4G,乖乖選擇第一種方案吧。
如果你的內存(4~32G)之間,你需要自行測試下兩種方案,孰強孰弱靠實踐。測試的時候記得加上命令
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy

MSLAB和In Memory Compaction(HBase2.X纔有)

HBase自己實現了一套以Memstore爲最小單元的內存管理機制,稱爲 MSLAB(Memstore-Local Allocation Buffers)

跟MSLAB相關的參數是:

  • hbase.hregion.memstore.mslab.enabled:設置爲true,即打開 MSLAB,默認爲true。

  • hbase.hregion.memstore.mslab.chunksize:每個chunk的大 小,默認爲2048 * 1024 即2MB。

  • hbase.hregion.memstore.mslab.max.allocation:能放入chunk 的最大單元格大小,默認爲256KB,已經很大了。

  • hbase.hregion.memstore.chunkpool.maxsize:在整個memstore 可以佔用的堆內存中,chunkPool佔用的比例。該值爲一個百分 比,取值範圍爲0.0~1.0。默認值爲0.0。hbase.hregion.memstore.chunkpool.initialsize:在 RegionServer啓動的時候可以預分配一些空的chunk出來放到 chunkPool裏面待使用。該值就代表了預分配的chunk佔總的 chunkPool的比例。該值爲一個百分比,取值範圍爲0.0~1.0,默認值爲0.0。

在HBase2.0版本中,爲了實現更高的寫入吞吐和更低的延遲,社區團隊對MemStore做了更細粒度的設計。這裏,主要指的就是In Memory Compaction。

開啓的條件也很簡單。

hbase.hregion.compacting.memstore.type=BASIC # 可選擇NONE/BASIC/EAGER

具體這裏不介紹了。

Region自動拆分

Region的拆分分爲自動拆分和手動拆分。自動拆分可以採用不同的策略。

拆分策略

ConstantSizeRegionSplitPolicy

0.94版本的策略方案

hbase.hregion.max.filesize

通過該參數設定單個Region的大小,超過這個閾值就會拆分爲兩個。

IncreasingToUpperBoundRegionSplitPolicy(默認)

文件尺寸限制是動態的,依賴以下公式來計算

Math.min(tableRegionCount^3 * initialSize, defaultRegionMaxFileSize)

  • tableRegionCount:表在所有RegionServer上所擁有的Region數量總和。

  • initialSize:如果你定義了 hbase.increasing.policy.initial.size,則使用這個數值;如果沒有定義,就用memstore的刷寫大小的2倍,即 hbase.hregion.memstore.flush.size * 2。

  • defaultRegionMaxFileSize:ConstantSizeRegionSplitPolicy 所用到的hbase.hregion.max.filesize,即Region最大大小。

假如hbase.hregion.memstore.flush.size定義爲128MB,那麼文件 尺寸的上限增長將是這樣:

  1. 剛開始只有一個文件的時候,上限是256MB,因爲1^3 1282 = 256MB。

  2. 當有2個文件的時候,上限是2GB,因爲2^3 128 2 2048MB。

  3. 當有3個文件的時候,上限是6.75GB,因爲3^3 128 2 = 6912MB。

  4. 以此類推,直到計算出來的上限達到 hbase.hregion.max.filesize所定義的10GB。

KeyPrefixRegionSplitPolicy

除了簡單粗暴地根據大小來拆分,我們還可以自己定義拆分點。KeyPrefixRegionSplitPolicy 是 IncreasingToUpperBoundRegionSplitPolicy的子類,在前者的基礎上增加了對拆分點(splitPoint,拆分點就是Region被拆分處的rowkey)的定義。它保證了有相同前綴的rowkey不會被拆分到兩個不同的Region裏面。這個策略用到的參數是KeyPrefixRegionSplitPolicy.prefix_length rowkey:前綴長度

那麼它與IncreasingToUpperBoundRegionSplitPolicy區別,用兩張圖來看。

默認策略爲

KeyPrefixRegionSplitPolicy策略

如果你的前綴劃分的比較細,你的查詢就比較容易發生跨Region查詢的情況,此時採用KeyPrefixRegionSplitPolicy較好。

所以這個策略適用的場景是:

  • 數據有多種前綴。

  • 查詢多是針對前綴,比較少跨越多個前綴來查詢數據。

DelimitedKeyPrefixRegionSplitPolicy

該策略也是繼承自IncreasingToUpperBoundRegionSplitPolicy,它也是根據你的rowkey前綴來進行切分的。唯一的不同就是:KeyPrefixRegionSplitPolicy是根據rowkey的固定前幾位字符來進行判斷,而DelimitedKeyPrefixRegionSplitPolicy是根據分隔符來判斷的。在有些系統中rowkey的前綴可能不一定都是定長的。

使用這個策略需要在表定義中加入以下屬性:

DelimitedKeyPrefixRegionSplitPolicy.delimiter:前綴分隔符

比如你定義了前綴分隔符爲_,那麼host1_001和host12_999的前綴就分別是host1和host12。

BusyRegionSplitPolicy

如果你的系統常常會出現熱點Region,而你對性能有很高的追求, 那麼這種策略可能會比較適合你。它會通過拆分熱點Region來緩解熱點 Region的壓力,但是根據熱點來拆分Region也會帶來很多不確定性因 素,因爲你也不知道下一個被拆分的Region是哪個。

DisabledRegionSplitPolicy

這種策略就是Region永不自動拆分。

如果你事先就知道這個Table應該按 怎樣的策略來拆分Region的話,你也可以事先定義拆分點 (SplitPoint)。所謂拆分點就是拆分處的rowkey,比如你可以按26個 字母來定義25個拆分點,這樣數據一到HBase就會被分配到各自所屬的 Region裏面。這時候我們就可以把自動拆分關掉,只用手動拆分。

手動拆分有兩種情況:預拆分(pre-splitting)和強制拆分 (forced splits)。

推薦方案

一開始可以先定義拆分點,但是當數據開始工作起來後會出現熱點 不均的情況,所以推薦的方法是:

  1. 用預拆分導入初始數據。

  2. 然後用自動拆分來讓HBase來自動管理Region。

==建議:不要關閉自動拆分。==

Region的拆分對性能的影響還是很大的,默認的策略已經適用於大 多數情況。如果要調整,儘量不要調整到特別不適合你的策略

BlockCache優化

一個RegionServer只有一個BlockCache。

BlockCache的工作原理:讀請求到HBase之後先嚐試查詢BlockCache,如果獲取不到就去HFile(StoreFile)和Memstore中去獲取。如果獲取到了則在返回數據的同時把Block塊緩存到BlockCache中。它默認是開啓的。

如果你想讓某個列簇不使用BlockCache,可以通過以下命令關閉它。

alter 'testTable', CONFIGURATION=>{NAME => 'cf',BLOCKCACHE=>'false'}

BlockCache的實現方案有

  • LRU BLOCKCACHE

  • SLAB CACHE

  • Bucket CACHE

LRU BLOCKCACHE

在0.92版本 之前只有這種BlockCache的實現方案。LRU就是Least Recently Used, 即近期最少使用算法的縮寫。讀出來的block會被放到BlockCache中待 下次查詢使用。當緩存滿了的時候,會根據LRU的算法來淘汰block。LRUBlockCache被分爲三個區域,

看起來是不是很像JVM的新生代、年老代、永久代?沒錯,這個方案就是模擬JVM的代設計而做的。

Slab Cache

SlabCache實際測試起來對Full GC的改善很小,所以這個方案最後被廢棄了。不過它被廢棄還有一個更大的原因,這就是有另一個更好的Cache方案產生了,也用到了堆外內存,它就是BucketCache。

Bucket Cache

  • 相比起只有2個區域的SlabeCache,BucketCache一上來就分配了 14種區域。注意:我這裏說的是14種區域,並不是14塊區域。這 14種區域分別放的是大小爲4KB、8KB、16KB、32KB、40KB、 48KB、56KB、64KB、96KB、128KB、192KB、256KB、384KB、 512KB的Block。而且這個種類列表還是可以手動通過設置 hbase.bucketcache.bucket.sizes屬性來定義(種類之間用逗號 分隔,想配幾個配幾個,不一定是14個!),這14種類型可以分 配出很多個Bucket。

  • BucketCache的存儲不一定要使用堆外內存,是可以自由在3種存 儲介質直接選擇:堆(heap)、堆外(offheap)、文件 (file,這裏的文件可以理解成SSD硬盤)。通過設置hbase.bucketcache.ioengine爲heap、 offfheap或者file來配置。

  • 每個Bucket的大小上限爲最大尺寸的block 4,比如可以容納的最大的Block類型是512KB,那麼每個Bucket的大小就是512KB 4 = 2048KB。

  • 系統一啓動BucketCache就會把可用的存儲空間按照每個Bucket 的大小上限均分爲多個Bucket。如果劃分完的數量比你的種類還少,比如比14(默認的種類數量)少,就會直接報錯,因爲每一種類型的Bucket至少要有一個Bucket。

Bucket Cache默認也是開啓的,如果要關閉的話

alter 'testTable', CONFIGURATION=>{CACHE_DATA_IN_L1 => 'true'}

它的配置項:

  • hbase.bucketcache.ioengine:使用的存儲介質,可選值爲 heap、offheap、file。不設置的話,默認爲offheap。

  • hbase.bucketcache.combinedcache.enabled:是否打開組合模 式(CombinedBlockCache),默認爲true

  • hbase.bucketcache.size:BucketCache所佔的大小

  • hbase.bucketcache.bucket.sizes:定義所有Block種類,默認 爲14種,種類之間用逗號分隔。單位爲B,每一種類型必須是 1024的整數倍,否則會報異常:java.io.IOException: Invalid HFile block magic。默認值爲:4、8、16、32、40、48、56、 64、96、128、192、256、384、512。

  • -XX:MaxDirectMemorySize:這個參數不是在hbase-site.xml中 配置的,而是JVM啓動的參數。如果你不配置這個參數,JVM會按 需索取堆外內存;如果你配置了這個參數,你可以定義JVM可以獲得的堆外內存上限。顯而易見的,這個參數值必須比 hbase.bucketcache.size大。

在SlabCache的時代,SlabCache,是跟LRUCache一起使用的,每一 個Block被加載出來都是緩存兩份,一份在SlabCache一份在LRUCache, 這種模式稱之爲DoubleBlockCache。讀取的時候LRUCache作爲L1層緩存 (一級緩存),把SlabCache作爲L2層緩存(二級緩存)。

在BucketCache的時代,也不是單純地使用BucketCache,但是這回 不是一二級緩存的結合;而是另一種模式,叫組合模式 (CombinedBlockCahce)。具體地說就是把不同類型的Block分別放到 LRUCache和BucketCache中。

Index Block和Bloom Block會被放到LRUCache中。Data Block被直 接放到BucketCache中,所以數據會去LRUCache查詢一下,然後再去 BucketCache中查詢真正的數據。其實這種實現是一種更合理的二級緩 存,數據從一級緩存到二級緩存最後到硬盤,數據是從小到大,存儲介質也是由快到慢。考慮到成本和性能的組合,比較合理的介質是:LRUCache使用內存->BuckectCache使用SSD->HFile使用機械硬盤。

總結

關於LRUBlockCache和BucketCache單獨使用誰比較強,曾經有人做 過一個測試。

  • 因爲BucketCache自己控制內存空間,碎片比較少,所以GC時間 大部分都比LRUCache短。

  • 在緩存全部命中的情況下,LRUCache的吞吐量是BucketCache的 兩倍;在緩存基本命中的情況下,LRUCache的吞吐量跟 BucketCache基本相等。

  • 讀寫延遲,IO方面兩者基本相等。

  • 緩存全部命中的情況下,LRUCache比使用fiile模式的 BucketCache CPU佔用率低一倍,但是跟其他情況下差不多。

從整體上說LRUCache的性能好於BucketCache,但由於Full GC的存在,在某些時刻JVM會停止響應,造成服務不可用。所以適當的搭配 BucketCache可以緩解這個問題。

HFile合併

合併分爲兩種操作:

  • Minor Compaction:將Store中多個HFile合併爲一個HFile。在 這個過程中達到TTL的數據會被移除,但是被手動刪除的數據不 會被移除。這種合併觸發頻率較高。

  • Major Compaction:合併Store中的所有HFile爲一個HFile。在 這個過程中被手動刪除的數據會被真正地移除。同時被刪除的還 有單元格內超過MaxVersions的版本數據。這種合併觸發頻率較 低,默認爲7天一次。不過由於Major Compaction消耗的性能較 大,你不會想讓它發生在業務高峯期,建議手動控制Major Compaction的時機。

Compaction合併策略

RatioBasedCompactionPolicy

從舊到新地掃描HFile文件,當掃描到某個文件,該文件滿足以下條件:

該文件大小 < 比它更新的所有文件的大小總和 * hbase.store.compation.ratio(默認1.2)

實際情況下的RatioBasedCompactionPolicy算法效果很差,經常引 發大面積的合併,而合併就不能寫入數據,經常因爲合併而影響IO。所 以HBase在0.96版本之後修改了合併算法。

ExploringCompactionPolicy

0.96版本之後提出了ExploringCompactionPolicy算法,並且把該 算法作爲了默認算法。

算法變更爲

該文件大小 < (所有文件大小總和 - 該文件大小) * hbase.store.compation.ratio(默認1.2)

如果該文件大小小於最小合併大小(minCompactSize),則連上面那個公式都不需要套用,直接進入待合併列表。最小合併大小的配置項:hbase.hstore.compaction.min.size。如果沒設定該配置項,則使用hbase.hregion.memstore.flush.size。

被挑選的文件必須能通過以上提到的篩選條件,並且組合內含有的文件數必須大於hbase.hstore.compaction.min,小於 hbase.hstore.compaction.max。

文件太少了沒必要合併,還浪費資源;文件太多了太消耗資源,怕 機器受不了。

挑選完組合後,比較哪個文件組合包含的文件更多,就合併哪個組 合。如果出現平局,就挑選那個文件尺寸總和更小的組合。

FIFOCompactionPolicy

這個合併算法其實是最簡單的合併算法。嚴格地說它都不算是一種合併算法,是一種刪除策略。

FIFOCompactionPolicy策略在合併時會跳過含有未過期數據的 HFile,直接刪除所有單元格都過期的塊。最終的效果是:

  • 過期的塊被整個刪除掉了。

  • 沒過期的塊完全沒有操作。

這個策略不能用於什麼情況

  1. 表沒有設置TTL,或者TTL=FOREVER。

  2. 表設置了MIN_VERSIONS,並且MIN_VERSIONS > 0

DateTieredCompactionPolicy

DateTieredCompactionPolicy解決的是一個基本的問題:最新的數據最 有可能被讀到。

配置項

  • hbase.hstore.compaction.date.tiered.base.window.millis:基本的時間窗口時長。默認是6小時。拿默認的時間窗口舉例:從現在到6小時之內的HFile都在同一個時間窗口裏 面,即這些文件都在最新的時間窗口裏面。

  • hbase.hstore.compaction.date.tiered.windows.per.tier:層 次的增長倍數。分層的時候,越老的時間窗口越寬。在同一個窗口裏面的文件如果達到最小合併數量(hbase.hstore.compaction.min)就會進行合併,但不 是簡單地合併成一個,而是根據 hbase.hstore.compaction.date.tiered.window.policy.class 所定義的合併規則來合併。說白了就是,具體的合併動作 使用的是用前面提到的合併策略中的一種(我剛開始看到 這個設計的時候都震撼了,居然可以策略套策略),默認是ExploringCompactionPolicy。

  • hbase.hstore.compaction.date.tiered.max.tier.age.millis:最老的層次時間。當文件太老了,老到超過這裏所定義的時間範 圍(以天爲單位)就直接不合並了。不過這個設定會帶來一個缺 點:如果Store裏的某個HFile太老了,但是又沒有超過TTL,並 且大於了最老的層次時間,那麼這個Store在這個HFile超時被刪 除前,都不會發生Major Compaction。沒有Major Compaction, 用戶手動刪除的數據就不會被真正刪除,而是一直佔着磁盤空間。

配置項好像很複雜的樣子,舉個例子畫個圖就清楚了。

假設基本窗口寬度 (hbase.hstore.compaction.date.tiered.base.window.millis) = 1。最小合併數量(hbase.hstore.compaction.min) = 3。層次增長倍數 (hbase.hstore.compaction.date.tiered.windows.per.tier) = 2。

這個策略非常適用於什麼場景

  • 經常讀寫最近數據的系統,或者說這個系統專注於最新的數據。

  • 因爲該策略有可能引發不了Major Compaction,沒有Major Compaction是沒有辦法刪除掉用戶手動刪除的信息,所以更適用 於那些基本不刪除數據的系統。

這個策略比較適用於什麼場景

  • 數據根據時間排序存儲。

  • 數據的修改頻率很有限,或者只修改最近的數據,基本不刪除數據。

這個策略不適用於什麼場景

  • 數據改動很頻繁,並且連很老的數據也會被頻繁改動。

  • 經常邊讀邊寫數據。

StripeCompactionPolicy

該策略在讀取方面穩定。

那麼什麼場景適合用StripeCompactionPolicy

  • Region要夠大:這種策略實際上就是把Region給細分成一個個 Stripe。Stripe可以看做是小Region,我們可以管它叫sub- region。所以如果Region不大,沒必要用Stripe策略。小Region 用Stripe反而增加IO負擔。多大才算大?作者建議如果Region大 小小於2GB,就不適合用StripeCompactionPolicy。

  • Rowkey要具有統一格式,能夠均勻分佈。由於要劃分KeyRange, 所以key的分佈必須得均勻,比如用26個字母打頭來命名 rowkey,就可以保證數據的均勻分佈。如果使用timestamp來做 rowkey,那麼數據就沒法均勻分佈了,肯定就不適合使用這個策略。

總結

請詳細地看各種策略的適合場景,並根據場景選擇策略。

  • 如果你的數據有固定的TTL,並且越新的數據越容易被讀到,那麼DateTieredCompaction一般是比較適合你的。

  • 如果你的數據沒有TTL或者TTL較大,那麼選擇StripeCompaction會比默認的策略更穩定。

  • FIFOCompaction一般不會用到,這只是一種極端情況,比如用於 生存時間特別短的數據。如果你想用FIFOCompaction,可以先考慮使用DateTieredCompaction。

往期推薦  點擊標題跳轉

1、初識ClickHouse:來自戰鬥民族的OLAP利器

2、大數據之數據交換和存儲序列化利器 Avro

3、MapReduce Shuffle 和 Spark Shuffle 結業篇

4、實時數倉 | 你想要的數倉分層設計與技術選型

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