­

海量列式非關係數據庫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 + "不存在");
            }
        }
    }
}