圖文詳解:內存總是不夠,我靠HBase說服了Leader為新項目保駕護航

大家好,我是小羽

最近在工作中用到了 Hbase 這個數據庫,也順便做了關於 Hbase 的知識記錄來分享給大家。其實 Hbase的內容體系真的很多很多,這裡介紹的是小羽認為在工作中會用到的一些技術點,希望可以幫助到大家。

可以這麼說互聯網都是建立在形形色色的數據庫之上的,現在主流的數據庫有這麼幾種:以 MySQL 為代表的關係型數據庫以及其分佈式解決方案,以 Redis 為代表的緩存數據庫,以 ES 為代表的檢索數據庫,再就是分佈式持久化 KV 數據庫。而在開源領域,尤其是國內,HBase 幾乎是分佈式持久化KV數據庫的首選方案。HBase 應用的業務場景非常之多,比如用戶畫像、實時(離線)推薦、實時風控、社交Feed流、商品歷史訂單、社交聊天記錄、監控系統以及用戶行為日誌等等。

前言

我們每一個人無論使用什麼科技產品,都會產生大量的數據,而這些數據的存儲和查詢對於小型數據庫來說其實是很難滿足我們的需求的,因此出現了 HBase 分佈式大數據。HBase 是一個構建在 Hadoop 文件系統之上的面向列的數據庫管理系統。HBase 是一種類似於 Google』s Big Table 的數據模型,它是 Hadoop 生態系統的一部分,它將數據存儲在 HDFS 上,客戶端可以通過 HBase 實現對 HDFS 上數據的隨機訪問。它主要有以下特性:

不支持複雜的事務,只支持行級事務,即單行數據的讀寫都是原子性的;

由於是採用 HDFS 作為底層存儲,所以和 HDFS 一樣,支持結構化、半結構化和非結構化的存儲;

支持通過增加機器進行橫向擴展;

支持數據分片

支持 RegionServers 之間的自動故障轉移

易於使用的 Java 客戶端 API

支持 BlockCache布隆過濾器

過濾器支持謂詞下推

HBase 原理

概念

HBase 是分佈式、面向列的開源數據庫(其實準確的說是面向列族)。HDFS 為 Hbase 提供可靠的底層數據存儲服務MapReduce 為 Hbase 提供高性能的計算能力Zookeeper 為 Hbase 提供穩定服務Failover 機制,因此我們說 Hbase 是一個通過大量廉價的機器解決海量數據的高速存儲和讀取的分佈式數據庫解決方案

列式存儲

我們先來看一下之前的關係型數據庫的按行來存儲的。如下圖:

可以看到只有第一行 ID:1 小羽的這一行的數據都填了,小娜和小智的數據都沒有填完。在我們的行結構中,都是固定的,每一行都一樣,就算不填,也要空着,不能沒有。

來看一下使用了非關係型數據庫的按列存儲的效果圖:

可以看到之前小羽的一列數據對應到了小羽現在的一行數據,原來小羽的七列數據變成了現在的七行。之前的七行數據在一行,共用過一個主鍵 ID:1 。在列式存儲里,變成了七行,每一行都有一個主鍵與其對應,也就是為什麼小羽的主鍵 ID:1 重複了七次。這樣排列最大的好處就是,我們對於不需要的數據就不需要添加,會大大節省我們的空間資源。因為查詢中的選擇規則是通過列來定義的,整個數據庫是自動索引化的。

NoSQL和關係型數據庫對比

對比如下圖

RDBMS 與 Hbase 對比

Hbase 是根據列族來存儲數據的。列族下面可以有非常多的列,列族在創建表的時候就必須指定。為了加深對 Hbase 列族的理解,下面是簡單的關係型數據庫的表和 Hbase 數據庫的表:

主要區別

HBase 架構

Hbase 是由 Client、Zookeeper、Master、HRegionServer、HDFS 等幾個核心體系組成。

Client

Client 使用 HBase 的 RPC 機制與 HMaster、HRegionServer 進行通信。Client 與 HMaster 進行管理類通信,與 HRegion Server 進行數據操作類通信

Zookeeper

Hbase 通過 Zookeeper 來做 master 的高可用、RegionServer 的監控、元數據的入口以及集群配置的維護等工作。具體工作如下:

1. 通過 Zoopkeeper 來保證集群中只有 1 個 master 在運行,如果 master 異常,會通過競爭機制產生新的 master 提供服務

2. 通過 Zoopkeeper 來監控 RegionServer 的狀態,當 RegionSevrer 有異常的時候,通過回調的形式通知 Master RegionServer 上下限的信息

3. 通過 Zoopkeeper 存儲元數據的統一入口地址

客戶端在使用 hbase 的時候,需要添加 zookeeper 的 ip 地址和節點路徑,建立起與zookeeper的連接,建立連接的方式如下面的代碼所示:

Configuration configuration = HBaseConfiguration.create();
configuration.set("hbase.zookeeper.quorum", "XXXX.XXX.XXX");
configuration.set("hbase.zookeeper.property.clientPort", "2181");
configuration.set("zookeeper.znode.parent", "XXXXX");
Connection connection = ConnectionFactory.createConnection(configuration);

Hmaster

master 節點的主要職責如下:

1. 為 RegionServer 分配 Region

2. 維護整個集群的負載均衡

3. 維護集群的元數據信息,發現失效的 Region,並將失效的 Region 分配到正常RegionServer 上當 RegionSever 失效的時候,協調對應 Hlog 的拆分

HRegionServer

HRegionServer 內部管理了一系列 HRegion 對象,每個 HRegion 對應 Table 中的一個 ColumnFamily 的存儲,即一個 Store 管理一個 Region 上的一個列族(CF)。每個 Store 包含一個 MemStore 和 0 到多個 StoreFile。Store 是 HBase 的存儲核心,由 MemStore 和 StoreFile 組成。

HLog

數據在寫入時,首先寫入預寫日誌(Write Ahead Log),每個 HRegionServer 服務的所有 Region 的寫操作日誌都存儲在同一個日誌文件中。數據並非直接寫入 HDFS,而是等緩存到一定數量再批量寫入,寫入完成後在日誌中做標記

MemStore

MemStore 是一個有序的內存緩存區,用戶寫入的數據首先放入 MemStore,當 MemStore 滿了以後 Flush 成一個 StoreFile(存儲時對應為 File),當 StoreFile 數量增到一定閥值,觸發 Compact 合併,將多個 StoreFile 合併成一個 StoreFile。StoreFiles 合併後逐步形成越來越大的 StoreFile,當 Region 內所有 StoreFiles(Hfile) 的總大小超過閥值(hbase.hregion.max.filesize)即觸發分裂 Split,把當前的 Region Split 分成 2 個 Region,父 Region 下線,新 Spilt 出的 2 個孩子 Region 被 HMaster 分配到合適的 HRegionServer 上,使得原先 1 個 Region 的壓力得以分流到 2 個 Region 上。

Region 尋址方式

通過 zookeeper.META,主要有以下幾步:

1. Client 請求 ZK 獲取.META.所在的 RegionServer 的地址

2. Client 請求.META.所在的 RegionServer 獲取訪問數據所在的 RegionServer 地址,client 會將.META.的相關信息 cache 下來,以便下一次快速訪問。

3. Client 請求數據所在的 RegionServer,獲取所需要的數據

HDFS

HDFS 為 Hbase 提供最終的底層數據存儲服務,同時為 Hbase 提供高可用(Hlog 存儲在HDFS)的支持。

HBase 組件

Column Family 列族

Column Family 又叫列族,Hbase 通過列族劃分數據的存儲,列族下面可以包含任意多的列,實現靈活的數據存取。Hbase 表的創建的時候就必須指定列族。就像關係型數據庫創建的時候必須指定具體的列是一樣的。Hbase 的列族不是越多越好,官方推薦的是列族最好小於或者等於 3。我們使用的場景一般是 1 個列族。

Rowkey

Rowkey 的概念和 mysql 中的主鍵是完全一樣的,Hbase 使用 Rowkey 來唯一的區分某一行的數據。Hbase 只支持 3 種查詢方式:基於 Rowkey 的單行查詢,基於 Rowkey 的範圍掃描全表掃描

Region 分區

Region:Region 的概念和關係型數據庫的分區或者分片差不多。Hbase 會將一個大表的數據基於 Rowkey 的不同範圍分配到不同的 Region 中,每個 Region 負責一定範圍的數據訪問和存儲。這樣即使是一張巨大的表,由於被切割到不同的 region,訪問起來的時延也很低

TimeStamp 多版本

TimeStamp 是實現 Hbase 多版本的關鍵。在 Hbase 中使用不同的 timestame 來標識相同 rowkey 行對應的不同版本的數據。在寫入數據的時候,如果用戶沒有指定對應的 timestamp,Hbase 會自動添加一個 timestamp,timestamp 和服務器時間保持一致。在Hbase 中,相同 rowkey 的數據按照 timestamp 倒序排列。默認查詢的是最新的版本,用戶可通過指定 timestamp 的值來讀取舊版本的數據。

Hbase 寫邏輯

Hbase 寫入流程

主要有三個步驟:

1. Client 獲取數據寫入的 Region 所在的 RegionServer

2. 請求寫 Hlog, Hlog 存儲在 HDFS,當 RegionServer 出現異常,需要使用 Hlog 來恢複數據

3. 請求寫 MemStore,只有當寫 Hlog 和寫 MemStore 都成功了才算請求寫入完成。MemStore 後續會逐漸刷到 HDFS 中。

MemStore 刷盤

為了提高 Hbase 的寫入性能,當寫請求寫入 MemStore 後,不會立即刷盤。而是會等到一定的時候進行刷盤的操作。具體是哪些場景會觸發刷盤的操作呢?總結成如下的幾個場景:

1. 這個全局的參數是控制內存整體的使用情況,當所有 memstore 占整個 heap 的最大比例的時候,會觸發刷盤的操作。這個參數是hbase.regionserver.global.memstore.upperLimit,默認為整個 heap 內存的 40%。但這並不意味着全局內存觸發的刷盤操作會將所有的 MemStore 都進行輸盤,而是通過另外一個參數 hbase.regionserver.global.memstore.lowerLimit 來控制,默認是整個 heap 內存的 35%。當 flush 到所有 memstore 占整個 heap 內存的比率為35%的時候,就停止刷盤。這麼做主要是為了減少刷盤對業務帶來的影響,實現平滑系統負載的目的。

2. 當 MemStore 的大小達到 hbase.hregion.memstore.flush.size 大小的時候會觸發刷盤,默認 128M 大小

3. 前面說到 Hlog 為了保證 Hbase 數據的一致性,那麼如果 Hlog 太多的話,會導致故障恢復的時間太長,因此 Hbase 會對 Hlog 的最大個數做限制。當達到 Hlog 的最大個數的時候,會強制刷盤。這個參數是 hase.regionserver.max.logs,默認是 32 個。

4. 可以通過 hbase shell 或者 java api 手工觸發 flush 的操作。

5. 在正常關閉 RegionServer 會觸發刷盤的操作,全部數據刷盤後就不需要再使用 Hlog 恢複數據。

6. 當 RegionServer 出現故障的時候,其上面的 Region 會遷移到其他正常的 RegionServer 上,在恢復完 Region 的數據後,會觸發刷盤,當刷盤完成後才會提供給業務訪問。

HBase 中間層

Phoenix 是 HBase 的開源 SQL 中間層,它允許你使用標準 JDBC 的方式來操作 HBase 上的數據。在 Phoenix 之前,如果你要訪問 HBase,只能調用它的 Java API,但相比於使用一行 SQL 就能實現數據查詢,HBase 的 API 還是過於複雜。Phoenix 的理念是 we put sql SQL back in NOSQL,即你可以使用標準的 SQL 就能完成對 HBase 上數據的操作。同時這也意味着你可以通過集成 Spring Data JPA 或 Mybatis 等常用的持久層框架來操作 HBase。

其次 Phoenix 的性能表現也非常優異,Phoenix 查詢引擎會將 SQL 查詢轉換為一個或多個 HBase Scan,通過並行執行來生成標準的 JDBC 結果集。它通過直接使用 HBase API 以及協處理器和自定義過濾器,可以為小型數據查詢提供毫秒級的性能,為千萬行數據的查詢提供秒級的性能。同時 Phoenix 還擁有二級索引等 HBase 不具備的特性,因為以上的優點,所以 Phoenix 成為了 HBase 最優秀的 SQL 中間層。

HBase 安裝使用

下載 HBase 壓縮包,首先解壓

tar -zxvf hbase-0.98.6-hadoop2-bin.tar.gz

打開 hbase-env.sh 文件配置 JAVA_HOME:

export JAVA_HOME=/opt/modules/jdk1.7.0_79

配置 hbase-site.xml:

<configuration>
 <property>
    <name>hbase.rootdir</name>
    <value>hdfs://hadoop-senior.shinelon.com:8020/hbase</value>
  </property>
  <property>
    <name>hbase.cluster.distributed</name>
    <value>true</value>
  </property>
  <property>
    <name>hbase.zookeeper.quorum</name>
    <value>hadoop-senior.shinelon.com</value>
  </property>
</configuration>

將上面的主機名換為自己的主機名,就可以啟動HBase了。Web 頁面訪問如下:

HBase 命令

下面是小羽整理的一些關於 Hbase 的經常會使用到的命令

HBase API 使用

API 如下

package com.initialize;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.regionserver.BloomType;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
/**
 *  
 *  1、構建連接
 *  2、從連接中取到一個表DDL操作工具admin
 *  3、admin.createTable(表描述對象);
 *  4、admin.disableTable(表名);
 * 5、admin.deleteTable(表名);
 * 6、admin.modifyTable(表名,表描述對象); 
 *
 */
public class HbaseClientDemo {

    Connection conn = null;

    @Before
    public void getConn() throws IOException {
        //構建一個連接對象
        Configuration conf = HBaseConfiguration.create();//會自動加載hbase-site.xml
        conf.set("hbase.zookeeper.quorum","n1:2181,n2:2181,n3:2181");

        conn = ConnectionFactory.createConnection(conf);
    }

    /**
     * DDL
     * 創建表
     */
    @Test
    public void testCreateTable() throws  Exception{

        //從連接中構造一個DDL操作器
        Admin admin = conn.getAdmin();

        //創建一個標定義描述對象
        HTableDescriptor hTableDescriptor = new HTableDescriptor(TableName.valueOf("user_info"));

        //創建列族定義描述對象
        HColumnDescriptor hColumnDescriptor_1 = new HColumnDescriptor("base_info");
        hColumnDescriptor_1.setMaxVersions(3);

        HColumnDescriptor hColumnDescriptor_2 = new HColumnDescriptor("extra_info");

        //將列族定義信息對象放入表定義對象中
        hTableDescriptor.addFamily(hColumnDescriptor_1);
        hTableDescriptor.addFamily(hColumnDescriptor_2);

        //用ddl操作器對象:admin來創建表
        admin.createTable(hTableDescriptor);

        //關閉連接
        admin.close();
        conn.close();

    }

    /**
     * 刪除表
     */
    @Test
    public void testDropTable() throws  Exception{

        Admin admin = conn.getAdmin();

        //停用表
        admin.disableTable(TableName.valueOf("user_info"));
        //刪除表
        admin.deleteTable(TableName.valueOf("user_info"));

        admin.close();
        conn.close();
    }

    /**
     * 修改表定義--添加一個列族
     */
    @Test
    public void testAlterTable() throws  Exception{

        Admin admin = conn.getAdmin();

        //取出舊的表定義信息
        HTableDescriptor tableDescriptor = admin.getTableDescriptor(TableName.valueOf("user_info"));

        //新構造一個列族定義
        HColumnDescriptor hColumnDescriptor = new HColumnDescriptor("other_info");
        hColumnDescriptor.setBloomFilterType(BloomType.ROWCOL);//設置該列族的布隆過濾器類型

        //將列族定義添加到表定義對象中
        tableDescriptor.addFamily(hColumnDescriptor);

        //將修改過的表定義交給admin去提交
        admin.modifyTable(TableName.valueOf("user_info"), tableDescriptor);

        admin.close();
        conn.close();
    }
}

示例如下

package com.initialize;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellScanner;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;

public class HbaseClientDML {

    Connection conn = null;

    @Before
    public void getConn() throws IOException {
        //構建一個連接對象
        Configuration conf = HBaseConfiguration.create();//會自動加載hbase-site.xml
        conf.set("hbase.zookeeper.quorum","n1:2181,n2:2181,n3:2181");

        conn = ConnectionFactory.createConnection(conf);
    }


    /**
     * 增,改:put來覆蓋
     */
    @Test
    public void testPut() throws  Exception{

        //獲取一個操作指定表的table對象,進行DML操作
        Table table = conn.getTable(TableName.valueOf("user_info"));

        //構造要插入的數據為一個Put類型(一個put對象只能對應一個rowkey)的對象
        Put put = new Put(Bytes.toBytes("001"));
        put.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("username"),Bytes.toBytes("小羽"));
        put.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("age"), Bytes.toBytes("18"));
        put.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("addr"), Bytes.toBytes("成都"));

        Put put2 = new Put(Bytes.toBytes("002"));
        put2.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("username"), Bytes.toBytes("小娜"));
        put2.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("age"), Bytes.toBytes("17"));
        put2.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("addr"), Bytes.toBytes("成都"));

        ArrayList<Put> puts = new ArrayList<>();
        puts.add(put);
        puts.add(put2);

        //插進去
        table.put(puts);

        table.close();
        conn.close();
    }

    /***
     * 循環插入大量數據
     */
    @Test
    public void testManyPuts() throws Exception{

        Table table = conn.getTable(TableName.valueOf("user_info"));
        ArrayList<Put> puts = new ArrayList<>();

        for(int i=0;i<10000;i++){
            Put put = new Put(Bytes.toBytes(""+i));
            put.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("usernaem"), Bytes.toBytes("小羽" +i));
            put.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("age"), Bytes.toBytes((18+i) + ""));
            put.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("addr"), Bytes.toBytes("成都"));

            puts.add(put);
        }

        table.put(puts);
    }

    /**
     * 刪
     */
    @Test
    public void testDelete() throws Exception{
        Table table = conn.getTable(TableName.valueOf("user_info"));

        //構造一個對象封裝要刪除的數據信息
        Delete delete1 = new Delete(Bytes.toBytes("001"));

        Delete delete2 = new Delete(Bytes.toBytes("002"));
        delete2.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("addr"));

        ArrayList<Delete> dels = new ArrayList<>();
        dels.add(delete1);
        dels.add(delete2);

        table.delete(dels);

        table.close();
        conn.close();
    }

    /**
     * 查
     */
    @Test
    public void  testGet() throws Exception{

        Table table = conn.getTable(TableName.valueOf("user_info"));

        Get get = new Get("002".getBytes());

        Result result = table.get(get);

        //從結果中取用戶指定的某個key和value的值
        byte[] value = result.getValue("base_info".getBytes(), "age".getBytes());
        System.out.println(new String(value));

        System.out.println("======================");

        //遍歷整行結果中的所有kv單元格
        CellScanner cellScanner = result.cellScanner();
        while(cellScanner.advance()){
            Cell cell = cellScanner.current();

            byte[] rowArray = cell.getRowArray();//本kv所屬行鍵的位元組數組
            byte[] familyArray = cell.getFamilyArray();//列族名的位元組數組
            byte[] qualifierArray = cell.getQualifierArray();//列名的位元組數組
            byte[] valueArray = cell.getValueArray();//value位元組數組
            
            System.out.println("行鍵:" + new String(rowArray, cell.getRowOffset(), cell.getRowLength()));
            System.out.println("列族名:" + new String(familyArray, cell.getFamilyOffset(), cell.getFamilyLength()));
            System.out.println("列名:" + new String(qualifierArray, cell.getQualifierOffset(), cell.getQualifierLength()));
            System.out.println("value:" + new String(valueArray, cell.getValueOffset(), cell.getValueLength()));

        }
        table.close();
        conn.close();

    }


    /**
     * 按行鍵範圍查詢數據
     */
    @Test
    public void testScan() throws Exception{

        Table table = conn.getTable(TableName.valueOf("user_info"));

        //包含起始行鍵,不包含結束行鍵,但是如果真的是想要查詢出末尾的那個行鍵,可以在尾行鍵上拼接一個不可見的字符(\000)
        Scan scan = new Scan("10".getBytes(), "10000\001".getBytes());

        ResultScanner scanner =table.getScanner(scan);

        Iterator<Result> iterator = scanner.iterator();

        while(iterator.hasNext()){

            Result result =iterator.next();
            //遍歷整行結果中的所有kv單元格
            CellScanner cellScanner = result.cellScanner();
            while(cellScanner.advance()){
                Cell cell = cellScanner.current();

                byte[] rowArray = cell.getRowArray();//本kv所屬行鍵的位元組數組
                byte[] familyArray = cell.getFamilyArray();//列族名的位元組數組
                byte[] qualifierArray = cell.getQualifierArray();//列明的位元組數組
                byte[] valueArray = cell.getValueArray();//value位元組數組

                System.out.println("行鍵:" + new String(rowArray, cell.getRowOffset(), cell.getRowLength()));
                System.out.println("列族名:" + new String(familyArray, cell.getFamilyOffset(), cell.getFamilyLength()));
                System.out.println("列名:" + new String(qualifierArray, cell.getQualifierOffset(), cell.getQualifierLength()));
                System.out.println("value:" + new String(valueArray, cell.getValueOffset(), cell.getValueLength()));
            }
            System.out.println("----------------------");
        }
    }

    @Test
    public void test(){
        String a = "000";
        String b = "000\0";

        System.out.println(a);
        System.out.println(b);

        byte[] bytes = a.getBytes();
        byte[] bytes2 = b.getBytes();
        
        System.out.println("");
    }
}
 

HBase 應用場景

對象存儲系統

HBase MOB(Medium Object Storage),中等對象存儲是 hbase-2.0.0 版本引入的新特性,用於解決 hbase 存儲中等文件(0.1m~10m)性能差的問題。這個特性適合將圖片、文檔、PDF、小視頻存儲到 Hbase 中。

OLAP 的存儲

Kylin 的底層用的是 HBase 的存儲,看中的是它的高並發和海量存儲能力。kylin 構建 cube 的過程會產生大量的預聚合中間數據,數據膨脹率高,對數據庫的存儲能力有很高要求。

Phoenix 是構建在 HBase 上的一個 SQL 引擎,通過 phoenix 可以直接調用 JDBC 接口操作 Hbase,雖然有 upsert 操作,但是更多的是用在 OLAP 場景,缺點是非常不靈活

時序型數據

openTsDB 應用,記錄以及展示指標在各個時間點的數值,一般用於監控的場景,是 HBase 上層的一個應用。

用戶畫像系統

動態列,稀疏列的特性。用於描述用戶特徵的維度數是不定的且可能會動態增長的(比如愛好,性別,住址等),不是每個特徵維度都會有數據。

消息/訂單系統

強一致性,良好的讀性能,hbase 可以保證強一致性

feed 流系統存儲

feed 流系統具有讀多寫少、數據模型簡單、高並發、波峰波谷式訪問、持久化可靠性存儲、消息排序這些特點,比如說 HBase 的 rowKey 按字典序排序正好適用於這個場景。

Hbase 優化

預先分區

默認情況下,在創建 HBase 表的時候會自動創建一個 Region 分區,當導入數據的時候,所有的 HBase 客戶端都向這一個 Region 寫數據,直到這個 Region 足夠大了才進行切分。一種可以加快批量寫入速度的方法是通過預先創建一些空的 Regions,這樣當數據寫入 HBase 時,會按照 Region 分區情況,在集群內做數據的負載均衡

Rowkey 優化

HBase 中 Rowkey 是按照字典序存儲,因此,設計 Rowkey 時,要充分利用排序特點,將經常一起讀取的數據存儲到一塊,將最近可能會被訪問的數據放在一塊。

此外,Rowkey 若是遞增的生成,建議不要使用正序直接寫入 Rowkey,而是採用 reverse 的方式反轉 Rowkey,使得 Rowkey 大致均衡分佈,這樣設計有個好處是能將 RegionServer 的負載均衡,否則容易產生所有新數據都在一個 RegionServer 上堆積的現象,這一點還可以結合 table 的預切分一起設計。

減少列族數量

不要在一張表裡定義太多的 ColumnFamily。目前 Hbase 並不能很好的處理超過 2~3ColumnFamily 的表。因為某個 ColumnFamily 在 flush 的時候,它鄰近的 ColumnFamily 也會因關聯效應被觸發 flush,最終導致系統產生更多的 I/O。

緩存策略

創建表的時候,可以通過 HColumnDescriptor.setInMemory(true) 將表放到 RegionServer 的緩存中,保證在讀取的時候被 cache 命中。

設置存儲生命期

創建表的時候,可以通過 HColumnDescriptor.setTimeToLive(int timeToLive) 設置表中數據的存儲生命期,過期數據將自動被刪除。

硬盤配置

每台 RegionServer 管理 10~1000 個 Regions,每個 Region 在 1~2G,則每台 Server 最少要 10G,最大要1000*2G=2TB,考慮 3 備份,則要 6TB。方案一是用 3 塊 2TB 硬盤,二是用 12 塊 500G 硬盤,帶寬足夠時,後者能提供更大的吞吐率,更細粒度的冗餘備份,更快速的單盤故障恢復。

分配合適的內存給 RegionServer 服務

在不影響其他服務的情況下,越大越好。例如在 HBase 的 conf 目錄下的 hbase-env.sh 的最後添加 export HBASE_REGIONSERVER_OPTS=”-Xmx16000m$HBASE_REGIONSERVER_OPTS」,其中 16000m 為分配給 RegionServer 的內存大小

寫數據的備份數

備份數與讀性能成正比,與寫性能成反比,且備份數影響高可用性。有兩種配置方式,一種是將 hdfs-site.xml拷貝到 hbase 的 conf 目錄下,然後在其中添加或修改配置項 dfs.replication 的值為要設置的備份數,這種修改對所有的 HBase 用戶表都生效,另外一種方式,是改寫 HBase 代碼,讓 HBase 支持針對列族設置備份數,在創建表時,設置列族備份數,默認為 3,此種備份數只對設置的列族生效

WAL(預寫日誌)

可設置開關,表示 HBase 在寫數據前用不用先寫日誌,默認是打開,關掉會提高性能,但是如果系統出現故障(負責插入的 RegionServer 掛掉),數據可能會丟失。配置 WAL 在調用 JavaAPI 寫入時,設置 Put 實例的 WAL,調用 Put.setWriteToWAL(boolean)

批量寫

HBase 的 Put 支持單條插入,也支持批量插入,一般來說批量寫更快,節省來回的網絡開銷。在客戶端調用 JavaAPI 時,先將批量的 Put 放入一個 Put 列表,然後調用 HTable 的 Put(Put 列表) 函數來批量寫

最後

在理解 HBase 時,可以發現 HBase 的設計其實和 Elasticsearch 十分相似,如 HBase 的 Flush&Compact 機制等設計與 Elasticsearch 如出一轍,因此理解起來比較順利。

從本質上來說,HBase 的定位是分佈式存儲系統,Elasticsearch 是分佈式搜索引擎,兩者並不等同,但兩者是互補的。HBase 的搜索能力有限,只支持基於 RowKey 的索引,其它二級索引等高級特性需要自己開發。因此,有一些案例是結合 HBase 和 Elasticsearch 實現存儲 + 搜索的能力。通過 HBase 彌補 Elasticsearch 存儲能力的不足,通過 Elasticsearch 彌補 HBase 搜索能力的不足。

其實,不只是 HBase 和 Elasticsearch。任何一種分佈式框架或系統,它們都有一定的共性,不同之處在於各自的關注點不同。小羽的感受是,在學習分佈式中間件時,應先弄清其核心關注點,再對比其它中間件,提取共性和特性,進一步加深理解。