【轉載】HBase 數據庫檢索性能優化策略

轉自:http://www.ibm.com/developerworks/cn/java/j-lo-HBase/index.htmlhtml

高性能 HBase 數據庫java

本文首先介紹了 HBase 數據庫基本原理及專用術語,而後介紹了 HBase 數據庫發佈的操做 API 及部分示例,重點介紹了 Scan 方法的操做方式,接着介紹了檢索 HBase 數據庫時的優化方案,最後經過一個示例總結了實際項目中遇到的檢索速度慢的解決方案。程序員

HBase 數據表介紹

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

HDFS 爲 HBase 提供底層存儲支持,MapReduce 爲其提供計算能力,ZooKeeper 爲其提供協調服務和 failover(失效轉移的備份操做)機制。Pig 和 Hive 爲 HBase 提供了高層語言支持,使其能夠進行數據統計(可實現多表 join 等),Sqoop 則爲其提供 RDBMS 數據導入功能。apache

HBase 不能支持 where 條件、Order by 查詢,只支持按照主鍵 Rowkey 和主鍵的 range 來查詢,可是能夠經過 HBase 提供的 API 進行條件過濾。編程

HBase 的 Rowkey 是數據行的惟一標識,必須經過它進行數據行訪問,目前有三種方式,單行鍵訪問、行鍵範圍訪問、全表掃描訪問。數據按行鍵的方式排序存儲,依次按位比較,數值較大的排列在後,例如 int 方式的排序:1,10,100,11,12,2,20…,906,…。數組

ColumnFamily 是「列族」,屬於 schema 表,在建表時定義,每一個列屬於一個列族,列名用列族做爲前綴「ColumnFamily:qualifier」,訪問控制、磁盤和內存的使用統計都是在列族層面進行的。緩存

Cell 是經過行和列肯定的一個存儲單元,值以字節碼存儲,沒有類型。sass

Timestamp 是區分不一樣版本 Cell 的索引,64 位整型。不一樣版本的數據按照時間戳倒序排列,最新的數據版本排在最前面。服務器

Hbase 在行方向上水平劃分紅 N 個 Region,每一個表一開始只有一個 Region,數據量增多,Region 自動分裂爲兩個,不一樣 Region 分佈在不一樣 Server 上,但同一個不會拆分到不一樣 Server。

Region 按 ColumnFamily 劃分紅 Store,Store 爲最小存儲單元,用於保存一個列族的數據,每一個 Store 包括內存中的 memstore 和持久化到 disk 上的 HFile。

圖 1 是 HBase 數據表的示例,數據分佈在多臺節點機器上面。

圖 1. HBase 數據表示例
圖 1. HBase 數據表示例
 

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 的操做部署。

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

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

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

1. 按照順序讀取出每一個 Store

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

這兩步都是經過堆來完成。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 列表) 函數來批量寫。

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

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

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

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

3)經過調用 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 是經過實踐檢驗獲得的分配給各角色的內存建議值。

表 1. HBase 相關服務配置信息
模塊 服務種類 內存需求
HDFS HDFS NameNode 16GB
HDFS DataNode 2GB
HBase HMaster 2GB
HRegionServer 16GB
ZooKeeper ZooKeeper 4GB

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

實際案例

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

圖 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 線程代碼圖
圖 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. 數據刪除流程對比圖
圖 4. 數據刪除流程對比圖

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

 

結束語

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

參考資料

學習

  • 參考 developerWorks 中國關於 HBase 知識 檢索頁面,查看 IBM 開發者論壇公佈的關於 HBase 的相關文章。
  • 查看文章「淺談 HBase」,做者對於 HBase 數據表做了基礎解釋。
  • 查看書籍《HBase Definition》,做者爲 HBase 創始人,對 HBase 數據庫進行權威解答。
  • 查看博客「HBase雜談」,做者有較多的實際經驗。
  • developerWorks Java 技術專區:這裏有數百篇關於 Java 編程各個方面的文章。

討論

  • 加入 developerWorks 中文社區,查看開發人員推進的博客、論壇、組和維基,並與其餘 developerWorks 用戶交流。
相關文章
相關標籤/搜索