HBase 数据库检索性能优化策略
?
HBase 數(shù)據(jù)庫是一個基于分布式的、面向列的、主要用于非結(jié)構(gòu)化數(shù)據(jù)存儲用途的開源數(shù)據(jù)庫。其設計思路來源于 Google 的非開源數(shù)據(jù)庫”BigTable”。
HDFS 為 HBase 提供底層存儲支持,MapReduce 為其提供計算能力,ZooKeeper 為其提供協(xié)調(diào)服務和 failover(失效轉(zhuǎn)移的備份操作)機制。Pig 和 Hive 為 HBase 提供了高層語言支持,使其可以進行數(shù)據(jù)統(tǒng)計(可實現(xiàn)多表 join 等),Sqoop 則為其提供 RDBMS 數(shù)據(jù)導入功能。
HBase 不能支持 where 條件、Order by 查詢,只支持按照主鍵 Rowkey 和主鍵的 range 來查詢,但是可以通過 HBase 提供的 API 進行條件過濾。
HBase 的 Rowkey 是數(shù)據(jù)行的唯一標識,必須通過它進行數(shù)據(jù)行訪問,目前有三種方式,單行鍵訪問、行鍵范圍訪問、全表掃描訪問。數(shù)據(jù)按行鍵的方式排序存儲,依次按位比較,數(shù)值較大的排列在后,例如 int 方式的排序:1,10,100,11,12,2,20…,906,…。
ColumnFamily 是“列族”,屬于 schema 表,在建表時定義,每個列屬于一個列族,列名用列族作為前綴“ColumnFamily:qualifier”,訪問控制、磁盤和內(nèi)存的使用統(tǒng)計都是在列族層面進行的。
Cell 是通過行和列確定的一個存儲單元,值以字節(jié)碼存儲,沒有類型。
Timestamp 是區(qū)分不同版本 Cell 的索引,64 位整型。不同版本的數(shù)據(jù)按照時間戳倒序排列,最新的數(shù)據(jù)版本排在最前面。
Hbase 在行方向上水平劃分成 N 個 Region,每個表一開始只有一個 Region,數(shù)據(jù)量增多,Region 自動分裂為兩個,不同 Region 分布在不同 Server 上,但同一個不會拆分到不同 Server。
Region 按 ColumnFamily 劃分成 Store,Store 為最小存儲單元,用于保存一個列族的數(shù)據(jù),每個 Store 包括內(nèi)存中的 memstore 和持久化到 disk 上的 HFile。
圖 1 是 HBase 數(shù)據(jù)表的示例,數(shù)據(jù)分布在多臺節(jié)點機器上面。
? ? ? ? ? ? ? ? ? ? ? ? ??
圖 1. HBase 數(shù)據(jù)表示例(查看大圖)
?
HBase 調(diào)用 API 示例
類似于操作關(guān)系型數(shù)據(jù)庫的 JDBC 庫,HBase client 包本身提供了大量可以供操作的 API,幫助用戶快速操作 HBase 數(shù)據(jù)庫。提供了諸如創(chuàng)建數(shù)據(jù)表、刪除數(shù)據(jù)表、增加字段、存入數(shù)據(jù)、讀取數(shù)據(jù)等等接口。清單 1 提供了一個作者封裝的工具類,包括操作數(shù)據(jù)表、讀取數(shù)據(jù)、存入數(shù)據(jù)、導出數(shù)據(jù)等方法。
清單 1.HBase API 操作工具類代碼
importorg.apache.hadoop.conf.Configuration; importorg.apache.hadoop.hbase.HColumnDescriptor; importorg.apache.hadoop.hbase.HTableDescriptor; importorg.apache.hadoop.hbase.KeyValue; importorg.apache.hadoop.hbase.client.Get; importorg.apache.hadoop.hbase.client.HBaseAdmin; importorg.apache.hadoop.hbase.client.HTable; importorg.apache.hadoop.hbase.client.Put; importorg.apache.hadoop.hbase.client.Result; importorg.apache.hadoop.hbase.client.ResultScanner; importorg.apache.hadoop.hbase.client.Scan; importorg.apache.hadoop.hbase.util.Bytes;importjava.io.IOException; importjava.util.ArrayList; importjava.util.List;publicclass HBaseUtil { privateConfiguration conf = null; privateHBaseAdmin admin = null;protectedHBaseUtil(Configuration conf) throwsIOException {this.conf = conf;this.admin = newHBaseAdmin(conf); }publicboolean existsTable(String table)throwsIOException {returnadmin.tableExists(table); }publicvoid createTable(String table, byte[][] splitKeys, String... colfams)throwsIOException { HTableDescriptor desc = newHTableDescriptor(table); for(String cf : colfams) { HColumnDescriptor coldef = newHColumnDescriptor(cf); desc.addFamily(coldef);} if(splitKeys != null) { admin.createTable(desc, splitKeys); }else{ admin.createTable(desc);} }publicvoid disableTable(String table) throwsIOException { admin.disableTable(table); }publicvoid dropTable(String table) throwsIOException {if(existsTable(table)) {disableTable(table);admin.deleteTable(table);} }publicvoid fillTable(String table, intstartRow, intendRow, intnumCols,intpad, booleansetTimestamp, booleanrandom,String... colfams) throwsIOException {HTable tbl = newHTable(conf, table);for(introw = startRow; row <= endRow; row++) {for(intcol = 0; col < numCols; col++) {Put put = newPut(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();}publicvoid put(String table, String row, String fam, String qual,String val) throwsIOException {HTable tbl = newHTable(conf, table);Put put = newPut(Bytes.toBytes(row));put.add(Bytes.toBytes(fam), Bytes.toBytes(qual), Bytes.toBytes(val));tbl.put(put);tbl.close();}publicvoid put(String table, String row, String fam, String qual, longts,String val) throwsIOException {HTable tbl = newHTable(conf, table);Put put = newPut(Bytes.toBytes(row));put.add(Bytes.toBytes(fam), Bytes.toBytes(qual), ts, Bytes.toBytes(val));tbl.put(put);tbl.close();}publicvoid put(String table, String[] rows, String[] fams, String[] quals,long[] ts, String[] vals) throwsIOException {HTable tbl = newHTable(conf, table);for(String row : rows) {Put put = newPut(Bytes.toBytes(row));for(String fam : fams) {intv = 0;for(String qual : quals) {String val = vals[v < vals.length ? v : vals.length];longt = 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();}publicvoid dump(String table, String[] rows, String[] fams, String[] quals)throwsIOException {HTable tbl = newHTable(conf, table);List<Get> gets = newArrayList<Get>();for(String row : rows) {Get get = newGet(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()));}}}privatestatic void scan(intcaching, intbatch) throwsIOException {HTable table = null;finalint[] counters = {0,0};Scan scan = newScan();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 的表數(shù)據(jù)分為多個層次,HRegion->HStore->[HFile,HFile,…,MemStore]。
在 HBase 中,一張表可以有多個 Column Family,在一次 Scan 的流程中,每個 Column Family(Store) 的數(shù)據(jù)讀取由一個 StoreScanner 對象負責。每個 Store 的數(shù)據(jù)由一個內(nèi)存中的 MemStore 和磁盤上的 HFile 文件組成,對應的 StoreScanner 對象使用一個 MemStoreScanner 和 N 個 StoreFileScanner 來進行實際的數(shù)據(jù)讀取。
因此,讀取一行的數(shù)據(jù)需要以下步驟:
這兩步都是通過堆來完成。RegionScanner 的讀取通過下面的多個 StoreScanner 組成的堆完成,使用 RegionScanner 的成員變量 KeyValueHeap storeHeap 表示。一個 StoreScanner 一個堆,堆中的元素就是底下包含的 HFile 和 MemStore 對應的 StoreFileScanner 和 MemStoreScanner。堆的優(yōu)勢是建堆效率高,可以動態(tài)分配內(nèi)存大小,不必事先確定生存周期。
接著調(diào)用 seekScanners() 對這些 StoreFileScanner 和 MemStoreScanner 分別進行 seek。seek 是針對 KeyValue 的,seek 的語義是 seek 到指定 KeyValue,如果指定 KeyValue 不存在,則 seek 到指定 KeyValue 的下一個。
Scan類常用方法說明:
- scan.addFamily()/scan.addColumn():指定需要的 Family 或 Column,如果沒有調(diào)用任何 addFamily 或 Column,會返回所有的 Columns;
- scan.setMaxVersions():指定最大的版本個數(shù)。如果不帶任何參數(shù)調(diào)用 setMaxVersions,表示取所有的版本。如果不掉用 setMaxVersions,只會取到最新的版本.;
- scan.setTimeRange():指定最大的時間戳和最小的時間戳,只有在此范圍內(nèi)的 Cell 才能被獲取;
- scan.setTimeStamp():指定時間戳;
- scan.setFilter():指定 Filter 來過濾掉不需要的信息;
- scan.setStartRow():指定開始的行。如果不調(diào)用,則從表頭開始;
- scan.setStopRow():指定結(jié)束的行(不含此行);
- scan. setCaching():每次從服務器端讀取的行數(shù)(影響 RPC);
- scan.setBatch():指定最多返回的 Cell 數(shù)目。用于防止一行中有過多的數(shù)據(jù),導致 OutofMemory 錯誤,默認無限制。
?
HBase 數(shù)據(jù)表優(yōu)化
HBase 是一個高可靠性、高性能、面向列、可伸縮的分布式數(shù)據(jù)庫,但是當并發(fā)量過高或者已有數(shù)據(jù)量很大時,讀寫性能會下降。我們可以采用如下方式逐步提升 HBase 的檢索速度。
預先分區(qū)
默認情況下,在創(chuàng)建 HBase 表的時候會自動創(chuàng)建一個 Region 分區(qū),當導入數(shù)據(jù)的時候,所有的 HBase 客戶端都向這一個 Region 寫數(shù)據(jù),直到這個 Region 足夠大了才進行切分。一種可以加快批量寫入速度的方法是通過預先創(chuàng)建一些空的 Regions,這樣當數(shù)據(jù)寫入 HBase 時,會按照 Region 分區(qū)情況,在集群內(nèi)做數(shù)據(jù)的負載均衡。
Rowkey 優(yōu)化
HBase 中 Rowkey 是按照字典序存儲,因此,設計 Rowkey 時,要充分利用排序特點,將經(jīng)常一起讀取的數(shù)據(jù)存儲到一塊,將最近可能會被訪問的數(shù)據(jù)放在一塊。
此外,Rowkey 若是遞增的生成,建議不要使用正序直接寫入 Rowkey,而是采用 reverse 的方式反轉(zhuǎn) Rowkey,使得 Rowkey 大致均衡分布,這樣設計有個好處是能將 RegionServer 的負載均衡,否則容易產(chǎn)生所有新數(shù)據(jù)都在一個 RegionServer 上堆積的現(xiàn)象,這一點還可以結(jié)合 table 的預切分一起設計。
減少ColumnFamily 數(shù)量
不要在一張表里定義太多的 ColumnFamily。目前 Hbase 并不能很好的處理超過 2~3 個 ColumnFamily 的表。因為某個 ColumnFamily 在 flush 的時候,它鄰近的 ColumnFamily 也會因關(guān)聯(lián)效應被觸發(fā) flush,最終導致系統(tǒng)產(chǎn)生更多的 I/O。
緩存策略 (setCaching)
創(chuàng)建表的時候,可以通過 HColumnDescriptor.setInMemory(true) 將表放到 RegionServer 的緩存中,保證在讀取的時候被 cache 命中。
設置存儲生命期
創(chuàng)建表的時候,可以通過 HColumnDescriptor.setTimeToLive(int timeToLive) 設置表中數(shù)據(jù)的存儲生命期,過期數(shù)據(jù)將自動被刪除。
硬盤配置
每臺 RegionServer 管理 10~1000 個 Regions,每個 Region 在 1~2G,則每臺 Server 最少要 10G,最大要 1000*2G=2TB,考慮 3 備份,則要 6TB。方案一是用 3 塊 2TB 硬盤,二是用 12 塊 500G 硬盤,帶寬足夠時,后者能提供更大的吞吐率,更細粒度的冗余備份,更快速的單盤故障恢復。
分配合適的內(nèi)存給 RegionServer 服務
在不影響其他服務的情況下,越大越好。例如在 HBase 的 conf 目錄下的 hbase-env.sh 的最后添加 export HBASE_REGIONSERVER_OPTS=”-Xmx16000m $HBASE_REGIONSERVER_OPTS”
其中 16000m 為分配給 RegionServer 的內(nèi)存大小。
寫數(shù)據(jù)的備份數(shù)
備份數(shù)與讀性能成正比,與寫性能成反比,且備份數(shù)影響高可用性。有兩種配置方式,一種是將 hdfs-site.xml 拷貝到 hbase 的 conf 目錄下,然后在其中添加或修改配置項 dfs.replication 的值為要設置的備份數(shù),這種修改對所有的 HBase 用戶表都生效,另外一種方式,是改寫 HBase 代碼,讓 HBase 支持針對列族設置備份數(shù),在創(chuàng)建表時,設置列族備份數(shù),默認為 3,此種備份數(shù)只對設置的列族生效。
WAL(預寫日志)
可設置開關(guān),表示 HBase 在寫數(shù)據(jù)前用不用先寫日志,默認是打開,關(guān)掉會提高性能,但是如果系統(tǒng)出現(xiàn)故障 (負責插入的 RegionServer 掛掉),數(shù)據(jù)可能會丟失。配置 WAL 在調(diào)用 Java API 寫入時,設置 Put 實例的 WAL,調(diào)用 Put.setWriteToWAL(boolean)。
批量寫
HBase 的 Put 支持單條插入,也支持批量插入,一般來說批量寫更快,節(jié)省來回的網(wǎng)絡開銷。在客戶端調(diào)用 Java API 時,先將批量的 Put 放入一個 Put 列表,然后調(diào)用 HTable 的 Put(Put 列表) 函數(shù)來批量寫。
客戶端一次從服務器拉取的數(shù)量
通過配置一次拉去的較大的數(shù)據(jù)量可以減少客戶端獲取數(shù)據(jù)的時間,但是它會占用客戶端內(nèi)存。有三個地方可進行配置:
RegionServer 的請求處理 IO 線程數(shù)
較少的 IO 線程適用于處理單次請求內(nèi)存消耗較高的 Big Put 場景 (大容量單次 Put 或設置了較大 cache 的 Scan,均屬于 Big Put) 或 ReigonServer 的內(nèi)存比較緊張的場景。
較多的 IO 線程,適用于單次請求內(nèi)存消耗低,TPS 要求 (每秒事務處理量 (TransactionPerSecond)) 非常高的場景。設置該值的時候,以監(jiān)控內(nèi)存為主要參考。
在 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 速度很快,內(nèi)存占用低。缺點是 split 和 compaction 會很頻繁,特別是數(shù)量較多的小 Region 不停地 split, compaction,會導致集群響應時間波動很大,Region 數(shù)量太多不僅給管理上帶來麻煩,甚至會引發(fā)一些 Hbase 的 bug。一般 512M 以下的都算小 Region。大 Region 則不太適合經(jīng)常 split 和 compaction,因為做一次 compact 和 split 會產(chǎn)生較長時間的停頓,對應用的讀寫性能沖擊非常大。
此外,大 Region 意味著較大的 StoreFile,compaction 時對內(nèi)存也是一個挑戰(zhàn)。如果你的應用場景中,某個時間點的訪問量較低,那么在此時做 compact 和 split,既能順利完成 split 和 compaction,又能保證絕大多數(shù)時間平穩(wěn)的讀寫性能。compaction 是無法避免的,split 可以從自動調(diào)整為手動。只要通過將這個參數(shù)值調(diào)大到某個很難達到的值,比如 100G,就可以間接禁用自動 split(RegionServer 不會對未到達 100G 的 Region 做 split)。再配合 RegionSplitter 這個工具,在需要 split 時,手動 split。手動 split 在靈活性和穩(wěn)定性上比起自動 split 要高很多,而且管理成本增加不多,比較推薦 online 實時系統(tǒng)使用。內(nèi)存方面,小 Region 在設置 memstore 的大小值上比較靈活,大 Region 則過大過小都不行,過大會導致 flush 時 app 的 IO wait 增高,過小則因 StoreFile 過多影響讀性能。
HBase 配置
建議 HBase 的服務器內(nèi)存至少 32G,表 1 是通過實踐檢驗得到的分配給各角色的內(nèi)存建議值。
| HDFS | HDFS NameNode | 16GB |
| HDFS DataNode | 2GB | |
| HBase | HMaster | 2GB |
| HRegionServer | 16GB | |
| ZooKeeper | ZooKeeper | 4GB |
表 1. HBase 相關(guān)服務配置信息
HBase 的單個 Region 大小建議設置大一些,推薦 2G,RegionServer 處理少量的大 Region 比大量的小 Region 更快。對于不重要的數(shù)據(jù),在創(chuàng)建表時將其放在單獨的列族內(nèi),并且設置其列族備份數(shù)為 2(默認是這樣既保證了雙備份,又可以節(jié)約空間,提高寫性能,代價是高可用性比備份數(shù)為 3 的稍差,且讀性能不如默認備份數(shù)的時候。
實際案例
項目要求可以刪除存儲在 HBase 數(shù)據(jù)表中的數(shù)據(jù),數(shù)據(jù)在 HBase 中的 Rowkey 由任務 ID(數(shù)據(jù)由任務產(chǎn)生) 加上 16 位隨機數(shù)組成,任務信息由單獨一張表維護。圖 2 所示是數(shù)據(jù)刪除流程圖。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
圖 2. 數(shù)據(jù)刪除流程圖
最初的設計是在刪除任務的同時按照任務 ID 刪除該任務存儲在 HBase 中的相應數(shù)據(jù)。但是 HBase 數(shù)據(jù)較多時會導致刪除耗時較長,同時由于磁盤 I/O 較高,會導致數(shù)據(jù)讀取、寫入超時。
查看 HBase 日志發(fā)現(xiàn)刪除數(shù)據(jù)時,HBase 在做 Major Compaction 操作。Major Compaction 操作的目的是合并文件,并清除刪除、過期、多余版本的數(shù)據(jù)。Major Compaction 時 HBase 將合并 Region 中 StoreFile,該動作如果持續(xù)長時間會導致整個 Region 都不可讀,最終導致所有基于這些 Region 的查詢超時。
如果想要解決 Major Compaction 問題,需要查看它的源代碼。通過查看 HBase 源碼發(fā)現(xiàn) RegionServer 在啟動時候,有個 CompactionChecker 線程在定期檢測是否需要做 Compact。源代碼如圖 3 所示。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
圖 3. CompactionChecker 線程代碼圖
isMajorCompaction 中會根據(jù) hbase.hregion.majorcompaction 參數(shù)來判斷是否做 Major Compact。如果 hbase.hregion.majorcompaction 為 0,則返回 false。修改配置文件 hbase.hregion.majorcompaction 為 0,禁止 HBase 的定期 Major Compaction 機制,通過自定義的定時機制 (在凌晨 HBase 業(yè)務不繁忙時) 執(zhí)行 Major 操作,這個定時可以是通過 Linux cron 定時啟動腳本,也可以通過 Java 的 timer schedule,在實際項目中使用 Quartz 來啟動,啟動的時間配置在配置文件中給出,可以方便的修改 Major Compact 啟動的時間。通過這種修改后,我們發(fā)現(xiàn)在刪除數(shù)據(jù)后仍會有 Compact 操作。這樣流程進入 needsCompaction = true 的分支。查看 needsCompaction 判斷條件為 (storefiles.size() – filesCompacting.size()) > minFilesToCompact 觸發(fā)。同時當需緊縮的文件數(shù)等于 Store 的所有文件數(shù),Minor Compact 自動升級為 Major Compact。但是 Compact 操作不能禁止,因為這樣會導致數(shù)據(jù)一直存在,最終影響查詢效率。
基于以上分析,我們必須重新考慮刪除數(shù)據(jù)的流程。對用戶來說,用戶只要在檢索時對于刪除的任務不進行檢索即可。那么只需要刪除該條任務記錄,對于該任務相關(guān)聯(lián)的數(shù)據(jù)不需要立馬進行刪除。當系統(tǒng)空閑時候再去定時刪除 HBase 數(shù)據(jù)表中的數(shù)據(jù),并對 Region 做 Major Compact,清理已經(jīng)刪除的數(shù)據(jù)。通過對任務刪除流程的修改,達到項目的需求,同時這種修改也不需要修改 HBase 的配置。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
圖 4. 數(shù)據(jù)刪除流程對比圖(查看大圖)
檢索、查詢、刪除 HBase 數(shù)據(jù)表中的數(shù)據(jù)本身存在大量的關(guān)聯(lián)性,需要查看 HBase 數(shù)據(jù)表的源代碼才能確定導致檢索性能瓶頸的根本原因及最終解決方案。
?
結(jié)束語
HBase 數(shù)據(jù)庫的使用及檢索優(yōu)化方式均與傳統(tǒng)關(guān)系型數(shù)據(jù)庫存在較多不同,本文從數(shù)據(jù)表的基本定義方式出發(fā),通過 HBase 自身提供的 API 訪問方式入手,舉例說明優(yōu)化方式及注意事項,最后通過實例來驗證優(yōu)化方案可行性。檢索性能本身是數(shù)據(jù)表設計、程序設計、邏輯設計等的結(jié)合產(chǎn)物,需要程序員深入理解后才能做出正確的優(yōu)化方案。
掃一掃 關(guān)注微信公眾號!號主 專注于搜索和推薦系統(tǒng),嘗試使用算法去更好的服務于用戶,包括但不局限于機器學習,深度學習,強化學習,自然語言理解,知識圖譜,還不定時分享技術(shù),資料,思考等文章!
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?【技術(shù)服務】,詳情點擊查看:https://mp.weixin.qq.com/s/PtX9ukKRBmazAWARprGIAg?
總結(jié)
以上是生活随笔為你收集整理的HBase 数据库检索性能优化策略的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 95后程序员辞职大厂卖柚子,3个月爆卖3
- 下一篇: mysql求回购率_SQL-- 用户行为