HBase(八): 表結構設計優化


    在 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
    1. 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
    2. 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
  • 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 包含三個級別的優先級隊列:
    1. Single: 如果一個Block被第一次訪問,則放在這一級的隊列中
    2. Multi:  如果一個Block被多次訪問,則從Single隊列移動Multi隊列
    3. 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,最后返回給用戶。寫路徑和讀路徑分別如下
  • 結合上圖,來看看數據壓縮對資源使用情況以及讀寫性能的影響:
    1. 資源使用情況:壓縮最直接、最重要的作用即是減少數據硬盤容量,理論上snappy壓縮率可以達到5:1,但是根據測試數據不同,壓縮率可能並沒有理論上理想;壓縮/解壓縮無疑需要大量計算,需要大量CPU資源;根據讀路徑來看,數據讀取到緩存之前block塊會先被解壓,緩存到內存中的block是解壓后的,因此和不壓縮情況相比,內存前后基本沒有任何影響
    2. 讀寫性能:因為數據寫入是先將kv數據值寫到緩存,最后再統一flush的硬盤,而壓縮是在flush這個階段執行的,因此會影響flush的操作,對寫性能本身並不會有太大影響;而數據讀取如果是從HDFS中讀取的話,首先需要解壓縮,因此理論上讀性能會有所下降;如果數據是從緩存中讀取,因為緩存中的block塊已經是解壓后的,因此性能不會有任何影響;一般情況下大多數讀都是熱點讀,緩存讀占大部分比例,壓縮並不會對讀有太大影響
  • 官方分別從壓縮率,編解碼速率三個方面對其進行對比如下圖:
  • 綜合來看,Snappy的壓縮率最低,但是編解碼速率最高,對CPU的消耗也最小,目前一般建議使用Snappy
  • 從上圖看數據編碼對資源使用情況以及讀寫性能的影響:
    1. 資源使用情況:和壓縮一樣,編碼最直接、最重要的作用也是減少數據硬盤容量,但是數據編碼壓縮率一般沒有數據壓縮的壓縮率高,理論上只有5:2;編碼/解碼一般也需要大量計算,需要大量CPU資源;根據讀路徑來看,數據讀取到緩存之前block塊並沒有被解碼,緩存到內存中的block是編碼后的,因此和不編碼情況相比,相同數據block快占用內存更少,即內存利用率更高
    2. 讀寫性能:和數據壓縮相同,數據編碼也是在數據flush到hdfs階段執行的,因此並不會直接影響寫入過程;前面講到,數據塊是以編碼形式緩存到blockcache中的,因此同樣大小的blockcache可以緩存更多的數據塊,這有利於讀性能。另一方面,用戶從緩存中加載出來數據塊之后並不能直接獲取KV,而需要先解碼,這卻不利於讀性能。可見,數據編碼在內存充足的情況下會降低讀性能,而在內存不足的情況下需要經過測試才能得出具體結論
  • HBase目前提供了四種常用的編碼方式:Prefix | Diff | Fast_Diff | Prefix_Tree
  • 壓縮與編碼使用測試結果示例,來源於:http://hbasefly.com/2016/07/02/hbase-pracise-cfsetting/
  •    
  • 結果分析:
    1. 數據壓縮率並沒有理論上0.2那么高,只有0.7左右,這和數據結構有關系。其中壓縮、編碼、壓縮+編碼三種方式的壓縮率基本相當
    2. 隨機讀場景:和默認配置相比,snappy壓縮在性能上沒有提升,CPU開銷卻上升了38%;prefix_tree性能上沒有提升,CPU利用率也基本相當;snappy+prefix_tree性能沒有提升,CPU開銷上升了38%
    3. 區間掃描場景:和默認配置相比,snappy壓縮在性能上略有10%的提升,但是CPU開銷卻上升了23%;prefix_tree性能上略有4%左右的下降,但是CPU開銷也下降了5%,snappy+prefix_tree在性能上基本沒有提升,CPU開銷卻上升了23%
  • 設計原則:
    1. 在任何場景下開啟prefix_tree編碼都是安全的
    2. 在任何場景下都不要同時開啟snappy壓縮和prefix_tree編碼
    3. 通常情況下snappy壓縮並不能比prefix_tree編碼獲得更好的優化結果,如果需要使用snappy需要針對業務數據進行實際測試

 VERSIONS


  •  用於定義某列族所能記錄的最多的版本數量,默認值是3,即每個單元格的最大版本數量是3
  • 對於更新頻繁的應用,建設設置為1,可以快速淘汰無用的數據,節省存儲空間同時還能提升查詢效率
  • 同樣道理,可在建表時指定或通過alter修改表結構實現

TTL


  • TTL:Time To Live 用於定義列族中單元格存活時間,過期數據自動刪除
  • TTL屬性特性:
    1. 單位是秒,默認值:FOREVEN (永不過期)
    2. 當一行所有列都過期后,RowKey也會被刪除
    3. 若TTL設置為兩個月,則時間戮為2個月之前的數據不能插入
  • 同理,在建表時指定或通過alter修改表結構設置


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM