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客户端的用法和设计,代码如下所示:

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对象的步骤。

步骤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表的构造如下:

hbase:meta表的构造非常简单,整个表只有一个名为info的ColumnFamily。而且HBase保障hbase:meta表始终只有一个Region,这是为了确保meta表屡次操作的原子性,因为HBase实质上只反对Region级别的事务。(留神表构造中用到了MultiRowMutationEndpoint这个coprocessor,就是为了实现Region级别事务)。

那么,hbase:meta表内具体寄存的是哪些信息呢?图4-1较为清晰地形容了hbase:meta表内存储的信息。

总体来说,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,能够设计如下查问语句:

为什么须要用一个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客户端,如图所示。

客户端定位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,如图所示。


客户端读取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列表。

留神,最右栏中,通过虚线框标出了每次loadCache的状况。因为设置caching=2,因而第二次loadCache最多只能拿到2个result。

例2:Scan只设置caching和maxResultSize的状况。和例1相似,都设了maxResultSize,因而RPC层拿到的result构造和例1是雷同的;不同的中央在于,本例没有设allowPartial,因而须要把RPC收到的result进行重组。最终重组的后果就是每个result蕴含该行残缺的cell,如图所示。

例3:Scan同时设置caching、batch、maxResultSize的状况。RPC收到的result和前两例相似。在重组时,因为batch=2,因而保障每个result最多蕴含一行数据的2个cell,如图所示。

文章基于《HBase原理与实际》一书