HBase 數據庫檢索性能優化策略

HBase 數據庫是一個基於分佈式的、面向列的、主要用於非結構化數據存儲用途的開源數據庫。其設計思路來源於 Google 的非開源數據庫」BigTable」。
java

HBase 調用 API 示例

相似於操做關係型數據庫的 JDBC 庫,HBase client 包自己提供了大量能夠供操做的 API,幫助用戶快速操做 HBase 數據庫。提供了諸如建立數據表、刪除數據表、增長字段、存入數據、讀取數據等等接口。清單 1 提供了一個做者封裝的工具類,包括操做數據表、讀取數據、存入數據、導出數據等方法。程序員

清單 1.HBase API 操做工具類代碼數據庫

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class HBaseUtil {
    private Configuration conf = null;
    private HBaseAdmin admin = null;
    protected HBaseUtil(Configuration conf) throws IOException
    {
        this.conf = conf;
        this.admin = new HBaseAdmin(conf);
    }
    public boolean existsTable(String table)
    throws IOException
    {
        return admin.tableExists(table);
    }
    public void createTable(String table, byte[][] splitKeys, String... colfams)
    throws IOException
    {
        HTableDescriptor desc = new HTableDescriptor(table);
        for (String cf : colfams)
        {
            HColumnDescriptor coldef = new HColumnDescriptor(cf);
            desc.addFamily(coldef);
        }
        if (splitKeys != null)
        {
            admin.createTable(desc, splitKeys);
        }
        else
        {
            admin.createTable(desc);
        }
    }
    public void disableTable(String table) throws IOException
    {
        admin.disableTable(table);
    }
    public void dropTable(String table) throws IOException
    {
        if (existsTable(table))
        {
            disableTable(table);
            admin.deleteTable(table);
        }
    }
    public void fillTable(String table, int startRow, int endRow, int numCols,
                          int pad, boolean setTimestamp, boolean random,
                          String... colfams) throws IOException
    {
        HTable tbl = new HTable(conf, table);
        for (int row = startRow; row <= endRow; row++)
        {
            for (int col = 0; col < numCols; col++)
            {
                Put put = new Put(Bytes.toBytes("row-"));
                for (String cf : colfams)
                {
                    String colName = "col-";
                    String val = "val-";
                    if (setTimestamp)
                    {
                        put.add(Bytes.toBytes(cf), Bytes.toBytes(colName),
                                col, Bytes.toBytes(val));
                    }
                    else
                    {
                        put.add(Bytes.toBytes(cf), Bytes.toBytes(colName),
                                Bytes.toBytes(val));
                    }
                }
                tbl.put(put);
            }
        }
        tbl.close();
    }
    public void put(String table, String row, String fam, String qual,
                    String val) throws IOException
    {
        HTable tbl = new HTable(conf, table);
        Put put = new Put(Bytes.toBytes(row));
        put.add(Bytes.toBytes(fam), Bytes.toBytes(qual), Bytes.toBytes(val));
        tbl.put(put);
        tbl.close();
    }
    public void put(String table, String row, String fam, String qual, long ts,
                    String val) throws IOException
    {
        HTable tbl = new HTable(conf, table);
        Put put = new Put(Bytes.toBytes(row));
        put.add(Bytes.toBytes(fam), Bytes.toBytes(qual), ts, Bytes.toBytes(val));
        tbl.put(put);
        tbl.close();
    }
    public void put(String table, String[] rows, String[] fams, String[] quals,
                    long[] ts, String[] vals) throws IOException
    {
        HTable tbl = new HTable(conf, table);
        for (String row : rows)
        {
            Put put = new Put(Bytes.toBytes(row));
            for (String fam : fams)
            {
                int v = 0;
                for (String qual : quals)
                {
                    String val = vals[v < vals.length ? v : vals.length];
                    long t = ts[v < ts.length ? v : ts.length - 1];
                    put.add(Bytes.toBytes(fam), Bytes.toBytes(qual), t,
                            Bytes.toBytes(val));
                    v++;
                }
            }
            tbl.put(put);
        }
        tbl.close();
    }
    public void dump(String table, String[] rows, String[] fams, String[] quals)
    throws IOException
    {
        HTable tbl = new HTable(conf, table);
        List<Get> gets = new ArrayList<Get>();
        for (String row : rows)
        {
            Get get = new Get(Bytes.toBytes(row));
            get.setMaxVersions();
            if (fams != null)
            {
                for (String fam : fams)
                {
                    for (String qual : quals)
                    {
                        get.addColumn(Bytes.toBytes(fam), Bytes.toBytes(qual));
                    }
                }
            }
            gets.add(get);
        }
        Result[] results = tbl.get(gets);
        for (Result result : results)
        {
            for (KeyValue kv : result.raw())
            {
                System.out.println("KV: " + kv +
                                   ", Value: " + Bytes.toString(kv.getValue()));
            }
        }
    }
    private static void scan(int caching, int batch) throws IOException
    {
        HTable table = null;
        final int[] counters = {0, 0};
        Scan scan = new Scan();
        scan.setCaching(caching); // co ScanCacheBatchExample-1-Set Set caching and batch parameters.
        scan.setBatch(batch);
        ResultScanner scanner = table.getScanner(scan);
        for (Result result : scanner)
        {
            counters[1]++; // co ScanCacheBatchExample-2-Count Count the number of Results available.
        }
        scanner.close();
        System.out.println("Caching: " + caching + ", Batch: " + batch +
                           ", Results: " + counters[1] + ", RPCs: " + counters[0]);
    }
}

操做表的 API 都有 HBaseAdmin 提供,特別講解一下 Scan 的操做部署。apache

HBase 的表數據分爲多個層次,HRegion->HStore->[HFile,HFile,…,MemStore]。api

在 HBase 中,一張表能夠有多個 Column Family,在一次 Scan 的流程中,每一個 Column Family(Store) 的數據讀取由一個 StoreScanner 對象負責。每一個 Store 的數據由一個內存中的 MemStore 和磁盤上的 HFile 文件組成,對應的 StoreScanner 對象使用一個 MemStoreScanner 和 N 個 StoreFileScanner 來進行實際的數據讀取。數組

所以,讀取一行的數據須要如下步驟:緩存

按照順序讀取出每一個 Store服務器

對於每一個 Store,合併 Store 下面的相關的 HFile 和內存中的 MemStoremarkdown

這兩步都是經過堆來完成。RegionScanner 的讀取經過下面的多個 StoreScanner 組成的堆完成,使用 RegionScanner 的成員變量 KeyValueHeap storeHeap 表示。一個 StoreScanner 一個堆,堆中的元素就是底下包含的 HFile 和 MemStore 對應的 StoreFileScanner 和 MemStoreScanner。堆的優點是建堆效率高,能夠動態分配內存大小,沒必要事先肯定生存週期。網絡

接着調用 seekScanners() 對這些 StoreFileScanner 和 MemStoreScanner 分別進行 seek。seek 是針對 KeyValue 的,seek 的語義是 seek 到指定 KeyValue,若是指定 KeyValue 不存在,則 seek 到指定 KeyValue 的下一個。

Scan類經常使用方法說明:

scan.addFamily()/scan.addColumn():指定須要的 Family 或 Column,若是沒有調用任何 addFamily 或 Column,會返回全部的 Columns;

scan.setMaxVersions():指定最大的版本個數。若是不帶任何參數調用 setMaxVersions,表示取全部的版本。若是不掉用 setMaxVersions,只會取到最新的版本.;

scan.setTimeRange():指定最大的時間戳和最小的時間戳,只有在此範圍內的 Cell 才能被獲取;

scan.setTimeStamp():指定時間戳;

scan.setFilter():指定 Filter 來過濾掉不須要的信息;

scan.setStartRow():指定開始的行。若是不調用,則從表頭開始;

scan.setStopRow():指定結束的行(不含此行);

scan. setCaching():每次從服務器端讀取的行數(影響 RPC);

scan.setBatch():指定最多返回的 Cell 數目。用於防止一行中有過多的數據,致使 OutofMemory 錯誤,默認無限制。

HBase 數據表優化

HBase 是一個高可靠性、高性能、面向列、可伸縮的分佈式數據庫,可是當併發量太高或者已有數據量很大時,讀寫性能會降低。咱們能夠採用以下方式逐步提高 HBase 的檢索速度。

預先分區

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

Rowkey 優化

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

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

減小ColumnFamily 數量

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

緩存策略 (setCaching)

建立表的時候,能夠經過 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 在調用 Java API 寫入時,設置 Put 實例的 WAL,調用 Put.setWriteToWAL(boolean)。

批量寫

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

客戶端一次從服務器拉取的數量

經過配置一次拉去的較大的數據量能夠減小客戶端獲取數據的時間,可是它會佔用客戶端內存。有三個地方可進行配置:

在 HBase 的 conf 配置文件中進行配置 hbase.client.scanner.caching;

經過調用 HTable.setScannerCaching(int scannerCaching) 進行配置;

經過調用 Scan.setCaching(int caching) 進行配置。三者的優先級愈來愈高。

RegionServer 的請求處理 IO 線程數

較少的 IO 線程適用於處理單次請求內存消耗較高的 Big Put 場景 (大容量單次 Put 或設置了較大 cache 的 Scan,均屬於 Big Put) 或 ReigonServer 的內存比較緊張的場景。

較多的 IO 線程,適用於單次請求內存消耗低,TPS 要求 (每秒事務處理量 (TransactionPerSecond)) 很是高的場景。設置該值的時候,以監控內存爲主要參考。

在 hbase-site.xml 配置文件中配置項爲 hbase.regionserver.handler.count。

Region 大小設置

配置項爲 hbase.hregion.max.filesize,所屬配置文件爲 hbase-site.xml.,默認大小 256M。

在當前 ReigonServer 上單個 Reigon 的最大存儲空間,單個 Region 超過該值時,這個 Region 會被自動 split 成更小的 Region。小 Region 對 split 和 compaction 友好,由於拆分 Region 或 compact 小 Region 裏的 StoreFile 速度很快,內存佔用低。缺點是 split 和 compaction 會很頻繁,特別是數量較多的小 Region 不停地 split, compaction,會致使集羣響應時間波動很大,Region 數量太多不只給管理上帶來麻煩,甚至會引起一些 Hbase 的 bug。通常 512M 如下的都算小 Region。大 Region 則不太適合常常 split 和 compaction,由於作一次 compact 和 split 會產生較長時間的停頓,對應用的讀寫性能衝擊很是大。

此外,大 Region 意味着較大的 StoreFile,compaction 時對內存也是一個挑戰。若是你的應用場景中,某個時間點的訪問量較低,那麼在此時作 compact 和 split,既能順利完成 split 和 compaction,又能保證絕大多數時間平穩的讀寫性能。compaction 是沒法避免的,split 能夠從自動調整爲手動。只要經過將這個參數值調大到某個很難達到的值,好比 100G,就能夠間接禁用自動 split(RegionServer 不會對未到達 100G 的 Region 作 split)。再配合 RegionSplitter 這個工具,在須要 split 時,手動 split。手動 split 在靈活性和穩定性上比起自動 split 要高不少,並且管理成本增長很少,比較推薦 online 實時系統使用。內存方面,小 Region 在設置 memstore 的大小值上比較靈活,大 Region 則過大太小都不行,過大會致使 flush 時 app 的 IO wait 增高,太小則因 StoreFile 過多影響讀性能。

HBase 配置

建議 HBase 的服務器內存至少 32G,表 1 是經過實踐檢驗獲得的分配給各角色的內存建議值。

模塊 服務種類 內存需求

HDFS    HDFS NameNode   16GB
HDFS DataNode   2GB 
HBase   HMaster 2GB
HRegionServer   16GB    
ZooKeeper   ZooKeeper   4GB

表 1. HBase 相關服務配置信息




HBase 的單個 Region 大小建議設置大一些,推薦 2G,RegionServer 處理少許的大 Region 比大量的小 Region 更快。對於不重要的數據,在建立表時將其放在單獨的列族內,而且設置其列族備份數爲 2(默認是這樣既保證了雙備份,又能夠節約空間,提升寫性能,代價是高可用性比備份數爲 3 的稍差,且讀性能不如默認備份數的時候。

實際案例

項目要求能夠刪除存儲在 HBase 數據表中的數據,數據在 HBase 中的 Rowkey 由任務 ID(數據由任務產生) 加上 16 位隨機數組成,任務信息由單獨一張表維護。圖 2 所示是數據刪除流程圖。

圖 2. 數據刪除流程圖




最初的設計是在刪除任務的同時按照任務 ID 刪除該任務存儲在 HBase 中的相應數據。可是 HBase 數據較多時會致使刪除耗時較長,同時因爲磁盤 I/O 較高,會致使數據讀取、寫入超時。

查看 HBase 日誌發現刪除數據時,HBase 在作 Major Compaction 操做。Major Compaction 操做的目的是合併文件,並清除刪除、過時、多餘版本的數據。Major Compaction 時 HBase 將合併 Region 中 StoreFile,該動做若是持續長時間會致使整個 Region 都不可讀,最終致使全部基於這些 Region 的查詢超時。

若是想要解決 Major Compaction 問題,須要查看它的源代碼。經過查看 HBase 源碼發現 RegionServer 在啓動時候,有個 CompactionChecker 線程在按期檢測是否須要作 Compact。源代碼如圖 3 所示。




圖 3. CompactionChecker 線程代碼圖

isMajorCompaction 中會根據 hbase.hregion.majorcompaction 參數來判斷是否作 Major Compact。若是 hbase.hregion.majorcompaction 爲 0,則返回 false。修改配置文件 hbase.hregion.majorcompaction 爲 0,禁止 HBase 的按期 Major Compaction 機制,經過自定義的定時機制 (在凌晨 HBase 業務不繁忙時) 執行 Major 操做,這個定時能夠是經過 Linux cron 定時啓動腳本,也能夠經過 Java 的 timer schedule,在實際項目中使用 Quartz 來啓動,啓動的時間配置在配置文件中給出,能夠方便的修改 Major Compact 啓動的時間。經過這種修改後,咱們發如今刪除數據後仍會有 Compact 操做。這樣流程進入 needsCompaction = true 的分支。查看 needsCompaction 判斷條件爲 (storefiles.size() – filesCompacting.size()) > minFilesToCompact 觸發。同時當需緊縮的文件數等於 Store 的全部文件數,Minor Compact 自動升級爲 Major Compact。可是 Compact 操做不能禁止,由於這樣會致使數據一直存在,最終影響查詢效率。

基於以上分析,咱們必須從新考慮刪除數據的流程。對用戶來講,用戶只要在檢索時對於刪除的任務不進行檢索便可。那麼只須要刪除該條任務記錄,對於該任務相關聯的數據不須要立馬進行刪除。當系統空閒時候再去定時刪除 HBase 數據表中的數據,並對 Region 作 Major Compact,清理已經刪除的數據。經過對任務刪除流程的修改,達到項目的需求,同時這種修改也不須要修改 HBase 的配置。



圖 4. 數據刪除流程對比圖

檢索、查詢、刪除 HBase 數據表中的數據自己存在大量的關聯性,須要查看 HBase 數據表的源代碼才能肯定致使檢索性能瓶頸的根本緣由及最終解決方案。

結束語

HBase 數據庫的使用及檢索優化方式均與傳統關係型數據庫存在較多不一樣,本文從數據表的基本定義方式出發,經過 HBase 自身提供的 API 訪問方式入手,舉例說明優化方式及注意事項,最後經過實例來驗證優化方案可行性。檢索性能自己是數據表設計、程序設計、邏輯設計等的結合產物,須要程序員深刻理解後才能作出正確的優化方案。

相關文章
相關標籤/搜索