HBase 數據庫是一個基於分佈式的、面向列的、主要用於非結構化數據存儲用途的開源數據庫。其設計思路來源於 Google 的非開源數據庫」BigTable」。html
HDFS 爲 HBase 提供底層存儲支持,MapReduce 爲其提供計算能力。ZooKeeper 爲其提供協調服務和 failover(失效轉移的備份操做)機制。Pig 和 Hive 爲 HBase 提供了高層語言支持,使其可以進行數據統計(可實現多表 join 等)。Sqoop 則爲其提供 RDBMS 數據導入功能。java
HBase 不能支持 where 條件、Order by 查詢,僅僅支持依照主鍵 Rowkey 和主鍵的 range 來查詢,但是可以經過 HBase 提供的 API 進行條件過濾。數據庫
HBase 的 Rowkey 是數據行的惟一標識。必須經過它進行數據行訪問。眼下有三種方式。單行鍵訪問、行鍵範圍訪問、全表掃描訪問。數據按行鍵的方式排序存儲,依次按位比較,數值較大的排列在後,好比 int 方式的排序:1,10,100。11。12。2,20…,906,…。apache
ColumnFamily 是「列族」,屬於 schema 表,在建表時定義,每個列屬於一個列族,列名用列族做爲前綴「ColumnFamily:qualifier」,訪問控制、磁盤和內存的使用統計都是在列族層面進行的。數組
Cell 是經過行和列肯定的一個存儲單元,值以字節碼存儲,沒有類型。緩存
Timestamp 是區分不一樣版本號 Cell 的索引,64 位整型。不一樣版本號的數據依照時間戳倒序排列,最新的數據版本號排在最前面。網絡
Hbase 在行方向上水平劃分紅 N 個 Region,每個表一開始僅僅有一個 Region,數據量增多,Region 本身主動分裂爲兩個。不一樣 Region 分佈在不一樣 Server 上。但同一個不會拆分到不一樣 Server。併發
Region 按 ColumnFamily 劃分紅 Store,Store 爲最小存儲單元。用於保存一個列族的數據。每個 Store 包含內存中的 memstore 和持久化到 disk 上的 HFile。app
圖 1 是 HBase 數據表的演示樣例,數據分佈在多臺節點機器上面。負載均衡
相似於操做關係型數據庫的 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 來進行實際的數據讀取。
所以,讀取一行的數據需要下面步驟:
這兩步都是經過堆來完畢。RegionScanner 的讀取經過如下的多個 StoreScanner 組成的堆完畢。使用 RegionScanner 的成員變量 KeyValueHeap storeHeap 表示。一個 StoreScanner 一個堆,堆中的元素就是底下包括的 HFile 和 MemStore 相應的 StoreFileScanner 和 MemStoreScanner。堆的優點是建堆效率高,可以動態分配內存大小,沒必要事先肯定生存週期。
接着調用 seekScanners() 對這些 StoreFileScanner 和 MemStoreScanner 分別進行 seek。seek 是針對 KeyValue 的。seek 的語義是 seek 到指定 KeyValue。假設指定 KeyValue 不存在,則 seek 到指定 KeyValue 的下一個。
Scan類常常用法說明:
假設不帶不論什麼參數調用 setMaxVersions,表示取所有的版本號。假設不掉用 setMaxVersions,僅僅會取到最新的版本號.;
用於防止一行中有過多的數據,致使 OutofMemory 錯誤,默認無限制。
HBase 是一個高可靠性、高性能、面向列、可伸縮的分佈式數據庫,但是當併發量太高或者已有數據量很是大時。讀寫性能會降低。咱們可以採用例如如下方式逐步提高 HBase 的檢索速度。
預先分區
默認狀況下,在建立 HBase 表的時候會本身主動建立一個 Region 分區。當導入數據的時候,所有的 HBase client都向這一個 Region 寫數據,直到這個 Region 足夠大了才進行切分。
一種可以加快批量寫入速度的方法是經過預先建立一些空的 Regions。這樣當數據寫入 HBase 時,會依照 Region 分區狀況。在集羣內作數據的負載均衡。
Rowkey 優化
HBase 中 Rowkey 是依照字典序存儲,所以,設計 Rowkey 時。要充分利用排序特色,將經常一塊兒讀取的數據存儲到一塊,將近期可能會被訪問的數據放在一塊。
此外,Rowkey 如果遞增的生成。建議不要使用正序直接寫入 Rowkey,而是採用 reverse 的方式反轉 Rowkey,使得 Rowkey 大體均衡分佈,這樣設計有個優勢是能將 RegionServer 的負載均衡,不然easy產生所有新數據都在一個 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 支持單條插入,也支持批量插入,通常來講批量寫更快,節省來回的網絡開銷。在client調用 Java API 時,先將批量的 Put 放入一個 Put 列表,而後調用 HTable 的 Put(Put 列表) 函數來批量寫。
client一次從server拉取的數量
經過配置一次拉去的較大的數據量可以下降client獲取數據的時間,但是它會佔用client內存。
有三個地方可進行配置:
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 的server內存至少 32G,表 1 是經過實踐檢驗獲得的分配給各角色的內存建議值。
模塊 | 服務種類 | 內存需求 |
---|---|---|
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 所看到的是數據刪除流程圖。
最初的設計是在刪除任務的同一時候依照任務 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 所看到的。
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 的配置。
檢索、查詢、刪除 HBase 數據表中的數據自己存在大量的關聯性,需要查看 HBase 數據表的源碼才幹肯定致使檢索性能瓶頸的根本緣由及終於解決方式。
HBase 數據庫的使用及檢索優化方式均與傳統關係型數據庫存在較多不一樣。本文從數據表的基本定義方式出發。經過 HBase 自身提供的 API 訪問方式入手,舉例說明優化方式及注意事項,最後經過實例來驗證優化方案可行性。檢索性能自己是數據表設計、程序設計、邏輯設計等的結合產物,需要程序猿深刻理解後才幹作出正確的優化方案。