在 HBase(六): HBase體繫結構剖析(上) 介紹過,Hbase創建表時,只需指定表名和至少一個列族,基於HBase表結構的設計優化主要是基於列族級別的屬性配置,如下圖: 目錄: BLOOMFILTER BLOCKSIZE IN_MEMORY COMPRESSION/ENCODING VER ...
在 HBase(六): HBase體繫結構剖析(上) 介紹過,Hbase創建表時,只需指定表名和至少一個列族,基於HBase表結構的設計優化主要是基於列族級別的屬性配置,如下圖:
目錄:
- BLOOMFILTER
- BLOCKSIZE
- IN_MEMORY
- COMPRESSION/ENCODING
- VERSIONS
- TTL
BLOOMFILTER:
- Bloom Filter是由Bloom在1970年提出的一種多哈希函數映射的快速查找演算法。通常應用在一些需要快速判斷某個元素是否屬於集合,但是並不嚴格要求100%正確的場合
- bloom filter的數據存在StoreFile的meta中,一旦寫入無法更新,因為StoreFile是不可變的。Bloomfilter是一個列族(cf)級別的配置屬性,如果在表中設置了Bloomfilter,那麼HBase會在生成StoreFile時包含一份bloomfilter結構的數據,稱其為MetaBlock;MetaBlock與DataBlock(真實的KeyValue數據)一起由LRUBlockCache維護。所以,開啟bloomfilter會有一定的存儲及記憶體cache開銷
- 對於已經存在的表,可以使用alter表的方式修改表結構,但這種修改對於之前的數據不會生效,只針對修改後插入的數據
- 包含三種類型:NONE、ROW、ROWCOL
- ROW: 根據KeyValue中的row來過濾storefile,舉例如下:假設有2個storefile文件sf1和sf2
- sf1包含kv1(r1 cf:q1 v)、kv2(r2 cf:q1 v)
- sf2包含kv3(r3 cf:q1 v)、kv4(r4 cf:q1 v)
- 如果設置了CF屬性中的bloomfilter為ROW,那麼get(r1)時就會過濾sf2,get(r3)就會過濾sf1
- ROWCOL:根據KeyValue中的row+qualifier來過濾storefile
- 如上例:若設置了CF屬性中的bloomfilter為ROW,無論get(r1,q1)還是get(r1,q2),都會讀取sf1+sf2;
- 而如果設置了CF屬性中的bloomfilter為ROWCOL,那麼get(r1,q1)就會過濾sf2,get(r1,q2)就會過濾sf1
- ROW: 根據KeyValue中的row來過濾storefile,舉例如下:假設有2個storefile文件sf1和sf2
- region下的storefile數目越多,bloomfilter的效果越好
- region下的storefile數目越少,HBase讀性能越好
BLOCKSIZE:
- 從上圖可發現,預設的BlockSize 為 65536B (64KB),在 HBase(七): HBase體繫結構剖析(下) 介紹HBase讀原理,如果在blaock cache 、memostore中都沒查到符合條件的數據,則迴圈遍歷 storeFile 文件,而hbase讀取磁碟文件是按其基本I/O單元(即 hbase block)讀數據的,因此HFile塊大小是影響性能的重要參數
- 參見Get\Scan場景下測試不同BlockSize大小(16K,64K,128K)對性能的影響,如下圖:對比結果參考:http://hbasefly.com/2016/07/02/hbase-pracise-cfsetting/
- 可見,如果業務請求以Get請求為主,可以考慮將塊大小設置較小;如果以Scan請求為主,可以將塊大小調大;預設的64K塊大小是在Scan和Get之間取得的一個平衡
- 平均鍵值對規劃,如下
[root@HDP0 bin]# hbase hfile -m -f /apps/hbase/data/data/default/PerTest/7685e6c39d1394d94e26cf5ddafb7f9f/d/3ef195ca65044eca93cfa147414b56c2 SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [jar:file:/usr/hdp/2.4.2.0-258/hadoop/lib/slf4j-log4j12-1.7.10.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: Found binding in [jar:file:/usr/hdp/2.4.2.0-258/zookeeper/lib/slf4j-log4j12-1.6.1.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation. SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory] 2016-09-11 12:54:40,514 INFO [main] hfile.CacheConfig: CacheConfig:disabled Block index size as per heapsize: 6520 reader=/apps/hbase/data/data/default/PerTest/7685e6c39d1394d94e26cf5ddafb7f9f/d/3ef195ca65044eca93cfa147414b56c2, compression=none, cacheConf=CacheConfig:disabled, firstKey=00123ed7-5af8-49b1-bd13-9e086a5bd5f2/d:Action/1471406616120/Put, lastKey=fffbc8f7-55f2-4c49-804f-444f6ccbc903/d:UserID/1471406614464/Put, avgKeyLen=55, avgValueLen=10, entries=54180, length=4070738
-
從上面輸出的信息可以看出,該HFile的平均鍵值對規模為55B + 10B = 65B,相對較小,在這種情況下可以適當將塊大小調小(例如8KB或16KB)。這樣可以使得一個block內不會有太多kv,kv太多會增大塊內定址的延遲時間,因為HBase在讀數據時,一個block內部的查找是順序查找
- 選擇較小塊的大小的目的是使隨機讀取更快,而付出的代價是塊索引變大,會消耗更多的記憶體。相反,如果平均鍵值對規模很大,或者磁碟速度慢造成了性能瓶頸,那就應該選擇一個較大的塊大小,以便使一次磁碟IO能夠讀取更多的數據
- 思考:實際場景大部分是Scan讀,但平均鍵值規劃較小,如何設置BlockSize?
IN_MEMORY:
- Block Cache 包含三個級別的優先順序隊列:
- Single: 如果一個Block被第一次訪問,則放在這一級的隊列中
- Multi: 如果一個Block被多次訪問,則從Single隊列移動Multi隊列
- In Memory: 它是靜態指定的(在column family上設置),不會像其他兩種cache會因訪問頻率而發生改變,這就決定了它的獨立性,另外兩種block訪問次數再多也不會被放到in-memory的區段里去,in-memory的block不管是第幾次訪問,總是被放置到in-memory的區段中
- LRU(Least Recently Used)淘汰數據時,Single會被優先淘汰,其次是Multi, 最後是In Memory, 這三個隊列分別占用 BlockCache的 25%、50%、25%
- 每load一個block到cache時,都會檢查當前cache的size是否已經超過了“警戒線”,這個“警戒線”是一個規定的當前block cache總體積占額定體積的安全比例,預設該值是0.85,即當載入了一個block到cache後總大小超過了既定的85%就開始觸發非同步的evict操作了
- evict的邏輯是這樣的:遍歷cache中的所有block,根據它們所屬的級別(single,multi,in-memory)分撥到三個優先順序隊列中,隊頭元素是最舊(最近訪問日間值最小)的那個block。對這個三隊列依次驅逐頭元素,釋放空間
- 註意: 標記 IN_MEMORY=>'true' 的column family的總體積最好不要超過in-memory cache的大小(in-memory cache = heap size * hfile.block.cache.size * 0.85 * 0.25),特別是當總體積遠遠大於了in-memory cache時,會在in-memory cache上發生嚴重的顛簸
- 換個角度再看,普遍提到的使用in-memory cache的場景是把元數據表的column family聲明為IN_MEMORY=>'true。實際上這裡的潛臺詞是:元數據表都很小。其時我們也可以大膽地把一些需要經常訪問的,總體積不會超過in-memory cache的column family都設為IN_MEMORY=>'true'從而更加充分地利用cache空間。普通的block永遠是不會被放入in-memory cache的,只存放少量metadata是對in-memory cache資源的浪費
- 操作命令如下(建表時或alter已創建的表):
hbase(main):002:0> create 'Test',{NAME=>'d',IN_MEMORY=>'true'} 0 row(s) in 4.4970 seconds => Hbase::Table - Test
hbase(main):003:0> describe 'Test' Table Test is ENABLED Test COLUMN FAMILIES DESCRIPTION {NAME => 'd', BLOOMFILTER => 'ROW', VERSIONS => '1', IN_MEMORY => 'true', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', TTL => 'FOREVER', COMPRESSION => 'NONE', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE => '65536', REPLICATION_SCOPE => '0'} 1 row(s) in 0.2530 seconds hbase(main):004:0> create 'Test1','d' 0 row(s) in 2.2400 seconds => Hbase::Table - Test1 hbase(main):005:0> disable 'Test1' 0 row(s) in 2.2730 seconds hbase(main):006:0> alter 'Test1',{NAME=>'d',IN_MEMORY=>'true'} Updating all regions with the new schema... 1/1 regions updated. Done. 0 row(s) in 2.4610 seconds hbase(main):007:0> enable 'Test1' 0 row(s) in 1.3370 seconds hbase(main):008:0> describe 'Test1' Table Test1 is ENABLED Test1 COLUMN FAMILIES DESCRIPTION {NAME => 'd', BLOOMFILTER => 'ROW', VERSIONS => '1', IN_MEMORY => 'true', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', TTL => 'FOREVER', COMPRESSION => 'NONE', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE => '65536', REPLICATION_SCOPE => '0'} 1 row(s) in 0.0330 seconds hbase(main):009:0>
COMPRESSION/ENCODING:
- Compression就是在用CPU資源換取磁碟空間資源,對讀寫性能並不會有太大影響,HBase目前提供了三種常用的壓縮方式:GZip | LZO | Snappy
- HBase在寫入數據塊到HDFS之前會首先對數據塊進行壓縮,再落盤,從而可以減少磁碟空間使用量
- 讀數據的時候首先從HDFS中載入出block塊之後進行解壓縮,然後再緩存到BlockCache,最後返回給用戶。寫路徑和讀路徑分別如下
- 結合上圖,來看看數據壓縮對資源使用情況以及讀寫性能的影響:
- 資源使用情況:壓縮最直接、最重要的作用即是減少數據硬碟容量,理論上snappy壓縮率可以達到5:1,但是根據測試數據不同,壓縮率可能並沒有理論上理想;壓縮/解壓縮無疑需要大量計算,需要大量CPU資源;根據讀路徑來看,數據讀取到緩存之前block塊會先被解壓,緩存到記憶體中的block是解壓後的,因此和不壓縮情況相比,記憶體前後基本沒有任何影響
- 讀寫性能:因為數據寫入是先將kv數據值寫到緩存,最後再統一flush的硬碟,而壓縮是在flush這個階段執行的,因此會影響flush的操作,對寫性能本身並不會有太大影響;而數據讀取如果是從HDFS中讀取的話,首先需要解壓縮,因此理論上讀性能會有所下降;如果數據是從緩存中讀取,因為緩存中的block塊已經是解壓後的,因此性能不會有任何影響;一般情況下大多數讀都是熱點讀,緩存讀占大部分比例,壓縮並不會對讀有太大影響
- 官方分別從壓縮率,編解碼速率三個方面對其進行對比如下圖:
- 綜合來看,Snappy的壓縮率最低,但是編解碼速率最高,對CPU的消耗也最小,目前一般建議使用Snappy
- 從上圖看數據編碼對資源使用情況以及讀寫性能的影響:
- 資源使用情況:和壓縮一樣,編碼最直接、最重要的作用也是減少數據硬碟容量,但是數據編碼壓縮率一般沒有數據壓縮的壓縮率高,理論上只有5:2;編碼/解碼一般也需要大量計算,需要大量CPU資源;根據讀路徑來看,數據讀取到緩存之前block塊並沒有被解碼,緩存到記憶體中的block是編碼後的,因此和不編碼情況相比,相同數據block快占用記憶體更少,即記憶體利用率更高
- 讀寫性能:和數據壓縮相同,數據編碼也是在數據flush到hdfs階段執行的,因此並不會直接影響寫入過程;前面講到,數據塊是以編碼形式緩存到blockcache中的,因此同樣大小的blockcache可以緩存更多的數據塊,這有利於讀性能。另一方面,用戶從緩存中載入出來數據塊之後並不能直接獲取KV,而需要先解碼,這卻不利於讀性能。可見,數據編碼在記憶體充足的情況下會降低讀性能,而在記憶體不足的情況下需要經過測試才能得出具體結論
- HBase目前提供了四種常用的編碼方式:Prefix | Diff | Fast_Diff | Prefix_Tree。
- 壓縮與編碼使用測試結果示例,來源於:http://hbasefly.com/2016/07/02/hbase-pracise-cfsetting/
- 結果分析:
- 數據壓縮率並沒有理論上0.2那麼高,只有0.7左右,這和數據結構有關係。其中壓縮、編碼、壓縮+編碼三種方式的壓縮率基本相當
- 隨機讀場景:和預設配置相比,snappy壓縮在性能上沒有提升,CPU開銷卻上升了38%;prefix_tree性能上沒有提升,CPU利用率也基本相當;snappy+prefix_tree性能沒有提升,CPU開銷上升了38%
- 區間掃描場景:和預設配置相比,snappy壓縮在性能上略有10%的提升,但是CPU開銷卻上升了23%;prefix_tree性能上略有4%左右的下降,但是CPU開銷也下降了5%,snappy+prefix_tree在性能上基本沒有提升,CPU開銷卻上升了23%
- 設計原則:
- 在任何場景下開啟prefix_tree編碼都是安全的
- 在任何場景下都不要同時開啟snappy壓縮和prefix_tree編碼
- 通常情況下snappy壓縮並不能比prefix_tree編碼獲得更好的優化結果,如果需要使用snappy需要針對業務數據進行實際測試
VERSIONS:
- 用於定義某列族所能記錄的最多的版本數量,預設值是3,即每個單元格的最大版本數量是3
- 對於更新頻繁的應用,建設設置為1,可以快速淘汰無用的數據,節省存儲空間同時還能提升查詢效率
- 同樣道理,可在建表時指定或通過alter修改表結構實現
TTL:
- TTL:Time To Live 用於定義列族中單元格存活時間,過期數據自動刪除
- TTL屬性特性:
- 單位是秒,預設值:FOREVEN (永不過期)
- 當一行所有列都過期後,RowKey也會被刪除
- 若TTL設置為兩個月,則時間戮為2個月之前的數據不能插入
- 同理,在建表時指定或通過alter修改表結構設置