HBase性能優化方法總結

本文主要是從Hbase應用程序設計與開發的角度,總結幾種常用的性能優化方法。有關hbase系統配置級別的優化,這裏涉及的不多,這部分可以參考:淘寶Ken Wu同學的博客

1. 表的設計

1.1 Pre-Creating Regions

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

有關預分區,詳情參見:Table Creation: Pre-Creating Regions,如下是官網文檔中關於hbase預先創建一些空的regions導入的方式

100.2. Table Creation: Pre-Creating Regions

Tables in HBase are initially created with one region by default.For bulk imports, this means that all clients will write to the same region until it is large enough to split and become distributed across the cluster.A useful pattern to speed up the bulk import process is to pre-create empty regions.Be somewhat conservative in this, because too-many regions can actually degrade performance.

There are two different approaches to pre-creating splits using the HBase API.The first approach is to rely on the defaultAdmin strategy (which is implemented in Bytes.split)…​

byte[] startKey = ...;      // your lowest key
byte[] endKey = ...;        // your highest key
int numberOfRegions = ...;  // # of regions to create
admin.createTable(table, startKey, endKey, numberOfRegions);

And the other approach, using the HBase API, is to define the splits yourself…​

byte[][] splits = ...;   // create your own splits
admin.createTable(table, splits);

You can achieve a similar effect using the HBase Shell to create tables by specifying split options.

# create table with specific split points
hbase>create 't1','f1',SPLITS => ['\x10\x00', '\x20\x00', '\x30\x00', '\x40\x00']

# create table with four regions based on random bytes keys
hbase>create 't2','f1', { NUMREGIONS => 4 , SPLITALGO => 'UniformSplit' }

# create table with five regions based on hex keys
create 't3','f1', { NUMREGIONS => 5, SPLITALGO => 'HexStringSplit' }

See Relationship Between RowKeys and Region Splits for issues related to understanding your keyspace and pre-creating regions.Seemanual region splitting decisions for discussion on manually pre-splitting regions.SeePre-splitting tables with the HBase Shell for more details of using the HBase Shell to pre-split tables.

下面是一個例子:

  1. public static boolean createTable(HBaseAdmin admin, HTableDescriptor table, byte[][] splits)  
  2. throws IOException {  
  3.   try {  
  4.     admin.createTable(table, splits);  
  5.     return true;  
  6.   } catch (TableExistsException e) {  
  7.     logger.info("table " + table.getNameAsString() + " already exists");  
  8.     // the table already exists...  
  9.     return false;  
  10.   }  
  11. }  
  12.    
  13. public static byte[][] getHexSplits(String startKey, String endKey, int numRegions) {  
  14.   byte[][] splits = new byte[numRegions-1][];  
  15.   BigInteger lowestKey = new BigInteger(startKey, 16);  
  16.   BigInteger highestKey = new BigInteger(endKey, 16);  
  17.   BigInteger range = highestKey.subtract(lowestKey);  
  18.   BigInteger regionIncrement = range.divide(BigInteger.valueOf(numRegions));  
  19.   lowestKey = lowestKey.add(regionIncrement);  
  20.   for(int i=0; i < numRegions-1;i++) {  
  21.     BigInteger key = lowestKey.add(regionIncrement.multiply(BigInteger.valueOf(i)));  
  22.     byte[] b = String.format("%016x", key).getBytes();  
  23.     splits[i] = b;  
  24.   }  
  25.   return splits;  
  26. }  

1.2 Row Key

HBase中row key用來檢索表中的記錄,支持以下三種方式:

  • 通過單個row key訪問:即按照某個row key鍵值進行get操作;
  • 通過row key的range進行scan:即通過設置startRowKey和endRowKey,在這個範圍內進行掃描;
  • 全表掃描:即直接掃描整張表中所有行記錄。

在HBase中,row key可以是任意字符串,最大長度64KB,實際應用中一般爲10~100bytes,存爲byte[]字節數組,一般設計成定長的

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

舉個例子:如果最近寫入HBase表中的數據是最可能被訪問的,可以考慮將時間戳作爲row key的一部分,由於是字典序排序,所以可以使用Long.MAX_VALUE – timestamp作爲row key,這樣能保證新寫入的數據在讀取時可以被快速命中。

1.3 Column Family

不要在一張表裏定義太多的column family。目前Hbase並不能很好的處理超過2~3個column family的表。因爲某個column family在flush的時候,它鄰近的column family也會因關聯效應被觸發flush,最終導致系統產生更多的I/O。感興趣的同學可以對自己的HBase集羣進行實際測試,從得到的測試結果數據驗證一下。

1.4 In Memory

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

1.5 Max Version

創建表的時候,可以通過HColumnDescriptor.setMaxVersions(int maxVersions)設置表中數據的最大版本,如果只需要保存最新版本的數據,那麼可以設置setMaxVersions(1)。

1.6 Time To Live

創建表的時候,可以通過HColumnDescriptor.setTimeToLive(int timeToLive)設置表中數據的存儲生命期,過期數據將自動被刪除,例如如果只需要存儲最近兩天的數據,那麼可以設置setTimeToLive(2 * 24 * 60 * 60)。

1.7 Compact & Split

在HBase中,數據在更新時首先寫入WAL 日誌(HLog)和內存(MemStore)中,MemStore中的數據是排序的,當MemStore累計到一定閾值時,就會創建一個新的MemStore,並且將老的MemStore添加到flush隊列,由單獨的線程flush到磁盤上,成爲一個StoreFile。於此同時, 系統會在zookeeper中記錄一個redo point,表示這個時刻之前的變更已經持久化了(minor compact)

StoreFile是隻讀的,一旦創建後就不可以再修改。因此Hbase的更新其實是不斷追加的操作。當一個Store中的StoreFile達到一定的閾值後,就會進行一次合併(major compact),將對同一個key的修改合併到一起,形成一個大的StoreFile,當StoreFile的大小達到一定閾值後,又會對 StoreFile進行分割(split),等分爲兩個StoreFile。

由於對錶的更新是不斷追加的,處理讀請求時,需要訪問Store中全部的StoreFile和MemStore,將它們按照row key進行合併,由於StoreFile和MemStore都是經過排序的,並且StoreFile帶有內存中索引,通常合併過程還是比較快的。

實際應用中,可以考慮必要時手動進行major compact,將同一個row key的修改進行合併形成一個大的StoreFile。同時,可以將StoreFile設置大些,減少split的發生。

2. 寫表操作

2.1 多HTable併發寫

創建多個HTable客戶端用於寫操作,提高寫數據的吞吐量,一個例子:

  1. static final Configuration conf = HBaseConfiguration.create();  
  2. static final String table_log_name = “user_log”;  
  3. wTableLog = new HTable[tableN];  
  4. for (int i = 0; i < tableN; i++) {  
  5.     wTableLog[i] = new HTable(conf, table_log_name);  
  6.     wTableLog[i].setWriteBufferSize(5 * 1024 * 1024); //5MB  
  7.     wTableLog[i].setAutoFlush(false);  
  8. }  

2.2 HTable參數設置

2.2.1 Auto Flush

通過調用HTable.setAutoFlush(false)方法可以將HTable寫客戶端的自動flush關閉,這樣可以批量寫入數據到HBase,而不是有一條put就執行一次更新,只有當put填滿客戶端寫緩存時,才實際向HBase服務端發起寫請求。默認情況下auto flush是開啓的。

2.2.2 Write Buffer

通過調用HTable.setWriteBufferSize(writeBufferSize)方法可以設置HTable客戶端的寫buffer大小,如果新設置的buffer小於當前寫buffer中的數據時,buffer將會被flush到服務端。其中,writeBufferSize的單位是byte字節數,可以根據實際寫入數據量的多少來設置該值。

2.2.3 WAL Flag

在HBae中,客戶端向集羣中的RegionServer提交數據時(Put/Delete操作),首先會先寫WAL(Write Ahead Log)日誌(即HLog,一個RegionServer上的所有Region共享一個HLog),只有當WAL日誌寫成功後,再接着寫MemStore,然後客戶端被通知提交數據成功;如果寫WAL日誌失敗,客戶端則被通知提交失敗。這樣做的好處是可以做到RegionServer宕機後的數據恢復。

因此,對於相對不太重要的數據,可以在Put/Delete操作時,通過調用Put.setWriteToWAL(false)或Delete.setWriteToWAL(false)函數,放棄寫WAL日誌,從而提高數據寫入的性能。

值得注意的是:謹慎選擇關閉WAL日誌,因爲這樣的話,一旦RegionServer宕機,Put/Delete的數據將會無法根據WAL日誌進行恢復。

2.3 批量寫

通過調用HTable.put(Put)方法可以將一個指定的row key記錄寫入HBase,同樣HBase提供了另一個方法:通過調用HTable.put(List<Put>)方法可以將指定的row key列表,批量寫入多行記錄,這樣做的好處是批量執行,只需要一次網絡I/O開銷,這對於對數據實時性要求高,網絡傳輸RTT高的情景下可能帶來明顯的性能提升。

2.4 多線程併發寫

在客戶端開啓多個HTable寫線程,每個寫線程負責一個HTable對象的flush操作,這樣結合定時flush和寫buffer(writeBufferSize),可以既保證在數據量小的時候,數據可以在較短時間內被flush(如1秒內),同時又保證在數據量大的時候,寫buffer一滿就及時進行flush。下面給個具體的例子:

  1. for (int i = 0; i < threadN; i++) {  
  2.     Thread th = new Thread() {  
  3.         public void run() {  
  4.             while (true) {  
  5.                 try {  
  6.                     sleep(1000); //1 second  
  7.                 } catch (InterruptedException e) {  
  8.                     e.printStackTrace();  
  9.                 }  
  10.                                 synchronized (wTableLog[i]) {  
  11.                     try {  
  12.                         wTableLog[i].flushCommits();  
  13.                     } catch (IOException e) {  
  14.                         e.printStackTrace();  
  15.                     }  
  16.                 }  
  17.             }  
  18.                 }  
  19.     };  
  20.     th.setDaemon(true);  
  21.     th.start();  
  22. }  

3. 讀表操作

3.1 多HTable併發讀

創建多個HTable客戶端用於讀操作,提高讀數據的吞吐量,一個例子:

  1. static final Configuration conf = HBaseConfiguration.create();  
  2. static final String table_log_name = “user_log”;  
  3. rTableLog = new HTable[tableN];  
  4. for (int i = 0; i < tableN; i++) {  
  5.     rTableLog[i] = new HTable(conf, table_log_name);  
  6.     rTableLog[i].setScannerCaching(50);  
  7. }  

3.2 HTable參數設置

3.2.1 Scanner Caching

通過調用HTable.setScannerCaching(int scannerCaching)可以設置HBase scanner一次從服務端抓取的數據條數,默認情況下一次一條。通過將此值設置成一個合理的值,可以減少scan過程中next()的時間開銷,代價是scanner需要通過客戶端的內存來維持這些被cache的行記錄。

3.2.2 Scan Attribute Selection

scan時指定需要的Column Family,可以減少網絡傳輸數據量,否則默認scan操作會返回整行所有Column Family的數據。

3.2.3 Close ResultScanner

通過scan取完數據後,記得要關閉ResultScanner,否則RegionServer可能會出現問題(對應的Server資源無法釋放)。

3.3 批量讀

通過調用HTable.get(Get)方法可以根據一個指定的row key獲取一行記錄,同樣HBase提供了另一個方法:通過調用HTable.get(List)方法可以根據一個指定的row key列表,批量獲取多行記錄,這樣做的好處是批量執行,只需要一次網絡I/O開銷,這對於對數據實時性要求高而且網絡傳輸RTT高的情景下可能帶來明顯的性能提升。

3.4 多線程併發讀

在客戶端開啓多個HTable讀線程,每個讀線程負責通過HTable對象進行get操作。下面是一個多線程併發讀取HBase,獲取店鋪一天內各分鐘PV值的例子:

  1. public class DataReaderServer {  
  2.      //獲取店鋪一天內各分鐘PV值的入口函數  
  3.      public static ConcurrentHashMap getUnitMinutePV(long uid, long startStamp, long endStamp){  
  4.          long min = startStamp;  
  5.          int count = (int)((endStamp - startStamp) / (60*1000));  
  6.          List lst = new ArrayList();  
  7.          for (int i = 0; i <= count; i++) {  
  8.             min = startStamp + i * 60 * 1000;  
  9.             lst.add(uid + "_" + min);  
  10.          }  
  11.          return parallelBatchMinutePV(lst);  
  12.      }  
  13.       //多線程併發查詢,獲取分鐘PV值  
  14. private static ConcurrentHashMap parallelBatchMinutePV(List lstKeys){  
  15.         ConcurrentHashMap hashRet = new ConcurrentHashMap();  
  16.         int parallel = 3;  
  17.         List<List<String>> lstBatchKeys  = null;  
  18.         if (lstKeys.size() < parallel ){  
  19.             lstBatchKeys  = new ArrayList<List<String>>(1);  
  20.             lstBatchKeys.add(lstKeys);  
  21.         }  
  22.         else{  
  23.             lstBatchKeys  = new ArrayList<List<String>>(parallel);  
  24.             for(int i = 0; i < parallel; i++  ){  
  25.                 List lst = new ArrayList();  
  26.                 lstBatchKeys.add(lst);  
  27.             }  
  28.    
  29.             for(int i = 0 ; i < lstKeys.size() ; i ++ ){  
  30.                 lstBatchKeys.get(i%parallel).add(lstKeys.get(i));  
  31.             }  
  32.         }  
  33.    
  34.         List >> futures = new ArrayList >>(5);  
  35.    
  36.         ThreadFactoryBuilder builder = new ThreadFactoryBuilder();  
  37.         builder.setNameFormat("ParallelBatchQuery");  
  38.         ThreadFactory factory = builder.build();  
  39.         ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(lstBatchKeys.size(), factory);  
  40.    
  41.         for(List keys : lstBatchKeys){  
  42.             Callable< ConcurrentHashMap > callable = new BatchMinutePVCallable(keys);  
  43.             FutureTask< ConcurrentHashMap > future = (FutureTask< ConcurrentHashMap >) executor.submit(callable);  
  44.             futures.add(future);  
  45.         }  
  46.         executor.shutdown();  
  47.    
  48.         // Wait for all the tasks to finish  
  49.         try {  
  50.           boolean stillRunning = !executor.awaitTermination(  
  51.               5000000, TimeUnit.MILLISECONDS);  
  52.           if (stillRunning) {  
  53.             try {  
  54.                 executor.shutdownNow();  
  55.             } catch (Exception e) {  
  56.                 // TODO Auto-generated catch block  
  57.                 e.printStackTrace();  
  58.             }  
  59.           }  
  60.         } catch (InterruptedException e) {  
  61.           try {  
  62.               Thread.currentThread().interrupt();  
  63.           } catch (Exception e1) {  
  64.             // TODO Auto-generated catch block  
  65.             e1.printStackTrace();  
  66.           }  
  67.         }  
  68.    
  69.         // Look for any exception  
  70.         for (Future f : futures) {  
  71.           try {  
  72.               if(f.get() != null)  
  73.               {  
  74.                   hashRet.putAll((ConcurrentHashMap)f.get());  
  75.               }  
  76.           } catch (InterruptedException e) {  
  77.             try {  
  78.                  Thread.currentThread().interrupt();  
  79.             } catch (Exception e1) {  
  80.                 // TODO Auto-generated catch block  
  81.                 e1.printStackTrace();  
  82.             }  
  83.           } catch (ExecutionException e) {  
  84.             e.printStackTrace();  
  85.           }  
  86.         }  
  87.    
  88.         return hashRet;  
  89.     }  
  90.      //一個線程批量查詢,獲取分鐘PV值  
  91.     protected static ConcurrentHashMap getBatchMinutePV(List lstKeys){  
  92.         ConcurrentHashMap hashRet = null;  
  93.         List lstGet = new ArrayList();  
  94.         String[] splitValue = null;  
  95.         for (String s : lstKeys) {  
  96.             splitValue = s.split("_");  
  97.             long uid = Long.parseLong(splitValue[0]);  
  98.             long min = Long.parseLong(splitValue[1]);  
  99.             byte[] key = new byte[16];  
  100.             Bytes.putLong(key, 0, uid);  
  101.             Bytes.putLong(key, 8, min);  
  102.             Get g = new Get(key);  
  103.             g.addFamily(fp);  
  104.             lstGet.add(g);  
  105.         }  
  106.         Result[] res = null;  
  107.         try {  
  108.             res = tableMinutePV[rand.nextInt(tableN)].get(lstGet);  
  109.         } catch (IOException e1) {  
  110.             logger.error("tableMinutePV exception, e=" + e1.getStackTrace());  
  111.         }  
  112.    
  113.         if (res != null && res.length > 0) {  
  114.             hashRet = new ConcurrentHashMap(res.length);  
  115.             for (Result re : res) {  
  116.                 if (re != null && !re.isEmpty()) {  
  117.                     try {  
  118.                         byte[] key = re.getRow();  
  119.                         byte[] value = re.getValue(fp, cp);  
  120.                         if (key != null && value != null) {  
  121.                             hashRet.put(String.valueOf(Bytes.toLong(key,  
  122.                                     Bytes.SIZEOF_LONG)), String.valueOf(Bytes  
  123.                                     .toLong(value)));  
  124.                         }  
  125.                     } catch (Exception e2) {  
  126.                         logger.error(e2.getStackTrace());  
  127.                     }  
  128.                 }  
  129.             }  
  130.         }  
  131.    
  132.         return hashRet;  
  133.     }  
  134. }  
  135. //調用接口類,實現Callable接口  
  136. class BatchMinutePVCallable implements Callable>{  
  137.      private List keys;  
  138.    
  139.      public BatchMinutePVCallable(List lstKeys ) {  
  140.          this.keys = lstKeys;  
  141.      }  
  142.    
  143.      public ConcurrentHashMap call() throws Exception {  
  144.          return DataReadServer.getBatchMinutePV(keys);  
  145.      }  
  146. }  

3.5 緩存查詢結果

對於頻繁查詢HBase的應用場景,可以考慮在應用程序中做緩存,當有新的查詢請求時,首先在緩存中查找,如果存在則直接返回,不再查詢HBase;否則對HBase發起讀請求查詢,然後在應用程序中將查詢結果緩存起來。至於緩存的替換策略,可以考慮LRU等常用的策略。

3.6 Blockcache

HBase上Regionserver的內存分爲兩個部分,一部分作爲Memstore,主要用來寫;另外一部分作爲BlockCache,主要用於讀。

寫請求會先寫入Memstore,Regionserver會給每個region提供一個Memstore,當Memstore滿64MB以後,會啓動 flush刷新到磁盤。當Memstore的總大小超過限制時(heapsize * hbase.regionserver.global.memstore.upperLimit * 0.9),會強行啓動flush進程,從最大的Memstore開始flush直到低於限制。

讀請求先到Memstore中查數據,查不到就到BlockCache中查,再查不到就會到磁盤上讀,並把讀的結果放入BlockCache。由於BlockCache採用的是LRU策略,因此BlockCache達到上限(heapsize * hfile.block.cache.size * 0.85)後,會啓動淘汰機制,淘汰掉最老的一批數據。

一個Regionserver上有一個BlockCache和N個Memstore,它們的大小之和不能大於等於heapsize * 0.8,否則HBase不能啓動。默認BlockCache爲0.2,而Memstore爲0.4。對於注重讀響應時間的系統,可以將 BlockCache設大些,比如設置BlockCache=0.4,Memstore=0.39,以加大緩存的命中率。

有關BlockCache機制,請參考這裏:HBase的Block cache,HBase的blockcache機制,hbase中的緩存的計算與使用。

4.數據計算

4.1 服務端計算

Coprocessor運行於HBase RegionServer服務端,各個Regions保持對與其相關的coprocessor實現類的引用,coprocessor類可以通過RegionServer上classpath中的本地jar或HDFS的classloader進行加載。

目前,已提供有幾種coprocessor:

Coprocessor:提供對於region管理的鉤子,例如region的open/close/split/flush/compact等;
RegionObserver:提供用於從客戶端監控表相關操作的鉤子,例如表的get/put/scan/delete等;
Endpoint:提供可以在region上執行任意函數的命令觸發器。一個使用例子是RegionServer端的列聚合,這裏有代碼示例。
以上只是有關coprocessor的一些基本介紹,本人沒有對其實際使用的經驗,對它的可用性和性能數據不得而知。感興趣的同學可以嘗試一下,歡迎討論。

4.2 寫端計算

4.2.1 計數

HBase本身可以看作是一個可以水平擴展的Key-Value存儲系統,但是其本身的計算能力有限(Coprocessor可以提供一定的服務端計算),因此,使用HBase時,往往需要從寫端或者讀端進行計算,然後將最終的計算結果返回給調用者。舉兩個簡單的例子:

PV計算:通過在HBase寫端內存中,累加計數,維護PV值的更新,同時爲了做到持久化,定期(如1秒)將PV計算結果同步到HBase中,這樣查詢端最多會有1秒鐘的延遲,能看到秒級延遲的PV結果。
分鐘PV計算:與上面提到的PV計算方法相結合,每分鐘將當前的累計PV值,按照rowkey + minute作爲新的rowkey寫入HBase中,然後在查詢端通過scan得到當天各個分鐘以前的累計PV值,然後順次將前後兩分鐘的累計PV值相減,就得到了當前一分鐘內的PV值,從而最終也就得到當天各個分鐘內的PV值。

4.2.2 去重

對於UV的計算,就是個去重計算的例子。分兩種情況:

如果內存可以容納,那麼可以在Hash表中維護所有已經存在的UV標識,每當新來一個標識時,通過快速查找Hash確定是否是一個新的UV,若是則UV值加1,否則UV值不變。另外,爲了做到持久化或提供給查詢接口使用,可以定期(如1秒)將UV計算結果同步到HBase中。
如果內存不能容納,可以考慮採用Bloom Filter來實現,從而儘可能的減少內存的佔用情況。除了UV的計算外,判斷URL是否存在也是個典型的應用場景。

4.3 讀端計算

如果對於響應時間要求比較苛刻的情況(如單次http請求要在毫秒級時間內返回),個人覺得讀端不宜做過多複雜的計算邏輯,儘量做到讀端功能單一化:即從HBase RegionServer讀到數據(scan或get方式)後,按照數據格式進行簡單的拼接,直接返回給前端使用。當然,如果對於響應時間要求一般,或者業務特點需要,也可以在讀端進行一些計算邏輯。

5.總結

作爲一個Key-Value存儲系統,HBase並不是萬能的,它有自己獨特的地方。因此,基於它來做應用時,我們往往需要從多方面進行優化改進(表設計、讀表操作、寫表操作、數據計算等),有時甚至還需要從系統級對HBase進行配置調優,更甚至可以對HBase本身進行優化。這屬於不同的層次範疇。

總之,概括來講,對系統進行優化時,首先定位到影響你的程序運行性能的瓶頸之處,然後有的放矢進行鍼對行的優化。如果優化後滿足你的期望,那麼就可以停止優化;否則繼續尋找新的瓶頸之處,開始新的優化,直到滿足性能要求。

轉載 http://blog.linezing.com/2012/03/hbase-performance-optimization
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章