HBase篇--HBase常用優化

LHBlog發表於2018-01-16

一.前述

HBase優化能夠讓我們對調優有一定的理解,當然企業並不是所有的優化全都用,優化還要根據業務具體實施。

二.具體優化

1.表的設計

 1.1 預分割槽

 

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

public static boolean createTable(HBaseAdmin admin, HTableDescriptor table, byte[][] splits)
throws IOException {
  try {
    admin.createTable(table, splits);
    return true;
  } catch (TableExistsException e) {
    logger.info("table " + table.getNameAsString() + " already exists");
    // the table already exists...
    return false;  
  }
}

public static byte[][] getHexSplits(String startKey, String endKey, int numRegions) { //start:001,endkey:100,10region [001,010]
[011,020]
  byte[][] splits = new byte[numRegions-1][];
  BigInteger lowestKey = new BigInteger(startKey, 16);
  BigInteger highestKey = new BigInteger(endKey, 16);
  BigInteger range = highestKey.subtract(lowestKey);
  BigInteger regionIncrement = range.divide(BigInteger.valueOf(numRegions));
  lowestKey = lowestKey.add(regionIncrement);
  for(int i=0; i < numRegions-1;i++) {
    BigInteger key = lowestKey.add(regionIncrement.multiply(BigInteger.valueOf(i)));
    byte[] b = String.format("%016x", key).getBytes();
    splits[i] = b;
  }
  return splits;
}

 

 1.2  Row Key設計

HBaserow key用來檢索表中的記錄,支援以下三種方式:

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

HBase中,row key可以是任意字串,最大長度64KB,實際應用中一般為10~100bytes,存為byte[]位元組陣列,一般設計成定長的

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

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

Rowkey規則

1、 越小越好

2、 Rowkey的設計是要根據實際業務來

3、 雜湊性

a) 取反   001  002  100 200

b) Hash

 

1.3 列族的設計

 

不要在一張表裡定義太多的column family。目前Hbase並不能很好的處理超過2~3column family的表。因為某個column familyflush的時候,它鄰近的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)(相當於Linux中的Crontab任務)

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中全部的StoreFileMemStore,將它們按照row key進行合併,由於StoreFileMemStore都是經過排序的,並且StoreFile帶有記憶體中索引,通常合併過程還是比較快的。

 

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

 

hbase為了防止小檔案(被刷到磁碟的menstore)過多,以保證保證查詢效率,hbase需要在必要的時候將這些小的store file合併成相對較大的store file,這個過程就稱之為compaction。在hbase中,主要存在兩種型別的compactionminor  compactionmajor compaction

 

minor compaction:的是較小、很少檔案的合併。

 

major compaction 的功能是將所有的store file合併成一個,觸發major compaction的可能條件有:major_compact 命令、majorCompact() APIregion server自動執行(相關引數:hbase.hregion.majoucompaction 預設為24 小時、hbase.hregion.majorcompaction.jetter 預設值為0.2 防止region server 在同一時間進行major compaction)。

 

hbase.hregion.majorcompaction.jetter引數的作用是:對引數hbase.hregion.majoucompaction 規定的值起到浮動的作用,假如兩個引數都為預設值240,2,那麼major compact最終使用的數值為:19.2~28.8 這個範圍。

 

1、 關閉自動major compaction

 

2、 手動程式設計major compaction

 

Timer類,contab

 

minor compaction的執行機制要複雜一些,它由一下幾個引數共同決定:

 

hbase.hstore.compaction.min :預設值為 3,表示至少需要三個滿足條件的store file時,minor compaction才會啟動

 

hbase.hstore.compaction.max 預設值為10,表示一次minor compaction中最多選取10store file

 

hbase.hstore.compaction.min.size 表示檔案大小小於該值的store file 一定會加入到minor compactionstore file

 

hbase.hstore.compaction.max.size 表示檔案大小大於該值的store file 一定會被minor compaction排除

 

hbase.hstore.compaction.ratio store file 按照檔案年齡排序(older to younger),minor compaction總是從older store file開始選擇

 2. 寫表操作

  2.1 HTable併發寫

 

建立多個HTable客戶端用於寫操作,提高寫資料的吞吐量,一個例子:

 

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

 

 

 

 

 

 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操作),首先會先寫WALWrite 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和寫bufferwriteBufferSize),可以既保證在資料量小的時候,資料可以在較短時間內被flush(如1秒內),同時又保證在資料量大的時候,寫buffer一滿就及時進行flush。下面給個具體的例子:

 

 

for (int i = 0; i < threadN; i++) {
    Thread th = new Thread() {
        public void run() {
            while (true) {
                try {
                    sleep(1000); //1 second
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
synchronized (wTableLog[i]) {
                    try {
                        wTableLog[i].flushCommits();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
}
    };
    th.setDaemon(true);
    th.start();
}

 

 

3. 讀表操作

3.1 HTable併發讀

建立多個HTable客戶端用於讀操作,提高讀資料的吞吐量,一個例子:

 

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

 

3.2 HTable引數設定

3.2.1 Scanner Caching

hbase.client.scanner.caching配置項可以設定HBase scanner一次從服務端抓取的資料條數,預設情況下一次一條。通過將其設定成一個合理的值,可以減少scan過程中next()的時間開銷,代價是scanner需要通過客戶端的記憶體來維持這些被cache的行記錄。

有三個地方可以進行配置:1)在HBaseconf配置檔案中進行配置;(一般不用次全域性配置!!!)2)通過呼叫HTable.setScannerCaching(int scannerCaching)進行配置;3)通過呼叫Scan.setCaching(int caching)進行配置。三者的優先順序越來越高。

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<Get>)方法可以根據一個指定的row key列表,批量獲取多行記錄,這樣做的好處是批量執行,只需要一次網路I/O開銷,這對於對資料實時性要求高而且網路傳輸RTT高的情景下可能帶來明顯的效能提升。

3.4 多執行緒併發讀

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

 

public class DataReaderServer {
     //獲取店鋪一天內各分鐘PV值的入口函式
     public static ConcurrentHashMap<String, String> getUnitMinutePV(long uid, long startStamp, long endStamp){
         long min = startStamp;
         int count = (int)((endStamp - startStamp) / (60*1000));
         List<String> lst = new ArrayList<String>();
         for (int i = 0; i <= count; i++) {
            min = startStamp + i * 60 * 1000;
            lst.add(uid + "_" + min);
         }
         return parallelBatchMinutePV(lst);
     }
      //多執行緒併發查詢,獲取分鐘PV值
private static ConcurrentHashMap<String, String> parallelBatchMinutePV(List<String> lstKeys){
        ConcurrentHashMap<String, String> hashRet = new ConcurrentHashMap<String, String>();
        int parallel = 3;
        List<List<String>> lstBatchKeys  = null;
        if (lstKeys.size() < parallel ){
            lstBatchKeys  = new ArrayList<List<String>>(1);
            lstBatchKeys.add(lstKeys);
        }
        else{
            lstBatchKeys  = new ArrayList<List<String>>(parallel);
            for(int i = 0; i < parallel; i++  ){
                List<String> lst = new ArrayList<String>();
                lstBatchKeys.add(lst);
            }

            for(int i = 0 ; i < lstKeys.size() ; i ++ ){
                lstBatchKeys.get(i%parallel).add(lstKeys.get(i));
            }
        }
        
        List<Future< ConcurrentHashMap<String, String> >> futures = new ArrayList<Future< ConcurrentHashMap<String, String> >>(5);
        
        ThreadFactoryBuilder builder = new ThreadFactoryBuilder();
        builder.setNameFormat("ParallelBatchQuery");
        ThreadFactory factory = builder.build();
        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(lstBatchKeys.size(), factory);
        
        for(List<String> keys : lstBatchKeys){
            Callable< ConcurrentHashMap<String, String> > callable = new BatchMinutePVCallable(keys);
            FutureTask< ConcurrentHashMap<String, String> > future = (FutureTask< ConcurrentHashMap<String, String> >) executor.submit(callable);
            futures.add(future);
        }
        executor.shutdown();
        
        // Wait for all the tasks to finish
        try {
          boolean stillRunning = !executor.awaitTermination(
              5000000, TimeUnit.MILLISECONDS);
          if (stillRunning) {
            try {
                executor.shutdownNow();
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
          }
        } catch (InterruptedException e) {
          try {
              Thread.currentThread().interrupt();
          } catch (Exception e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
          }
        }
        
        // Look for any exception
        for (Future f : futures) {
          try {
              if(f.get() != null)
              {
                  hashRet.putAll((ConcurrentHashMap<String, String>)f.get());
              }
          } catch (InterruptedException e) {
            try {
                 Thread.currentThread().interrupt();
            } catch (Exception e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }
          } catch (ExecutionException e) {
            e.printStackTrace();
          }
        }
        
        return hashRet;
    }
     //一個執行緒批量查詢,獲取分鐘PV值
    protected static ConcurrentHashMap<String, String> getBatchMinutePV(List<String> lstKeys){
        ConcurrentHashMap<String, String> hashRet = null;
        List<Get> lstGet = new ArrayList<Get>();
        String[] splitValue = null;
        for (String s : lstKeys) {
            splitValue = s.split("_");
            long uid = Long.parseLong(splitValue[0]);
            long min = Long.parseLong(splitValue[1]);
            byte[] key = new byte[16];
            Bytes.putLong(key, 0, uid);
            Bytes.putLong(key, 8, min);
            Get g = new Get(key);
            g.addFamily(fp);
            lstGet.add(g);
        }
        Result[] res = null;
        try {
            res = tableMinutePV[rand.nextInt(tableN)].get(lstGet);
        } catch (IOException e1) {
            logger.error("tableMinutePV exception, e=" + e1.getStackTrace());
        }

        if (res != null && res.length > 0) {
            hashRet = new ConcurrentHashMap<String, String>(res.length);
            for (Result re : res) {
                if (re != null && !re.isEmpty()) {
                    try {
                        byte[] key = re.getRow();
                        byte[] value = re.getValue(fp, cp);
                        if (key != null && value != null) {
                            hashRet.put(String.valueOf(Bytes.toLong(key,
                                    Bytes.SIZEOF_LONG)), String.valueOf(Bytes
                                    .toLong(value)));
                        }
                    } catch (Exception e2) {
                        logger.error(e2.getStackTrace());
                    }
                }
            }
        }

        return hashRet;
    }
}
//呼叫介面類,實現Callable介面
class BatchMinutePVCallable implements Callable<ConcurrentHashMap<String, String>>{
     private List<String> keys;

     public BatchMinutePVCallable(List<String> lstKeys ) {
         this.keys = lstKeys;
     }

     public ConcurrentHashMap<String, String> call() throws Exception {
         return DataReadServer.getBatchMinutePV(keys);
     }

 

 

3.5 快取查詢結果

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

3.6 Blockcache !!!常用,設定讀快取,在伺服器端

HBaseRegionserver的記憶體分為兩個部分,一部分作為Memstore,主要用來寫;另外一部分作為BlockCache,主要用於讀。

寫請求會先寫入MemstoreRegionserver會給每個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上有一個BlockCacheNMemstore它們的大小之和不能大於等於heapsize * 0.8,否則HBase不能啟動。預設BlockCache0.2,而Memstore0.4對於注重讀響應時間的系統,可以將 BlockCache設大些,比如設定BlockCache=0.4Memstore=0.39,以加大快取的命中率

 

HTableHTablePool使用注意事項

HTableHTablePool都是HBase客戶端API的一部分可以使用它們對HBase表進行CRUD操作。下面結合在專案中的應用情況,對二者使用過程中的注意事項做一下概括總結。

Configuration conf = HBaseConfiguration.create();

try (Connection connection = ConnectionFactory.createConnection(conf)) {

  try (Table table = connection.getTable(TableName.valueOf(tablename)) {

    // use table as needed, the table returned is lightweight

  }

}

 

HTable

HTableHBase客戶端與HBase服務端通訊的Java API物件,客戶端可以通過HTable物件與服務端進行CRUD操作(增刪改查)。它的建立很簡單:

Configuration conf = HBaseConfiguration.create();

HTable table = new HTable(conf, "tablename");

//TODO CRUD Operation……

HTable使用時的一些注意事項:

1.   規避HTable物件的建立開銷

因為客戶端建立HTable物件後,需要進行一系列的操作:檢查.META.表確認指定名稱的HBase表是否存在,表是否有效等等,整個時間開銷比較重,可能會耗時幾秒鐘之長,因此最好在程式啟動時一次性建立完成需要的HTable物件,如果使用Java API,一般來說是在建構函式中進行建立,程式啟動後直接重用。

2.   HTable物件不是執行緒安全的

HTable物件對於客戶端讀寫資料來說不是執行緒安全的,因此多執行緒時,要為每個執行緒單獨建立複用一個HTable物件,不同物件間不要共享HTable物件使用,特別是在客戶端auto flash被置為false時,由於存在本地write buffer,可能導致資料不一致。

3.   HTable物件之間共享Configuration

HTable物件共享Configuration物件,這樣的好處在於:

  • 共享ZooKeeper的連線:每個客戶端需要與ZooKeeper建立連線,查詢使用者的table regions位置,這些資訊可以在連線建立後快取起來共享使用;
  • 共享公共的資源:客戶端需要通過ZooKeeper查詢-ROOT-.META.表,這個需要網路傳輸開銷,客戶端快取這些公共資源後能夠減少後續的網路傳輸開銷,加快查詢過程速度。

因此,與以下這種方式相比:

HTable table1 = new HTable("table1");

HTable table2 = new HTable("table2");

下面的方式更有效些:

Configuration conf = HBaseConfiguration.create();//共用一個配置

HTable table1 = new HTable(conf, "table1");

HTable table2 = new HTable(conf, "table2");

備註:即使是高負載的多執行緒程式,也並沒有發現因為共享Configuration而導致的效能問題;如果你的實際情況中不是如此,那麼可以嘗試不共享Configuration

HTablePool

HTablePool可以解決HTable存在的執行緒不安全問題,同時通過維護固定數量的HTable物件,能夠在程式執行期間複用這些HTable資源物件。

Configuration conf = HBaseConfiguration.create();

HTablePool pool = new HTablePool(conf, 10);

1.   HTablePool可以自動建立HTable物件,而且對客戶端來說使用上是完全透明的,可以避免多執行緒間資料併發修改問題。

2.   HTablePool中的HTable物件之間是公用Configuration連線的,能夠可以減少網路開銷。

HTablePool的使用很簡單:每次進行操作前,通HTablePoolgetTable方法取得一個HTable物件,然後進行put/get/scan/delete等操作,最後通過HTablePoolputTable方法將HTable物件放回到HTablePool中。

下面是個使用HTablePool的簡單例子:

 

public void createUser(String username, String firstName, String lastName, String email, String password, String roles) throws IOException {

  HTable table = rm.getTable(UserTable.NAME);

  Put put = new Put(Bytes.toBytes(username));

  put.add(UserTable.DATA_FAMILY, UserTable.FIRSTNAME,

  Bytes.toBytes(firstName));

  put.add(UserTable.DATA_FAMILY, UserTable.LASTNAME,

    Bytes.toBytes(lastName));

  put.add(UserTable.DATA_FAMILY, UserTable.EMAIL, Bytes.toBytes(email));

  put.add(UserTable.DATA_FAMILY, UserTable.CREDENTIALS,

    Bytes.toBytes(password));

  put.add(UserTable.DATA_FAMILY, UserTable.ROLES, Bytes.toBytes(roles));

  table.put(put);

  table.flushCommits();

  rm.putTable(table);

}

 

 

 

 補充:

HbaseDBMS比較:

查詢資料不靈活:

1、 不能使用column之間過濾查詢

2、 不支援全文索引。使用solrhbase整合完成全文搜尋。

a) 使用MR批量讀取hbase中的資料,solr裡面建立索引(no  store)之儲存rowkey的值。

b) 根據關鍵詞從索引中搜尋到rowkey(分頁)

c) 根據rowkeyhbase查詢所有資料

相關文章