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 原理与实际》一书