海量列式非關係數據庫HBase 原理深入
- 2021 年 9 月 15 日
- 筆記
前置知識(上一篇):海量列式非關係數據庫HBase 架構,shell與API
HBase讀數據流程:
前置關鍵詞描述:
- Block Cache :讀緩存,緩存上一次讀的數據,整個ReginServer只有一個
- MemStore :寫緩存,緩存上一次寫的數據,每個Store有一個
- WAL: 預寫入日誌
讀取數據流程:
- 1.請求zk 查詢meta表的地址
- 2.根據meta表的地址查詢rowkey屬於哪個reginserver的哪個regin,元數據緩存到MetaCache
- 3.先去BlockCache 和MemStore查找,找不到才去storeFile找,如果在storeFile 查詢到,就緩存到BlockCache里
HBase寫數據流程:
寫數據流程:
- 1.請求zk 查詢meta表的地址
- 2.根據meta表的地址查詢rowkey屬於哪個reginserver的哪個regin,元數據緩存到MetaCache
- 3.先寫WAL,再寫MemStore,寫入MemStore就返回了,
- 如果MemStore內存不夠,會flush storeFile文件,然後合併多個storeFile
註: Hbase的寫流程比讀流程效率高,因為寫流程只需要寫入內存,讀流程先讀內存,如果讀不到,還需要讀磁盤文件。
HBase的flush(刷寫) 機制:
刷寫條件:
- 1.MemStore大小達到128M
- 2.時間超過1小時
- 3.Reginserver的所有Memstore大小達到reginserver佔用的堆內存大小的40%
註: 上述條件默認每10s檢查一次
為防止檢查之前達到刷寫條件,會觸發阻塞機制.
阻塞機制觸發條件:
- Memstore達到512M
- Reginserver的所有Memstore大小達到堆內存的0.95*0.4
避免阻塞機制的解決方案:
如果出現這種情況,可以增大memstore大小,增大reginserver的堆內存大小。
Compact合併機制:
minor compact 小合併:
文件被選中條件:
- 1. 待合併文件數量大於3
- 2.待合併文件數量 小於10
- 3.文件大小小於128M的文件一定會加入
- 4.排除特別大的文件
合併觸發條件:
- 1.menstore flush
- 2.定期檢查,默認10s
Major compact:
- 合併所有的HFile,默認7天執行一次,生產中默認關閉
- 手動:major_compact 表名
注意:真正的刪除是在這一步進行
Region 拆分機制:
IncreasingToUpperBoundRegionSplitPolicy:
0.94版本~2.0版本默認切分策略:
切分策略稍微有點複雜,總體看和ConstantSizeRegionSplitPolicy思路相同,一個region大小大於設 置閾值就會觸發切分。但是這個閾值並不像ConstantSizeRegionSplitPolicy是一個固定的值,而是會 在一定條件下不斷調整,調整規則和region所屬表在當前regionserver上的region個數有關係. region split的計算公式是: regioncount^3 * 128M * 2,當region達到該size的時候進行split 例如: 第一次split:1^3 * 256 = 256MB 第二次split:2^3 * 256 = 2048MB 第三次split:3^3 * 256 = 6912MB 第四次split:4^3 * 256 = 16384MB > 10GB,因此取較小的值10GB 後面每次split的size都是10GB了
SteppingSplitPolicy:
2.0版本默認切分策略,其它版本參考百度:
這種切分策略的切分閾值又發生了變化,相比 IncreasingToUpperBoundRegionSplitPolicy 簡單了 一些,依然和待分裂region所屬表在當前regionserver上的region個數有關係,如果region個數等於 1, 切分閾值為flush size(128M) * 2,否則為MaxRegionFileSize(10GB)。這種切分策略對於大集群中的大表、小表會 比 IncreasingToUpperBoundRegionSplitPolicy 更加友好,小表不會再產生大量的小region,而是 適可而止。
Hbase 預分區:
為了負載均衡,提高讀寫效率,否則剛開始讀寫都在一個機器上進行。
通常解決負載均衡問題,還有以下解決方案:
- 給row key 加前綴
- 對row key 進行hash
- 反轉
Region 合併:
Region的合併不是為了性能,而是出於維護的目的。
通過Merge類冷合併Region:
- 需要先關閉hbase集群
- 需求:需要把student表中的2個region數據進行合併:
student,,1593244870695.10c2df60e567e73523a633f20866b4b5.
student,1000,1593244870695.0a4c3ff30a98f79ff6c1e4cc927b3d0d.
這裡通過org.apache.hadoop.hbase.util.Merge類來實現,不需要進入hbase shell,直接執行(需要 先關閉hbase集群):
hbase org.apache.hadoop.hbase.util.Merge student \ student,,1595256696737.fc3eff4765709e66a8524d3c3ab42d59. \ student,aaa,1595256696737.1d53d6c1ce0c1bed269b16b6514131d0.
通過online_merge熱合併Region:
- 不需要關閉hbase集群,在線進行合併。
與冷合併不同的是,online_merge的傳參是Region的hash值,而Region的hash值就是Region名稱的最 後那段在兩個.之間的字符串部分。 需求:需要把lagou_s表中的2個region數據進行合併: student,,1587392159085.9ca8689901008946793b8d5fa5898e06. \ student,aaa,1587392159085.601d5741608cedb677634f8f7257e000. 需要進入hbase shell: merge_region 'c8bc666507d9e45523aebaffa88ffdd6','02a9dfdf6ff42ae9f0524a3d8f4c7777'
RowKey 設計:
- RowKey長度原則
- rowkey是一個二進制碼流,可以是任意字符串,最大長度64kb,實際應用中一般為10-100bytes, 以byte[]形式保存,一般設計成定長。
- 建議越短越好,不要超過16個位元組 設計過長會降低memstore內存的利用率和HFile存儲數據的效率。
- RowKey散列原則
- 建議將rowkey的高位作為散列字段,這樣將提高數據均衡分佈在每個RegionServer,以實現負載均 衡的幾率。
- RowKey唯一原則
- 必須在設計上保證其唯一性
- RowKey排序原則
- HBase的Rowkey是按照ASCII有序設計的,我們在設計Rowkey時要充分利用這點
scan使用的時候注意:setStartRow,setEndRow 限定範圍, 範圍越小,性能越高。
Hbase 協處理器:
協處理器類型:
Observer:
協處理器與觸發器(trigger)類似:在一些特定事件發生時回調函數(也被稱作鉤子函數,hook)被執 行。這些事件包括一些用戶產生的事件,也包括服務器端內部自動產生的事件。
協處理器框架提供的接口如下:
- RegionObserver:用戶可以用這種的處理器處理數據修改事件,它們與表的region聯繫緊密。
- MasterObserver:可以被用作管理或DDL類型的操作,這些是集群級事件。
- WALObserver:提供控制WAL的鉤子函數
Endpoint:
這類協處理器類似傳統數據庫中的存儲過程,客戶端可以調用這些 Endpoint 協處理器在Regionserver 中執行一段代碼,並將 RegionServer 端執行結果返回給客戶端進一步處理。
Endpoint常見用途:
聚合操作 :
假設需要找出一張表中的最大數據,即 max 聚合操作,普通做法就是必須進行全表掃描,然後Client 代碼內遍歷掃描結果,並執行求最大值的操作。這種方式存在的弊端是無法利用底層集群的並發運算能 力,把所有計算都集中到 Client 端執行,效率低下。
使用Endpoint Coprocessor,用戶可以將求最大值的代碼部署到 HBase RegionServer 端,HBase 會利用集群中多個節點的優勢來並發執行求最大值的操作。也就是在每個 Region 範圍內執行求最大值 的代碼,將每個 Region 的最大值在 Region Server 端計算出,僅僅將該 max 值返回給Client。在 Client進一步將多個 Region 的最大值匯總進一步找到全局的最大值。
Endpoint Coprocessor的應用我們後續可以藉助於Phoenix非常容易就能實現。針對Hbase數據集進行 聚合運算直接使用SQL語句就能搞定。
Observer 案例:
需求: 通過協處理器Observer實現Hbase當中t1表插入數據,指定的另一張表t2也需要插入相對應的數據。
create 't1','info' create 't2','info'
實現思路:
通過Observer協處理器捕捉到t1插入數據時,將數據複製一份並保存到t2表中
java 實現:
<!-- //mvnrepository.com/artifact/org.apache.hbase/hbase-server --> <dependency> <groupId>org.apache.hbase</groupId> <artifactId>hbase-server</artifactId> <version>1.3.1</version> </dependency>
package com.lagou.hbase.processor; import org.apache.hadoop.hbase.Cell; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.Durability; import org.apache.hadoop.hbase.client.HTable; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.coprocessor.BaseRegionObserver; import org.apache.hadoop.hbase.coprocessor.ObserverContext; import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment; import org.apache.hadoop.hbase.regionserver.wal.WALEdit; import org.apache.hadoop.hbase.util.Bytes; import java.io.IOException; import java.util.List; //重寫prePut方法,監聽到向t1表插入數據時,執行向t2表插入數據的代碼 public class MyProcessor extends BaseRegionObserver { @Override public void prePut(ObserverContext<RegionCoprocessorEnvironment> e, Put put, WALEdit edit, Durability durability) throws IOException { //把自己需要執行的邏輯定義在此處,向t2表插入數據,數據具體是什麼內容與Put一樣 //獲取t2表table對象 final HTable t2 = (HTable) e.getEnvironment().getTable(TableName.valueOf("t2")); //解析t1表的插入對象put final Cell cell = put.get(Bytes.toBytes("info"), Bytes.toBytes("name")).get(0); //table對象.put final Put put1 = new Put(put.getRow()); put1.add(cell); t2.put(put1); //執行向t2表插入數據 t2.close(); } }
打成Jar包,上傳HDFS:
cd /opt/lagou/softwares mv original-hbaseStudy-1.0-SNAPSHOT.jar processor.jar hdfs dfs -mkdir -p /processor hdfs dfs -put processor.jar /processor
掛載協處理器:
hbase(main):056:0> describe 't1' hbase(main):055:0> alter 't1',METHOD => 'table_att','Coprocessor'=>'hdfs://linux121:9000/processor/processor.jar|com .lagou.hbase.processor.MyProcessor|1001|' #再次查看't1'表, hbase(main):043:0> describe 't1'
驗證協處理器:
向t1表中插入數據(shell方式驗證)
put 't1','rk1','info:name','lisi'
卸載協處理器:
disable 't1' alter 't1',METHOD=>'table_att_unset',NAME=>'coprocessor$1' enable 't2'
布隆過濾器在hbase的應用:
從前面的hbase的數據存儲原理,我們知道hbase的讀操作需要訪問大量的文件,大部分的 實現通過布隆過濾器來避免大量的讀文件操作。
布隆過濾器原理:
通常判斷某個元素是否存在用的可以選擇hashmap。但是 HashMap 的實現也有缺點,例如存儲 容量佔比高,考慮到負載因子的存在,通常空間是不能被用滿的,而一旦你的值很多例如上億的時候, 那 HashMap 佔據的內存大小就變得很可觀了。
Bloom Filter是一種空間效率很高的隨機數據結構,它利用位數組很簡潔地表示一個集合,並能判 斷一個元素是否屬於這個集合。 hbase 中布隆過濾器來過濾指定的rowkey是否在目標文件,避免掃描多個文件。使用布隆過濾器來判 斷。 布隆過濾器返回true,結果不一定正確,如果返回false則說明確實不存在。
原理示意圖:
Bloom Filter案例:
布隆過濾器,已經不需要自己實現,Google已經提供了非常成熟的實現。
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.0.1-jre</version> </dependency>
例: 預估數據量1w,錯誤率需要減小到萬分之一。使用如下代碼進行創建:
package com.lagou.hbase.bloom; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; import java.nio.charset.Charset; public class BloomFilterDemo { public static void main(String[] args) { // 1.創建符合條件的布隆過濾器 // 預期數據量10000,錯誤率0.0001 BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf-8")), 10000, 0.0001); // 2.將一部分數據添加進去 for (int i = 0; i < 5000; i++) { bloomFilter.put("" + i); } System.out.println("數據寫入完畢"); // 3.測試結果 for (int i = 0; i < 10000; i++) { if (bloomFilter.mightContain("" + i)) { System.out.println(i + "存在"); } else { System.out.println(i + "不存在"); } } } }