主頁 >  其他 > 超詳細梳理HBase核心知識點(上)【建議收藏】

超詳細梳理HBase核心知識點(上)【建議收藏】

2021-09-23 11:46:41 其他

之前我的公眾號名字叫做:’‘Java不睡覺’’,原因就是當時看了一本書,名字是《HBase不睡覺書》,這本書正如其名字一樣,是一本讓人讀起來根本不會發困的書,very奈斯,本文就是整理了這本書上的知識點而形成的文章,準備分為上下兩篇文章系統梳理HBase核心知識點,如果你想了解HBase,那么這篇文章不會讓你失望的,同時推薦閱讀一下原書,讓我們開始吧,

文章目錄

    • 前言
    • 一、整體架構
      • 1.1 Region
      • 1.2 RegionServer
      • 1.3 Master
      • 1.4 Zookeeper
      • 1.5 微觀架構
    • 二、常用Shell命令與API
      • 2.1 常用Shell命令
      • 2.2 Java API
    • 三、存盤機制
      • 3.1 RegionServer結構
      • 3.2 Region內部結構
      • 3.3 WAL(預寫日志)
      • 3.4 Store內部結構
      • 3.5 MemStore
      • 3.6 HFile(StoreFile)
      • 3.7 Data資料塊
      • 3.8 KeyValue類
      • 3.9 資料增刪改查的真面目
      • 3.10 Hbase資料結構總結
    • 四、KeyValue的寫入和讀出
      • 4.1 寫入
      • 4.2 讀出
    • 五、 Region的定位

前言

HBase 是一個開源的、面向列的非關系型分布式資料庫,目前是Hadoop體系中非常關鍵的一部分,
在最初,HBase是基于谷歌的 BigTable 原型實作的,許多技術來自于Fay Chang在2006年所撰寫的Google論文"BigTable",與 BigTable基于Google檔案系統(File System)一樣,HBase則是基于HDFS(Hadoop的分布式檔案系統)之上而開發的,

HBase 采用 Java 語言實作,在其內部實作了BigTable論文提到的一些壓縮演算法、記憶體操作和布隆過濾器等,這些能力使得HBase 在海量資料存盤、高性能讀寫場景中得到了大量應用,如 Facebook 在 2010年11 月開始便一直選用 HBase來作為訊息平臺的存盤層技術,
HBase 以 Apache License Version 2.0開源,這是一種對商業應用友好的協議,同時該專案當前也是Apache軟體基金會的頂級專案之一,

有什么特性?

  • 基于列式存盤模型,對于資料實作了高度壓縮,節省存盤成本
  • 采用 LSM 機制而不是B(+)樹,這使得HBase非常適合海量資料實時寫入的場景
  • 高可靠,一個資料會包含多個副本(默認是3副本),這得益于HDFS的復制能力,由RegionServer提供自動故障轉移的功能
  • 高擴展,支持分片擴展能力(基于Region),可實作自動、資料均衡
  • 強一致性讀寫,資料的讀寫都針對主Region上進行,屬于CP型的系統
  • 易操作,HBase提供了Java API、RestAPI/Thrift API等介面
  • 查詢優化,采用Block Cache 和 布隆過濾器來支持海量資料的快速查找

一、整體架構

整個HBase 集群主要由 Zookeeper、HBase Master、HBase RegionServer和HDFS構成,HBase集群架構圖如下:

其中 Master 節點是允許存在多個的,當多個 Master 節點共存時,只有一個 Master 是提供服務的,這種主備角色的"仲裁"由 ZooKeeper 實作,

RegionServer是直接負責存盤資料的服務器,RegionServer保存的表資料直接存盤在Hadoop的HDFS上,RegionServer非常依賴ZooKeeper服務,ZooKeeper管理了HBase中所有的RegionServer的資訊,包括具體的資料段存放在哪個 RegionServer上, 客戶端每次與HBase連接,其實都是先與ZooKeeper通信,查詢出具體需要連接哪個RegionServer,然后再連接到RegionServer,

1.1 Region

Region就是一段資料的集合,HBase中的表一般擁有一個到多個Region,Region具有以下特性:

  • Region不能跨服務器,一個RegionServer上有一個或者多個 Region,
  • 資料量小的時候,一個Region足以存盤所有資料;但是,當資料 量大的時候,HBase會拆分Region,
  • 當HBase在進行負載均衡的時候,也有可能會從一臺 RegionServer上把Region移動到另一臺RegionServer上,
  • Region是基于HDFS的,它的所有資料存取操作都是呼叫了HDFS的 客戶端介面來實作的,

1.2 RegionServer

RegionServer就是存放Region的容器,直觀上說就是服務器上的一 個服務,當客戶端從ZooKeeper獲取RegionServer的地址后,它會直接從 RegionServer獲取資料,

1.3 Master

在1.2小節中提到過,客戶端從 ZooKeeper獲取了RegionServer的地址后,會直接從RegionServer獲取資料,其實不光是獲取資料,包括插入、洗掉等所有的資料操作都是直接操作RegionServer,而不需要經過Master,

不像Hadoop等其他分布式系統,在HBase中,Master更像是一個打雜的,Master只負責各種協調作業,比如建表、刪表、 移動Region、合并等操作,它們的共性就是需要跨RegionServer,這些 操作由哪個RegionServer來執行都不合適,所以HBase就將這些操作放 到了Master上了,

這種結構的好處是大大降低了集群對Master的依賴,Master節點一般只有一個到兩個,一旦宕機,如果集群對Master的依賴度很大,那么就會產生單點故障問題,在HBase中,即使Master宕機了,集群依然 可以正常地運行,依然可以存盤和洗掉資料,

1.4 Zookeeper

Zookeeper 對于 HBase的作用是至關重要的,

  • Zookeeper 提供了 HBase Master 的高可用實作,并保證同一時刻有且僅有一個主 Master 可用,
  • Zookeeper 保存了 Region 和 Region Server 的關聯資訊(提供尋址入口),并保存了集群的元資料(Schema/Table),
  • Zookeeper 實時監控Region server的上線和下線資訊,并實時通知Master,

除了 HBase之外,有許多分布式大資料相關的開源框架,都依賴于 Zookeeper 實作 HA,

1.5 微觀架構

HBase是一個分布式列式資料庫,最基本的存盤單位是列(column),一個列或者多個列形成一行(row),在HBase中,這一行有三個列a、b、 c,下一個行也許是有4個列a、e、f、g,行跟行的列可以完全不一樣,這個行的資料跟另外一個行的資料也可以存盤在不同的機器上,甚至同一行內的列也可以存盤在完全不同的機器上!

每個行(row)都擁有唯一的行鍵(row key)來標定這個行的唯一 性,每個列都有多個版本,多個版本的值存盤在單元格(cell)中,

綜上,HBase的存盤結構可以表示成下圖所示的結構:

二、常用Shell命令與API

2.1 常用Shell命令

①進入hbase命令列:

$HBASE_HOME/bin/hbase shell


②create命令建表
在hbase shell下執行:

create 'zhb_test', 'cf'

創建一個名為’zhb_test’的表,并且帶有一個名為’cf’的列族,

補充一下列族的知識點:
前面提到過,HBase是一個分布式列式資料庫,

HBase的表都是由列族(Column Family)組成的;
沒有列族的表是沒有意義的;
列并不是依附于表上,而是依附于列族上;如下圖所示:

通過剛才的命令,我們現在建立的表有一個列族,叫cf,但是我們沒有指定這個列族里面有什么列,向表TableA中插入資料時,你只是向HBase中插入了一個單元格(Cell),而這個單元格是由表:列族:行:列來定位的,而別的行有沒有此列HBase并不知道,

HBase的所有資料屬性都是定義在列族上的,同一個表的不同列族可以定義完全不同的兩套屬性,所以從這個意義上來說,列族更像是傳統關系資料庫中的表,而表本身反倒變成只是存放列族的空殼了,

③ list命令看到整個庫中有哪些表

可以看到庫里共有15個表,包括我們剛才創建的’zhb_test’

④ describe命令來查看表屬性

輸入命令alter ‘zhb_test’, 'cf2’再添加一個列族,然后再describe看一下:

可以看到現在describe輸出的是兩個元素,分別對應cf和cf2兩 個列族,也印證了我們之前說的資料屬性是存在于列族上的,

補充:
在執行alter命令之前,最好先停用(disable)這個表 ,因為對列族的所有操作都會同步到所有擁有這個表的RegionServer上,你在執行命令的時候可以看到總共 有多少個RegionServer,當前執行了幾個RegionServer,當有很多客戶端都在連著的時候,直接新增一個列族對性能的影響較大,

⑤ put命令來插入資料

在HBase中,如果你的一行有10列,那存盤一行的資料得寫10行的 陳述句,這是因為HBase中行的每一個列都存盤在不同的位置,你必須指 定你要存盤在哪個單元格;而單元格需要根據表、行、列這幾個維度來 定位,

執行命令:
put ‘zhb_test’, ‘rowkey1’, ‘cf:name’, ‘zhb’

表示:
往’zhb_test’表插入一個單元格,這個單元格的rowkey為’rowkey1’,也就是說它是屬于’rowkey1’這個行中的 一個列,該單元格的列族為’cf’,該單元格的列名為’name’,資料值為’zhb’,

之后我們用scan命令掃描一下表,就可以看到我們剛才插入的資料了:
scan ‘zhb_test’

看這條記錄的時候,你會看到時間戳屬性,每一個單元格都可 以存盤多個版本(version)的值,HBase的單元格并沒有version這個 屬性,它用timestamp來存盤該條記錄的時間戳,這個時間戳就用來當 版本號使用,如果你在寫put陳述句的時候不指定時間戳,系統就會自動用當前時 間幫你指定它,有意思的是,這個timestamp雖然說是時間的標定,其 實你可以輸入任意的數字,比如1、2、3都可以存盤進去,當你用scan命令的時候HBase會顯示擁有最大(最新)的timestamp的資料版本,可以指定列族中保存的Cell版本數,

⑥ get命令獲取單元格資料

過get只能查詢一個單元格的記錄,在表的資料很大的時候,get查詢 的速度遠遠高于scan,

get ‘zhb_test’, ‘rowkey1’, ‘cf:name’

2.2 Java API

Hbase API 檔案:https://hbase.apache.org/apidocs/index.html

創建一個Maven專案,在pom.xml中添加如下依賴:

<dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-client</artifactId>
    <version>2.0.0</version>
</dependency>

HbaseClient.java

package javaa.sg.bigo;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HbaseClient {
    // config zookeeper
    static private org.apache.hadoop.conf.Configuration configuration = null;
    static private Connection connection = null;
    private static Logger logger = LoggerFactory.getLogger(HbaseClient.class);
    static private Lock lock = new ReentrantLock();
 
    static Connection getConnectionInstance() {
        if (null == connection) {
            lock.lock();
            try {
                if (null == connection) {
                    configuration = HBaseConfiguration.create();
                    configuration.set("hbase.zookeeper.quorum", "zk1:2182,zk2:2182,zk3:2182");
                    configuration.set("hbase.client.keyvalue.maxsize", "100000000");
                    connection = ConnectionFactory.createConnection(configuration);
                }
            } catch (IOException e) {
                logger.error("create hbase error ", e);
            } finally {
                lock.unlock();
            }
        }
        return connection;
    }
}

使用demo


    // 懶加載單例模式
    static private Connection connection = HbaseClient.getConnectionInstance();
    /**
     * 創建表
     *
     * @param tableName
     */
    public static void createTable(String tableStr, String[] familyNames) {
        System.out.println("start create table ......");
        try {
            Admin admin = connection.getAdmin();
            TableName tableName = TableName.valueOf(tableStr);
            if (admin.tableExists(tableName)) {// 如果存在要創建的表,那么先洗掉,再創建
                admin.disableTable(tableName);
                admin.deleteTable(tableName);
                System.out.println(tableName + " is exist,detele....");
            }
            HTableDescriptor tableDescriptor = new HTableDescriptor(tableName);
            // 添加表列資訊
            if (familyNames != null && familyNames.length > 0) {
                for (String familyName : familyNames) {
                    tableDescriptor.addFamily(new HColumnDescriptor(familyName));
                }
            }
            admin.createTable(tableDescriptor);
        } catch (MasterNotRunningException e) {
            e.printStackTrace();
        } catch (ZooKeeperConnectionException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("end create table ......");
    }
    /**
     * 添加行列資料資料
     *
     * @param tableName
     * @throws Exception
     */
    public static void insertData(String tableName, String rowId, String familyName,String qualifier, String value) throws Exception {
        System.out.println("start insert data ......");
        Table table = connection.getTable(TableName.valueOf(tableName));
        Put put = new Put(rowId.getBytes());// 一個PUT代表一行資料,再NEW一個PUT表示第二行資料,每行一個唯一的ROWKEY,此處rowkey為put構造方法中傳入的值
        put.addColumn(familyName.getBytes(), qualifier.getBytes(), value.getBytes());// 本行資料的第一列
        try {
            table.put(put);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("end insert data ......");
    }
    /**
     * 添加行列資料資料
     *
     * @param tableName
     * @throws Exception
     */
    public static void batchInsertData(String tableName, String rowId, List<String> familyNames,
                                  String qualifier, List<String> values) throws Exception {
        if (null == qualifier) qualifier = "tmp";
        Table table = connection.getTable(TableName.valueOf(tableName));
        Put put = new Put(rowId.getBytes());// 一個PUT代表一行資料,再NEW一個PUT表示第二行資料,每行一個唯一的ROWKEY,此處rowkey為put構造方法中傳入的值
        for (int i = 0; i < familyNames.size(); ++i) {
            put.addColumn(familyNames.get(i).getBytes(),
                    qualifier.getBytes(), values.get(i).getBytes());// 本行資料的第一列
        }
        try {
            table.put(put);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 洗掉行
     *
     * @param tablename
     * @param rowkey
     */
    public static void deleteRow(String tablename, String rowkey) {
        try {
            Table table = connection.getTable(TableName.valueOf(tablename));
            Delete d1 = new Delete(rowkey.getBytes());
            table.delete(d1);//d1.addColumn(family, qualifier);d1.addFamily(family);
            System.out.println("洗掉行成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 查詢所有資料
     *
     * @param tableName
     * @throws Exception
     */
    public static void queryAll(String tableName) throws Exception {
        Table table = connection.getTable(TableName.valueOf(tableName));
        try {
            ResultScanner rs = table.getScanner(new Scan());
            for (Result r : rs) {
                System.out.println("獲得到rowkey:" + new String(r.getRow()));
                for (Cell keyValue : r.rawCells()) {
                    System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":"+
                            new String(CellUtil.cloneQualifier(keyValue)) + "====值:" + new String(CellUtil.cloneValue(keyValue)));
                }
            }
            rs.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 根據rowId查詢
     *
     * @param tableName
     * @throws Exception
     */
    public static void queryByRowId(String tableName, String rowId) throws Exception {
        Table table = connection.getTable(TableName.valueOf(tableName));
        try {
            Get scan = new Get(rowId.getBytes());// 根據rowkey查詢
            Result r = table.get(scan);
            System.out.println("獲得到rowkey:" + new String(r.getRow()));
            for (Cell keyValue : r.rawCells()) {
                System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":"+
                        new String(CellUtil.cloneQualifier(keyValue)) + "====值:" + new String(CellUtil.cloneValue(keyValue)));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 根據列條件查詢
     *
     * @param tableName
     */
    public static void queryByCondition(String tableName, String familyName,String qualifier,String value) {
        try {
            Table table = connection.getTable(TableName.valueOf(tableName));
            Filter filter = new SingleColumnValueFilter(Bytes.toBytes(familyName),
                    Bytes.toBytes(qualifier), CompareOp.EQUAL, Bytes.toBytes(value)); // 當列familyName的值為value時進行查詢
            Scan s = new Scan();
            s.setFilter(filter);
            ResultScanner rs = table.getScanner(s);
            for (Result r : rs) {
                System.out.println("獲得到rowkey:" + new String(r.getRow()));
                for (Cell keyValue : r.rawCells()) {
                    System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":"+
                            new String(CellUtil.cloneQualifier(keyValue)) + "====值:" + new String(CellUtil.cloneValue(keyValue)));
                }
            }
            rs.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 多條件查詢
     *
     * @param tableName
     */
    public static void queryByConditions(String tableName, String[] familyNames, String[] qualifiers,String[] values) {
        try {
            Table table = connection.getTable(TableName.valueOf(tableName));
            List<Filter> filters = new ArrayList<Filter>();
            if (familyNames != null && familyNames.length > 0) {
                int i = 0;
                for (String familyName : familyNames) {
                    Filter filter = new SingleColumnValueFilter(Bytes.toBytes(familyName),
                            Bytes.toBytes(qualifiers[i]), CompareOp.EQUAL, Bytes.toBytes(values[i]));
                    filters.add(filter);
                    i++;
                }
            }
            FilterList filterList = new FilterList(filters);
            Scan scan = new Scan();
            scan.setFilter(filterList);
            ResultScanner rs = table.getScanner(scan);
            for (Result r : rs) {
                System.out.println("獲得到rowkey:" + new String(r.getRow()));
                for (Cell keyValue : r.rawCells()) {
                    System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":" +
                            new String(CellUtil.cloneQualifier(keyValue)) +
                            "====值:" + new String(CellUtil.cloneValue(keyValue)));
                }
            }
            rs.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 洗掉表
     *
     * @param tableName
     */
    public static void dropTable(String tableStr) {
        try {
            Admin admin = connection.getAdmin();
            TableName tableName = TableName.valueOf(tableStr);
            admin.disableTable(tableName);
            admin.deleteTable(tableName);
            admin.close();
        } catch (MasterNotRunningException e) {
            e.printStackTrace();
        } catch (ZooKeeperConnectionException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

三、存盤機制

這一節我們來看一下HBase這個資料庫是怎么存盤資料的,資料是怎么存盤到磁盤上的,我們會從最宏觀的Master和RegionServer結構一直 到最小的單元格(Cell),

再把宏觀架構圖拿出來復習一下:

從這張圖上可以看出一個HBase集群由一個Master(也可以把兩個 Master做成 HighAvailable)和多個RegionServer組成,右下角是其中 一個RegionServer的內部構造圖,我們先看完這幅圖的圖解后,再解剖 它,

RegionServer上有一個或者多個Region,我們讀 寫的資料就存盤在Region上,HBase是一個會自動分片的資料庫, 一個Region就相當于關系型資料庫中磁區表的一個磁區,

3.1 RegionServer結構

放大RegionServer,如下圖:

從這幅圖中我們可以看出一個RegionServer包含有:

  • 一個WAL:
    預寫日志,WAL是Write-Ahead Log的縮寫,從名字就可以看出它的用途,就是:預先寫入,當操作到達Region的時候,HBase先不管三七二十一把操作寫到WAL里面去,HBase會先把資料放到基于記憶體實作的Memstore里,等資料達到一定的數量 時才刷寫(flush)到最終存盤的HFile內,而如果在這個程序中 服務器宕機或者斷電了,那么資料就丟失了,WAL是一個保隙訓 制,資料在寫到Memstore之前,先被寫到WAL了,這樣當故障恢復的時候可以從WAL中恢復資料,

  • 多個Region:
    Region相當于一個資料分片,每一個Region都有起始rowkey和結束rowkey,代表了它所存盤的row范圍,

3.2 Region內部結構

接下來我們來看單個Region內部的結構

一個Region包含有:

  • 多個Store:每一個Region內都包含有多個Store實體,一個 Store對應一個列族的資料,舉個例子,如果一個表有兩個列族,那么在一 個Region里面就有兩個Store,在最右邊的單個Store的解剖圖上,我們可以看到Store內部有MemStore和HFile這兩個組成部分,

3.3 WAL(預寫日志)

預寫日志(Write-ahead log,WAL)就是設計來解決宕機之后的操作恢復問題的,資料到達 Region 的時候是先寫入 WAL,然后再被加載到 Memstore,就算 Region 的機器宕掉了,由于 WAL 的資料是存盤在 HDFS 上的,所以資料并不會丟失,

WAL 是默認開啟的,可以通過下面的代碼關閉 WAL,

Mutation.setDurability(Durability.SKIP_WAL);

Put、Append、Increment、Delete 都是 Mutation 的子類,所以他們都有 setDurability 方法,這樣可以讓該資料操作快一點,但是最好不要這樣做,因為當服務器宕機時,資料就會丟失,

如果你實在想不惜通過關閉 WAL 來提高性能,可以選擇異步寫入 WAL,

Mutation.setDurability(Durability.ASYNC WAL);

這樣設定后 Region 會等到條件滿足的時候才把操作寫入 WAL,這里提到的條件主要指的是時間間隔 hbase.regionserver.optionallogflushinterval,這個時間間隔的意思是 HBase 間隔多久會把操作從記憶體寫入 WAL,默認值是 1s,

3.4 Store內部結構

在 Store 中有兩個重要組成部分:

  • MemStore:每個 Store 中有一個 MemStore 實體,資料寫入 WAL 之后就會被放入 MemStore,MemStore 是記憶體的存盤物件,只有當 MemStore 滿了的時候才會將資料刷寫(flush)到 HFile 中;

  • HFile:在 Store 中有多個 HFile,當 MemStore 滿了之后 HBase 就會在 HDFS 上生成一個新的 HFile,然后把 MemStore 中的內容寫到這個 HFile 中,HFile 直接跟 HDFS 打交道,它是資料的存盤物體,

WAL 是存盤在 HDFS 上的,Memstore 是存盤在記憶體中的,HFile 又是存盤在 HDFS 上的;資料是先寫入 WAL,再被放入 Memstore,最后被持久化到 HFile 中,資料在進入 HFile 之前已經被存盤到 HDFS 一次了,為什么還需要被放入 Memstore?

這是因為 HDFS 上的檔案只能創建、追加、洗掉,但是不能修改,對于一個資料庫來說,按順序地存放資料是非常重要的,這是性能的保障,所以我們不能按照資料到來的順序來寫入硬碟,

可以使用記憶體先把資料整理成順序存放,然后再一起寫入硬碟,這就是 Memstore 存在的意義,雖然 Memstore 是存盤在記憶體中的,HFile 和 WAL 是存盤在 HDFS 上的,但由于資料在寫入 Memstore 之前,要先被寫入 WAL,所以增加 Memstore 的大小并不能加速寫入速度,Memstore 存在的意義是維持資料按照 rowkey 順序排列,而不是做一個快取,

3.5 MemStore

設計 MemStore 的原因有以下幾點:

  • 由于 HDFS 上的檔案不可修改,為了讓資料順序存盤從而提高讀取效率,HBase 使用了 LSM 樹結構來存盤資料,資料會先在 Memstore 中整理成 LSM 樹,最后再刷寫到 HFile 上,

  • 優化資料的存盤,比如一個資料添加后就馬上洗掉了,這樣在刷寫的時候就可以直接不把這個資料寫到 HDFS 上,

不過不要想當然地認為讀取也是先讀取 Memstore 再讀取磁盤喲!讀取的時候是有專門的快取叫 BlockCache,這個 BlockCache 如果開啟了,就是先讀 BlockCache,讀不到才是讀 HFile+Memstore,

3.6 HFile(StoreFile)

HFile是資料存盤的實際載體,我們創建的所有表、列等資料都存盤在HFile里面,HFile類似 Hadoop的TFile類,它模仿了BigTable的SSTable格式,HFile中組成部分如下圖所示:

我們可以看到HFile是由一個一個的塊組成的,在HBase中一個塊的 大小默認為64KB,由列族上的BLOCKSIZE屬性定義,這些塊區分了不同 的角色:

  • Data:資料塊,每個 HFile 有多個 Data 塊,我們存盤在 HBase 表中的資料就在這里,Data 塊其實是可選的,但是幾乎很難看到不包含 Data 塊的 HFile,

  • Meta:元資料塊,Meta 塊是可選的,Meta 塊只有在檔案關閉的時候才會寫入,Meta 塊存盤了該 HFile 檔案的元資料資訊,在 v2 之前布隆過濾器(Bloom Filter)的資訊直接放在 Meta 里面存盤,v2 之后分離出來單獨存盤,

  • FileInfo:檔案資訊,其實也是一種資料存盤塊,FileInfo 是 HFile 的必要組成部分,是必選的,它只有在檔案關閉的時候寫入,存盤的是這個檔案的資訊,比如最后一個 Key(LastKey),平均的 Key 長度(AvgKeyLen)等;

  • DataIndex:存盤 Data 塊索引資訊的塊檔案,索引的資訊其實也就是 Data 塊的偏移值(offset),DataIndex 也是可選的,有 Data 塊才有 DataIndex;

  • MetaIndex:存盤 Meta 塊索引資訊的塊檔案,MetaIndex 塊也是可選的,有 Meta 塊才有 MetaIndex;

  • Trailer:必選的,它存盤了 FileInfo、DataIndex、MetaIndex 塊的偏移值,

其實叫 HFile 或者 StoreFile 都沒錯,在物理存盤上我們管 MemStore 刷寫而成的檔案叫 HFile,StoreFile 就是 HFile 的抽象類而已,

3.7 Data資料塊

剛剛我們講完了HBase的底層存盤HFile,不過我們現在還沒有達到 HBase架構的最微觀結構,接下來,我們繼續解剖Data資料塊,

Data資料塊的第一位存盤的是塊的型別,后面存盤的是多個 KeyValue鍵值對,也就是單元格(Cell)的實作類,Cell是一個介面, KeyValue是它的實作類,

3.8 KeyValue類

讓我們來看看單元格最重要的實作類KeyValue類的結構,如下圖所示:

一個 KeyValue 類里面最后一個部分是存盤資料的 Value,而前面的部分都是存盤跟該單元格相關的元資料資訊,如果你存盤的 value 很小,那么這個單元格的絕大部分空間就都是 rowkey、column family、column 等的元資料,所以大家的列族和列的名字如果很長,大部分的空間就都被拿來存盤這些資料了,

不過如果采用適當的壓縮演算法就可以極大地節省存盤列族、列等資訊的空間了,所以在實際的使用中,可以通過指定壓縮演算法來壓縮這些元資料,不過壓縮和解壓必然帶來性能損耗,所以使用壓縮也需要根據實際情況來取舍,如果你的資料主要是歸檔資料,不太要求讀寫性能,那么壓縮演算法就比較適合你,

至此,我們終于將HBase剖析到了最小的不可分割的資料結構 KeyValue,

3.9 資料增刪改查的真面目

HBase 是一個可以隨機讀寫的資料庫,而它所基于的持久化層 HDFS 卻是要么新增,要么整個洗掉,不能修改的系統,那 HBase 怎么實作我們的增刪查改的?真實的情況是這樣的:HBase 幾乎總是在做新增操作,

  • 當你新增一個單元格的時候,HBase 在 HDFS 上新增一條資料;

  • 當你修改一個單元格的時候,HBase 在 HDFS 又新增一條資料,只是版本號比之前那個大(或者你自己定義);

  • 當你洗掉一個單元格的時候,HBase 還是新增一條資料!只是這條資料沒有 value,型別為 DELETE,這條資料叫墓碑標記(Tombstone),

由于資料庫在使用程序中積累了很多增刪查改操作,資料的連續性和順序性必然會被破壞,為了提升性能,HBase 每間隔一段時間都會進行一次合并(Compaction),合并的物件為 HFile 檔案,

合并分為 minor compaction 和 major compaction,在 HBase 進行 major compaction 的時候,它會把多個 HFile 合并成 1 個 HFile,在這個程序中,一旦檢測到有被打上墓碑標記的記錄,在合并的程序中就忽略這條記錄,這樣在新產生的 HFile 中,就沒有這條記錄了,自然也就相當于被真正地洗掉了,

3.10 Hbase資料結構總結

HBase 資料的內部結構大體如下:

  • 一個 RegionServer 包含多個 Region,劃分規則是:一個表的一段鍵值在一個 RegionServer 上會產生一個 Region,不過當某一行的資料量太大了(要非常大),HBase 也會把這個 Region 根據列族切分到不同的機器上去;

  • 一個 Region 包含多個 Store,劃分規則是:一個列族分為一個 Store,如果一個表只有一個列族,那么這個表在這個機器上的每一個 Region 里面都只有一個 Store;

  • 一個 Store 里面只有一個 Memstore;

  • 一個 Store 里面有多個 HFile,每次 Memstore 的刷寫(flush)就產生一個新的 HFile 出來,

對應關系如下圖:

四、KeyValue的寫入和讀出

4.1 寫入

一個KeyValue被持久化到HDFS的程序的總結見下圖:

  • WAL:資料被發出之后第一時間被寫入WAL,由于WAL是基于HDFS 來實作的,所以也可以說現在單元格就已經被持久化了,但是 WAL只是一個暫存的日志,它是不區分Store的,這些資料是不能
    被直接讀取和使用,
  • Memstore:資料隨后會立即被放入Memstore中進行整理, Memstore會負責按照LSM樹的結構來存放資料,這個程序就像我 們在打牌的時候,抓牌之后在手上對牌進行整理的程序,
  • HFile:最后,當Memstore太大了達到尺寸上的閥值,或者達到了刷寫時間間隔閥值的時候,HBaes會被這個Memstore的內容刷 寫到HDFS系統上,稱為一個存盤在硬碟上的HFile檔案,至此, 我們可以稱為資料真正地被持久化到硬碟上,就算宕機,斷電, 資料也不會丟失了,

4.2 讀出

由于有 MemStore(基于記憶體)和 HFile(基于HDFS)這兩個機制,你一定會立馬想到先讀取 MemStore,如果找不到,再去 HFile 中查詢,這是顯而易見的機制,可惜 HBase 在處理讀取的時候并不是這樣的,實際的讀取順序是先從 BlockCache 中找資料,找不到了再去 Memstore 和 HFile 中查詢資料,

墓碑標記和資料不在一個地方,讀取資料的時候怎么知道這個資料要洗掉呢?如果這個資料比它的墓碑標記更早被讀到,那在這個時間點真是不知道這個資料會被刪 除,只有當掃描器接著往下讀,讀到墓碑標記的時候才知道這個資料是被標記為洗掉的,不需要回傳給用戶,

所以 HBase 的 Scan 操作在取到所需要的所有行鍵對應的資訊之后還會繼續掃描下去,直到被掃描的資料大于給出的限定條件為止,這樣它才能知道哪些資料應該被回傳給用戶,而哪些應該被舍棄,所以你增加過濾條件也無法減少 Scan 遍歷的行數,只有縮小 STARTROW 和 ENDROW 之間的行鍵范圍才可以明顯地加快掃描的速度,

在 Scan 掃描的時候 store 會創建 StoreScanner 實體,StoreScanner 會把 MemStore 和 HFile 結合起來掃描,所以具體從 MemStore 還是 HFile 中讀取資料,外部的呼叫者都不需要知道具體的細節,當 StoreScanner 打開的時候,會先定位到起始行鍵(STARTROW)上,然后開始往下掃描,

其中紅色塊部分都是屬于指定row的資料,Scan要把所有符合條件 的StoreScanner都掃描過一遍之后才會回傳資料給用戶,

五、 Region的定位

Region 的查找,早期的設計(0.96.0)之前是被稱為三層查詢架構:

  • Region:查找的資料所在的 Region;

  • .META.:是一張元資料表,它存盤了所有 Region 的簡要資訊,.META. 表中的一行記錄就是一個 Region,該行記錄了該 Region 的起始行、結束行和該 Region 的連接資訊,這樣客戶端就可以通過這個來判斷需要的資料在哪個 Region 上;

  • ROOT-:是一張存盤 .META. 表的表,.META. 可以有很多張,而 -ROOT- 就是存盤了 .META. 表在什么 Region 上的資訊(.META. 表也是一張普通的表,也在 Region 上),通過兩層的擴展最多可以支持約 171 億個 Region,

-ROOT- 表記錄在 ZooKeeper 上,路徑為:/hbase/root-region-server;Client 查找資料的流程從宏觀角度來看是這樣的:

  • 用戶通過查找 zk(ZooKeeper)的 /hbase/root-regionserver 節點來知道 -ROOT- 表在什么 RegionServer 上;

  • 訪問 -ROOT- 表,看需要的資料在哪個 .META. 表上,這個 .META. 表在什么 RegionServer 上;

  • 訪問 .META. 表來看要查詢的行鍵在什么 Region 范圍里面;

  • 連接具體的資料所在的 RegionServer,這回就真的開始用 Scan 來遍歷 row 了,

從 0.96 版本之后這個三層查詢架構被改成了二層查詢架構,-ROOT- 表被去掉了,同時 zk 中的 /hbase/root-region-server 也被去掉了,直接把 .META. 表所在的 RegionServer 資訊存盤到了 zk 中的 /hbase/meta-region-server,再后來引入了 namespace,.META. 表被修改成了 hbase:meta,

新版 Region 查找流程:

  • 客戶端先通過 ZooKeeper 的 /hbase/meta-region-server 節點查詢到哪臺 RegionServer 上有 hbase:meta 表,

  • 客戶端連接含有 hbase:meta 表的 RegionServer,hbase:meta 表存盤了所有 Region 的行鍵范圍資訊,通過這個表就可以查詢出要存取的 rowkey 屬于哪個 Region 的范圍里面,以及這個 Region 又是屬于哪個 RegionServer;

  • 獲取這些資訊后,客戶端就可以直連其中一臺擁有要存取的 rowkey 的 RegionServer,并直接對其操作;

  • 客戶端會把 meta 資訊快取起來,下次操作就不需要進行以上加載 hbase:meta 的步驟了,

二層查詢架構如下圖所示:

下半篇主要介紹:
① Region的拆分和合并,及相關經驗總結,
② HFile的合并(compaction)
③ Region自動均衡
④ BlockCache 和 BloomFilter
等,

參考資料:
《HBase不睡覺書》
https://www.cnblogs.com/littleatp/p/11946199.html

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/302191.html

標籤:其他

上一篇:深入理解HDFS

下一篇:資料庫與SQL語言基礎(內含MySQL壓縮包環境安裝詳細步驟)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more