HBase原理--客戶端

HBase提供了面向Java、C/C++、Python等多種語言的客戶端。因爲HBase自己是Java開發的,因此非Java語言的客戶端須要先訪問ThriftServer,而後經過ThriftServer的Java HBase客戶端來請求HBase集羣。固然,有部分第三方團隊實現了其餘一些HBase客戶端,例如OpenTSDB團隊使用的asynchbase和gohbase等,但因爲社區客戶端和服務端協議在大版本之間可能產生較大不兼容,而第三方開發的客戶端通常會落後於社區,所以這裏不推薦使用第三方客戶端,建議統一使用HBase社區的客戶端。對其餘語言的客戶端,推薦使用ThriftServer的方式來訪問HBase服務。數據庫

另外,HBase也支持Shell交互式客戶端。Shell客戶端實質是用JRuby(用Java編寫的Ruby解釋器,方便Ruby腳本跑在JVM虛擬機上)腳本調用官方HBase客戶端來實現的。所以,各類客戶端的核心實現都在社區Java版本客戶端上。本節主要探討HBase社區Java客戶端。緩存

下面咱們經過一個訪問HBase集羣的典型示例代碼,闡述HBase客戶端的用法和設計,代碼以下所示:async

public class TestDemo {
    private static final HBaseTestingUtility TEST_UTIL=new HBaseTestingUtility() ;
    public static final TableName tableName=TableName.valueOf("t
            estTable");
    public static final byte[] ROW_KEYO=Bytes.toBytes("rowkey
            0");
    public static final byte[] ROW_KEY1=Bytes.toBytes("rowkey
            1");
    public static final byte[]FAMILY=Bytes.toBytes("family");
    public static final byte[]QUALIFIER=Bytes.toBytes("qualifie
            r");
    public static final byte[] VALUE-Bytes.toBytes("value");
    @BeforeClass
    public static void setUpBeforeClass( throws Exception {
        TEST_UTIL.startMiniCluster();
    }

    @AfterClass
    public static void tearDownAfterClass( throws Exception {
        TEST_UTIL.shutdownMiniCluster(;
        @Test
        public void test() throws IOException {
            Configuration conf=TEST_UTIL.getConfiguration();
            try (Connection conn=ConnectionFactory.createConnection(co
                    nf)){
                try (Table table=conn.getTable(tableName)){
                    for (byte[]rowkey : new byte[][]ROW_KEYO,ROW_KEY1
                }){
                    Put put=new Put(rowkey).addColumn(FAMILY,QUALIFIER,
                            VALUE);
                    table.put(put);
                }
                Scan scan=new Scan().withStartRow(ROW_KEY1).setLimit
                        (1);
                try (ResultScanner scanner=table.getScanner(scan)){
                    List<Cell> cells=new ArrayList<>();
                    for (Result result : scanner){
                        cells.addAll(result.listCells();
                        Assert.assertEquals(cells.size(),1);
                        Cell firstCell=cells.get(O);
                        Assert.assertArrayEquals(CellUtil.cloneRow(firstCel
                                l),ROW_KEY1);
                        Assert.assertArrayEquals(CellUtil.cloneFamily(firstC
                                ell),FAMILY);
                        Assert.assertArrayEquals(CellUtil.cloneQualifier(fir
                                stCel1),QUALIFIER);
                        Assert.assertArrayEquals(CellUtil.cloneValue(firstCe
                                ll),VALUE);
                    }
                }
            }
        }
    }

這個示例是一個訪問HBase的單元測試代碼。咱們在類TestDemo初始化前,經過HBase的HBaseTestingUtility工具啓動一個運行在本地的Mini HBase集羣,最後跑完全部的單元測試樣例以後,一樣經過HBaseTestingUtility工具清理相關資源,並關閉集羣。分佈式

下面重點講解TestDemo#test方法的實現。主要步驟以下。工具

步驟1:獲取集羣的Conf iguration對象。單元測試

對訪問HBase集羣的客戶端來講,通常須要3個配置文件:hbase-site.xml、core-site. xml、hdfs-site.xml。只需把這3個配置文件放到JVM能加載的classpath下便可,而後經過以下代碼便可加載到Conf iguration對象:測試

Configuration conf = HBaseConfiguraction.create();

在示例中,因爲HBaseTestingUtility擁有API能夠方便地獲取到Conf iguration對象,因此省去了加載Conf iguration對象的步驟。spa

步驟2:經過Conf iguration初始化集羣Connection。線程

Connection是HBase客戶端進行一切操做的基礎,它維持了客戶端到整個HBase集羣的鏈接,例如一個HBase集羣中有2個Master、5個RegionServer,那麼通常來講,這個Connection會維持一個到Active Master的TCP鏈接和5個到RegionServer的TCP鏈接。設計

一般,一個進程只須要爲一個獨立的集羣創建一個Connection便可,並不須要創建鏈接池。創建多個鏈接,是爲了提升客戶端的吞吐量,鏈接池是爲了減小創建和銷燬鏈接的開銷,而HBase的Connection本質上是由鏈接多個節點的TCP連接組成,客戶端的請求分發到各個不一樣的物理節點,所以吞吐量並不存在問題;另外,客戶端主要負責收發請求,而大部分請求的響應耗時都花在服務端,因此使用鏈接池也不必定能帶來更高的效益。

Connection還緩存了訪問的Meta信息,這樣後續的大部分請求均可以經過緩存的Meta信息定位到對應的RegionServer。

步驟3:經過Connection初始化Table。

Table是一個很是輕量級的對象,它實現了用戶訪問表的全部API操做,例如Put、Get、Delete、Scan等。本質上,它所使用的鏈接資源、配置信息、線程池、Meta緩存等,都來自步驟2建立的Connection對象。所以,由同一個Connection建立的多個Table,都會共享鏈接、配置信息、線程池、Meta緩存這些資源。

步驟4:經過Table執行Put和Scan操做。

從示例代碼中能夠明顯看出,HBase操做的rowkey、family、column、value等都須要先序列化成byte[],一樣讀取的每個cell也是用byte[]來表示的。

以上就是訪問HBase表數據的全過程。

定位Meta表

HBase一張表的數據是由多個Region構成,而這些Region是分佈在整個集羣的RegionServer上的。那麼客戶端在作任何數據操做時,都要先肯定數據在哪一個Region上,而後再根據Region的RegionServer信息,去對應的RegionServer上讀取數據。所以,HBase系統內部設計了一張特殊的表——hbase:meta表,專門用來存放整個集羣全部的Region信息。hbase:meta中的hbase指的是namespace,HBase允許針對不一樣的業務設計不一樣的namespace,系統表採用統一的namespace,即hbase;meta指的是hbase這個namespace下的表名。

首先,咱們來介紹一下hbase:meta表的基本結構,打開HBase Shell,咱們能夠看到hbase:meta表的結構以下:

image.png

hbase:meta表的結構很是簡單,整個表只有一個名爲info的ColumnFamily。並且HBase保證hbase:meta表始終只有一個Region,這是爲了確保meta表屢次操做的原子性,由於HBase本質上只支持Region級別的事務。(注意表結構中用到了MultiRowMutationEndpoint這個coprocessor,就是爲了實現Region級別事務)。

那麼,hbase:meta表內具體存放的是哪些信息呢?圖4-1較爲清晰地描述了hbase:meta表內存儲的信息。

image.png

整體來講,hbase:meta的一個rowkey就對應一個Region,rowkey主要由TableName(業務表名)、StartRow(業務表Region區間的起始rowkey)、Timestamp(Region建立的時間戳)、EncodedName(上面3個字段的MD5Hex值)4個字段拼接而成。每一行數據又分爲4列,分別是info:regioninfo、info:seqnumDuringOpen、info:server、info:serverstartcode。

• info:regioninfo:該列對應的Value主要存儲4個信息,即EncodedName、RegionName、Region的StartRow、Region的StopRow。
• info:seqnumDuringOpen:該列對應的Value主要存儲Region打開時的sequenceId。
• info:server:該列對應的Value主要存儲Region落在哪一個RegionServer上。
• info:serverstartcode:該列對應的Value主要存儲所在RegionServer的啓動Timestamp。

理解了hbase:meta表的基本信息後,就能夠根據rowkey來查找業務的Region了。例如,如今須要查找micloud:note表中rowkey='userid334452'所在的Region,能夠設計以下查詢語句:

image.png

爲何須要用一個9999999999999的timestamp,以及爲何要用反向查詢Reversed Scan呢?

首先,9999999999999是13位時間戳中最大值。其次由於HBase在設計hbase:meta表的rowkey時,把業務表的StartRow(而不是StopRow)放在hbase:meta表的rowkey上。這樣,若是某個Region對應的區間是[bbb, ccc),爲了定位rowkey=bc的Region,經過正向Scan只會找到[bbb, ccc)這個區間的下一個區間,可是,即便咱們找到了[bbb, ccc)的下一個區間,也無法快速找到[bbb,ccc)這個Region的信息。因此,採用Reversed Scan是比較合理的方案。

在理解了如何根據rowkey去hbase:meta表中定位業務表的Region以後,試着思考另一個問題:HBase做爲一個分佈式數據庫系統,一個大的集羣可能承擔數千萬的查詢寫入請求,而hbase:meta表只有一個Region,若是全部的流量都先請求hbase:meta表找到Region,再請求Region所在的RegionServer,那麼hbase:meta表的將承載巨大的壓力,這個Region將立刻成爲熱點Region,且根本沒法承擔數千萬的流量。那麼,如何解決這個問題呢?

事實上,解決思路很簡單:把hbase:meta表的Region信息緩存在HBase客戶端,如圖所示。
image.png
客戶端定位Region示意圖

HBase客戶端有一個叫作MetaCache的緩存,在調用HBase API時,客戶端會先去MetaCache中找到業務rowkey所在的Region,這個Region可能有如下三種狀況:

•Region信息爲空,說明MetaCache中沒有這個rowkey所在Region的任何Cache。此時直接用上述查詢語句去hbase:meta表中Reversed Scan便可,注意首次查找時,須要先讀取ZooKeeper的/hbase/meta-region-server這個ZNode,以便肯定hbase:meta表所在的RegionServer。在hbase:meta表中找到業務rowkey所在的Region以後,將(regionStartRow, region)這樣的二元組信息存放在一個MetaCache中。這種狀況極少出現,通常發生在HBase客戶端到服務端鏈接第一次創建後的少數幾個請求內,因此並不會對HBase服務端形成巨大壓力。

•Region信息不爲空,可是調用RPC請求對應RegionServer後發現Region並不在這個RegionServer上。這說明MetaCache信息過時了,一樣直接Reversed Scan hbase:meta表,找到正確的Region並緩存。一般,某些Region在兩個RegionServer之間移動後會發生這種狀況。但事實上,不管是RegionServer宕機致使Region移動,仍是因爲Balance致使Region移動,發生的概率都極小。並且,也只會對Region移動後的極少數請求產生影響,這些請求只須要經過HBase客戶端自動重試locate meta便可成功。

•Region信息不爲空,且調用RPC請求到對應RegionSsrver後,發現是正確的RegionServer。絕大部分的請求都屬於這種狀況,也是代價極小的方案。

因爲MetaCache的設計,客戶端分攤了幾乎全部定位Region的流量壓力,避免出現全部流量都打在hbase:meta的狀況,這也是HBase具有良好拓展性的基礎。

Scan的複雜之處

HBase客戶端的Scan操做應該是比較複雜的RPC操做。爲了知足客戶端多樣化的數據庫查詢需求,Scan必須能設置衆多維度的屬性。經常使用的有startRow、endRow、Filter、caching、batch、reversed、maxResultSize、version、timeRange等。

爲便於理解,咱們先來看一下客戶端Scan的核心流程。在上面的代碼示例中,咱們已經知道table.getScanner(scan)能夠拿到一個scanner,而後只要不斷地執行scanner.next()就能拿到一個Result,如圖所示。

image.png
客戶端讀取Result流程

用戶每次執行scanner.next(),都會嘗試去名爲cache的隊列中拿result(步驟4)。若是cache隊列已經爲空,則會發起一次RPC向服務端請求當前scanner的後續result數據(步驟1)。客戶端收到result列表以後(步驟2),經過scanResultCache把這些results內的多個cell進行重組,最終組成用戶須要的result放入到Cache中(步驟3)。其中,步驟1+步驟2+步驟3統稱爲loadCache操做。

爲何須要在步驟3對RPC response中的result進行重組呢?這是由於RegionServer爲了不被當前RPC請求耗盡資源,實現了多個維度的資源限制(例如timeout、單次RPC響應最大字節數等),一旦某個維度資源達到閾值,就立刻把當前拿到的cell返回給客戶端。這樣客戶端拿到的result可能就不是一行完整的數據,所以在步驟3須要對result進行重組。

理解了scanner的執行流程以後,再來理解Scan的幾個重要的概念。

• caching:每次loadCache操做最多放caching個result到cache隊列中。控制caching,也就能控制每次loadCache向服務端請求的數據量,避免出現某一次scanner.next()操做耗時極長的狀況。

• batch:用戶拿到的result中最多含有一行數據中的batch個cell。若是某一行有5個cell,Scan設的batch爲2,那麼用戶會拿到3個result,每一個result中cell個數依次爲2,2,1。

• allowPartial:用戶能容忍拿到一行部分cell的result。設置了這個屬性,將跳過上圖中的第三步重組流程,直接把服務端收到的result返回給用戶。

• maxResultSize:loadCache時單次RPC操做最多拿到maxResultSize字節的結果集。

對上面4個概念有了基本認識以後,再來分析如下具體的案例。

例1:Scan同時設置caching、allowPartial和maxResultSize的狀況。如圖所示,最左側表示服務端有4行數據,每行依次有3,1,2,3個cell。中間一欄表示每次RPC收到的result。因爲cell-1佔用字節超過了maxResultSize,因此單獨組成一個result-1,剩餘的兩個cell組成result-2。同時,因爲用戶設了allowPartial,RPC返回的result不經重組即可直接被用戶拿到。最右側表示用戶經過scanner.next()拿到的result列表。
image.png
注意,最右欄中,經過虛線框標出了每次loadCache的狀況。因爲設置caching=2,所以第二次loadCache最多隻能拿到2個result。

例2:Scan只設置caching和maxResultSize的狀況。和例1相似,都設了maxResultSize,所以RPC層拿到的result結構和例1是相同的;不一樣的地方在於,本例沒有設allowPartial,所以須要把RPC收到的result進行重組。最終重組的結果就是每一個result包含該行完整的cell,如圖所示。
image.png

例3:Scan同時設置caching、batch、maxResultSize的狀況。RPC收到的result和前兩例相似。在重組時,因爲batch=2,所以保證每一個result最多包含一行數據的2個cell,如圖所示。

image.png

文章基於《HBase原理與實踐》一書

相關文章
相關標籤/搜索