4 批处理操作 (Batch Operations)
之前介绍的 API 都是应用到单一行上的操作。本节介绍另外一些 API 调用,这些调用可以批量处理跨多行的不同操作。
NOTE:
---------
事实上,许多基于列表的操作,如 delete(List<Delete> deletes) 或 get(List<Get> gets), 都是基于 batch() 方法实现的。它们都是一些为了方便
用户使用而存在的方法。如果是新手,推荐使用 batch() 方法进行操作。
下面的客户端 API 方法提供了批量处理操作。其中引入了一个新的类 Row, 它是 Get 和所有 Mutation 类型的基类, Put 和 Delete 类的父类。
void batch(final List<? extends Row> actions, final Object[] results) throws IOException, InterruptedException
void batchCallback(final List<? extends Row> actions, final Object[] results, final Batch.Callback<R> callback)
throws IOException, InterruptedException
使用相同的父类允许在列表中实现多态,
示例:
//Example application using batch operations
//Create a list to hold all values
List<Row> batch = new ArrayList<Row>();
//Add a Put instance
Put put = new Put(ROW2);
put.addColumn(COLFAM2, QUAL1, 4, Bytes.toBytes("val5"));
batch.add(put);
//Add a Get instance for a different row.
Get get1 = new Get(ROW1);
get1.addColumn(COLFAM1, QUAL1);
batch.add(get1);
//Add a Delete instance.
Delete delete = new Delete(ROW1);
delete.addColumns(COLFAM1, QUAL2);
batch.add(delete);
//Add a Get instance that will fail
Get get2 = new Get(ROW2);
get2.addFamily(Bytes.toBytes("BOGUS"));
batch.add(get2);
//Create result array.
Object[] results = new Object[batch.size()];
try {
table.batch(batch, results);
} catch (Exception e) {
//Print error that was caught
System.err.println("Error: " + e);
}
for (int i = 0; i < results.length; i++) {
//Print all results and class types
System.out.println("Result[" + i + "]: type = " +
results[i].getClass().getSimpleName() + "; " + results[i]);
}
输出:
Before batch call...
Cell: row1/colfam1:qual1/1/Put/vlen=4/seqid=0, Value: val1
Cell: row1/colfam1:qual2/2/Put/vlen=4/seqid=0, Value: val2
Cell: row1/colfam1:qual3/3/Put/vlen=4/seqid=0, Value: val3
Error: org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException:
\
Failed 1 action: \
org.apache.hadoop.hbase.regionserver.NoSuchColumnFamilyException:
\
Column family BOGUS does not exist in ...
...
: 1 time,
Result[0]: type = Result; keyvalues=NONE
Result[1]: type = Result; keyvalues={row1/colfam1:qual1/1/Put/
vlen=4/seqid=0}
Result[2]: type = Result; keyvalues=NONE
Result[3]: type = NoSuchColumnFamilyException; \
org.apache.hadoop.hbase.regionserver.NoSuchColumnFamilyException:
\
org.apache.hadoop.hbase.regionserver.NoSuchColumnFamilyException:
\
Column family BOGUS does not exist in ...
...
After batch call...
Cell: row1/colfam1:qual1/1/Put/vlen=4/seqid=0, Value: val1
Cell: row1/colfam1:qual3/3/Put/vlen=4/seqid=0, Value: val3
Cell: row2/colfam2:qual1/4/Put/vlen=4/seqid=0, Value: val5
batch() 调用返回可能的 result 值
+---------------+-----------------------------------------------------------------------------------------------------------+
| 结果 | 描述 |
+---------------+-----------------------------------------------------------------------------------------------------------+
| null | The operation has failed to communicate with the remote server. |
+---------------+-----------------------------------------------------------------------------------------------------------+
| Empty Result | Returned for successful Put and Delete operations. |
+---------------+-----------------------------------------------------------------------------------------------------------+
| Result | Returned for successful Get operations, but may also be empty when there was no matching row or column. |
+---------------+-----------------------------------------------------------------------------------------------------------+
| Throwable | In case the servers return an exception for the operation it is returned to the client as-is. You can |
| | use it to check what went wrong and maybe handle the problem automatically in your code. |
+---------------+-----------------------------------------------------------------------------------------------------------+
更进一步观察控制台上输出的返回结果数组,会发现空的 Result 实例打印出来的是:keyvalues=NONE (Result[0]). Get 请求成功找到相匹配的结果,
返回对应的 Cell 实例(Result[1]). Delete 操作也成功执行,返回空的 Result 实例 (Result[2]). 最后,BOGUS 列族操作产生异常 (Result[3])。
NOTE:
-------------------------------------------------------------------------------------------------------------------------------------
用户使用 batch() 功能时, Put 实例不会被客户端写入缓冲区缓存。 batch() 请求是同步的,会把操作直接发送到服务器端,这个过程没有什么延迟
或其它中间操作。这与 put() 调用明显不同,所以谨慎挑选需要的方法。
有两种不同的批处理调用看起来类似。上面的示例使用了第一种方式。第二种方式是允许提供一个回调实例,当在服务器上执行异步或并行调用时,由客户端
类库接收响应时调用。需要实现 Batch.Callback 接口,提供了 update() 方法。
以下示例是在上一示例基础上修改的,只是添加了回调实例:
//Example application using batch operations with callbacks
// Create a list to hold all values
List<Row> batch = new ArrayList<Row>();
Put put = new Put(ROW2);
put.addColumn(COLFAM2, QUAL1, 4, Bytes.toBytes("val5"));
batch.add(put);
Get get1 = new Get(ROW1);
get1.addColumn(COLFAM1, QUAL1);
batch.add(get1);
Delete delete = new Delete(ROW1);
delete.addColumns(COLFAM1, QUAL2);
batch.add(delete);
Get get2 = new Get(ROW2);
get2.addFamily(Bytes.toBytes("BOGUS"));
batch.add(get2);
try {
table.batchCallback(batch, results, new Batch.Callback<Result>()
{
@Override
public void update(byte[] region, byte[] row, Result result) {
System.out.println("Received callback for row[" +
Bytes.toString(row) + "] -> " + result);
}
});
} catch (Exception e) {
System.err.println("Error: " + e);
}
for (int i = 0; i < results.length; i++) {
System.out.println("Result[" + i + "]: type = " +
results[i].getClass().getSimpleName() + "; " + results[i]);
}
会看到与上个示例相同的输出结果,但还有些额外的信息是回调实现发出的,类似如下:
Before delete call...
Cell: row1/colfam1:qual1/1/Put/vlen=4/seqid=0, Value: val1
Cell: row1/colfam1:qual2/2/Put/vlen=4/seqid=0, Value: val2
Cell: row1/colfam1:qual3/3/Put/vlen=4/seqid=0, Value: val3
Received callback for row[row2] ->
keyvalues=NONE
Received callback for row[row1] ->
keyvalues={row1/colfam1:qual1/1/Put/vlen=4/seqid=0}
Received callback for row[row1] ->
keyvalues=NONE
Error: org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException:
Failed 1 action:
...
: 1 time,
Result[0]: type = Result; keyvalues=NONE
Result[1]: type = Result; keyvalues={row1/colfam1:qual1/1/Put/
vlen=4/seqid=0}
Result[2]: type = Result; keyvalues=NONE
Result[3]: type = NoSuchColumnFamilyException;
org.apache.hadoop.hbase.regionserver.NoSuchColumnFamilyException:
...
After batch call...
Cell: row1/colfam1:qual1/1/Put/vlen=4/seqid=0, Value: val1
Cell: row1/colfam1:qual3/3/Put/vlen=4/seqid=0, Value: val3
Cell: row2/colfam2:qual1/4/Put/vlen=4/seqid=0, Value: val5
5 扫描 (Scans)
-----------------------------------------------------------------------------------------------------------------------------------------
在讨论过基本的 CRUD 类型的操作之后,现在来看一下扫描技术(scan), 这种技术类似于数据库系统中的游标(cursor), 并利用到了 HBase 提供的底层顺序
存储的数据结构。
5.1 介绍 (Introduction)
-----------------------------------------------------------------------------------------------------------------------------------------
扫描操作的使用跟 get() 方法非常类似。类似于其它功能,也有一个与之相关的支持类,Scan. 但由于扫描类似于迭代,因此没有响应的 scan() 调用,而
是通过一个 getScanner() 方法返回实际的扫描器实例用于迭代,可用的方法如下:
ResultScanner getScanner(Scan scan) throws IOException
ResultScanner getScanner(byte[] family) throws IOException
ResultScanner getScanner(byte[] family, byte[] qualifier) throws IOException
后面两个方法是便利方法,隐式地帮用户创建 Scan 实例,随后会调用 getScanner(Scan scan) 方法。
Scan 类有如下构造器:
Scan()
Scan(byte[] startRow, Filter filter)
Scan(byte[] startRow)
Scan(byte[] startRow, byte[] stopRow)
Scan(Scan scan) throws IOException
Scan(Get get)
这与 Get 类的不同之处是很明显的:不是指定单个行的行键,可选地提供一个 startRow 参数 —— 定义从 HBase table 上扫描开始的行键。可选的 stopRow
参数用于限定扫描结束的行键。
NOTE:
-------------------------------------------------------------------------------------------------------------------------------------
起始行总是包含的,而结束行是不包含的,表示为 [startRow, stopRow) 区间标记。
扫描操作的一个特性是,不必精确匹配起始和结束行。而是,扫描匹配的第一行等于或大于给定的起始行(startRow). 如果没有给定起始行,扫描从表的开始
扫描。在当前的行键等于或大于可选的停止行时,扫描结束工作。如果没有指定停止行,扫描会运行到表的结尾。
还有另外一个可选参数 filter, 一个 Filter 实例的引用。通常,Scan 实例简单使用空参数构造器创建,所有的可选参数都有相应的 getter 和 setter
方法可以使用。
类似于其它数据相关的类型,有一个从现有 Scan 实例拷贝所有参数的构造器。也有一个从现有的 Get 实例中拷贝参数的构造器。get 和 scan 在服务器端
的功能实际上是相同的。唯一不同的是,Get 的扫描必须在扫描中包含停止行,起始行和停止行是同一个值。
另外,当使用基于 Get 实例的构造器时,Scan 的下面方法返回 true:
boolean isGetScan()
一旦创建了 Scan 实例,就可以向其添加详细的限制信息。但也允许使用空的 scan, 这样会读取整个 table, 包括所有的列族以及它们的列。可以使用各种方法
限定读取数据:
Scan addFamily(byte [] family)
Scan addColumn(byte[] family, byte[] qualifier)
有很多类似于 Get 类的方法可用:可以通过 addFamily() 设定列族以限定扫描返回的数据,或者,通过 addColumn() 更严格地限定仅包含特定的列。
NOTE:
-------------------------------------------------------------------------------------------------------------------------------------
如果用户只需要数据的子集,那么限制扫描的范围能发挥 HBase 的优势。因为 HBase 中的数据是按列存储的,如果扫描不读取某个列族,那么整个列族
文件就不会读取,这就是列式存储架构的优势。
Scan 还有其它用于选择性的方法,第一组是围绕返回的 cell 版本的方法:
Scan setTimeStamp(long timestamp) throws IOException
Scan setTimeRange(long minStamp, long maxStamp) throws IOException
TimeRange getTimeRange()
Scan setMaxVersions()
Scan setMaxVersions(int maxVersions)
int getMaxVersions()
setTimeStamp() 方法时设置时间区间的快捷方法:setTimeRange(time, time + 1), 这两个方法会选择匹配设定区间的 cell. 很明显,前者更特定,选择
确切匹配的一个时间戳。getTimeRange() 返回上两个方法设置的值。每个列多少个 cell —— 换句话说,扫描返回多少个版本由 setMaxVersions() 方法控制,
一个设定给定的数量,另一个设置所有版本。
下一组方法与包含到扫描中的行相关:
Scan setStartRow(byte[] startRow)
byte[] getStartRow()
Scan setStopRow(byte[] stopRow)
byte[] getStopRow()
Scan setRowPrefixFilter(byte[] rowPrefix)
使用 setStartRow() 和 setStopRow() 可以定义与构造器相同的参数,所有的方法都是为了更进一步限定返回的数据。对应的 getter 返回当前的设置(可能
为 null, 因为这两个参数都是可选的). setRowPrefixFilter() 是设置起始行为 rowPrefix 的值,停止行为大于当前 key 的下一个 key,增大二进制 key
的计算逻辑是这样,例如,假设行键为 { 0x12, 0x23, 0xFF, 0xFF }, 增加它的结果为 { 0x12, 0x24 }, 因为最后两个字节已经到达了它们的最大值。
下一组方法时围绕过滤器的:
Filter getFilter()
Scan setFilter(Filter filter)
boolean hasFilter()
过滤器是联合时间区间,行选择器的。可以更进一步加入列族和列名的选择支持。setFilter() 可以分配一个或多个过滤器给扫描,getFilter() 返回当前的
过滤器。
Scan 还提供了一些更特殊的方法,用于处理特定场景。可以把它们当做为高级用户提供的方法,但它们实际上用起来很直接:
Scan setReversed(boolean reversed)
boolean isReversed()
Scan setRaw(boolean raw)
boolean isRaw()
Scan setSmall(boolean small)
boolean isSmall()
第一对方法使应用不使用 forward-only 迭代,而是使用相反的迭代。传统上 HBase 只提供向前的扫描,但最近的版本引入了反向迭代选项。由于数据是按
升序存储的,反向扫描涉及更复杂的处理。换句话说,反向扫描会比正向扫描慢一些。但会减轻在构建应用程序级别两个方向上的查找索引 (but alleviate
the previous necessity of building application-level lookup indexes for both directions)。
反向扫描的一个精妙之处在于反向是按每行的,而不是在一行之内。仍然会像正向扫描那样接收到每一行。
第二对方法 setRaw() method 切换扫描器为一个特使的模式,返回它找到的每一个 cell. 这包括被删除的 cell 但还没有从物理上移除,也包括那些带有
delete 标记的 cell.
最后一对方法用于处理 "小" 扫描。这类扫描只需要读取非常小的数据集,可以在一个 RPC 调用中返回。在一个 scan 实例上调用 setSmall(true) 指示客
户端 API 不要以通常的方式联合使用打开扫描器、获取数据、关闭扫描器的远程调用,而是在一个调用中完成所有操作。这种模式下,服务器端也有对读取
的优化,因此扫描尽可能地快。
Scan 类还提供了其它的方法调用,详细信息参考最新版本的 API javadoc.
5.2 ResultScanner 接口 (The ResultScanner Interface)
-----------------------------------------------------------------------------------------------------------------------------------------
扫描操作不会通过一次 RPC 请求返回所有匹配的行,而是以行为单位(per-row basis)返回。很明显,行的数量非常大,可能有上千条甚至更多,因此一次
调用会占用大量的系统资源并消耗很长的时间。
ResultScanner 把操作转换为类似 get 的操作,将每一行数据封装成一个 Result 实例,并将所有的 Result 实例放入一个迭代器中。ResultScanner 有如下
方法:
Result next() throws IOException
Result[] next(int nbRows) throws IOException
void close()
两个 next() 调用供用户选择,调用 close() 方法释放所有扫描控制的资源。
● 扫描器租约 (Scanner Leases)
-------------------------------------------------------------------------------------------------------------------------------------
要确保尽早释放扫描器实例。一个打开的扫描器会占用不少服务器端资源,积累多了会占用大量的堆空间。当使用完 ResultScanner 之后应调用它的
close() 方法,并考虑将它放到 try/finally 块中,或 try-with-resources 结构中,这样,即便在迭代过程中发生异常或者错误,也会保证 close()
方法被调用。
就像行锁一样,扫描器也使用同样的租约超时机制,保护其不被失效的客户端阻塞太久。设置如下配置属性来修改超时阈值(单位为毫秒):
<property>
<name>hbase.client.scanner.timeout.period</name>
<value>120000</value>
</property>
要确保这个属性的值对锁和扫描器租约同样有效。
next() 调用返回一个单独的 Result 实例,这个实例代表了下一个可用的行。此外,可以使用 next(int nbRows) 一次获取多行数据,它返回一个数组,数组
中包含的 Result 实例最多可达 nbRows 个,每个实例代表唯一的一行。如果没有足够多的行,返回的数组也可能比较短,也可能数组为空。很明显,之前的
扫描已到达表尾部或停止行,没有足够的行来填充数据。
注意, 如果已到达表末尾 next() 调用会返回 null,但 next(int nbRows) 总会返回一个有效的数组,同样的原因,数组为空元素,但数组是有效的。
示例:
// Example using a scanner to access data in a table
// Create empty Scan instance.
Scan scan1 = new Scan();
// Get a scanner to iterate over the rows.
ResultScanner scanner1 = table.getScanner(scan1);
for (Result res : scanner1) {
// Print row content
System.out.println(res);
}
// Close scanner to free remote resources.
scanner1.close();
Scan scan2 = new Scan();
//Add one column family only, this will suppress the retrieval of “colfam2”.
scan2.addFamily(Bytes.toBytes("colfam1"));
ResultScanner scanner2 = table.getScanner(scan2);
for (Result res : scanner2) {
System.out.println(res);
}
scanner2.close();
Scan scan3 = new Scan();
//Use fluent pattern to add specific details to the Scan.
scan3.addColumn(Bytes.toBytes("colfam1"), Bytes.toBytes("col-5")).
addColumn(Bytes.toBytes("colfam2"), Bytes.toBytes("col-33")).
setStartRow(Bytes.toBytes("row-10")).
setStopRow(Bytes.toBytes("row-20"));
ResultScanner scanner3 = table.getScanner(scan3);
for (Result res : scanner3) {
System.out.println(res);
}
scanner3.close();
Scan scan4 = new Scan();
// Only select one column.
scan4.addColumn(Bytes.toBytes("colfam1"),Bytes.toBytes("col-5")).
setStartRow(Bytes.toBytes("row-10")).
setStopRow(Bytes.toBytes("row-20"));
ResultScanner scanner4 = table.getScanner(scan4);
for (Result res : scanner4) {
System.out.println(res);
}
scanner4.close();
Scan scan5 = new Scan();
// One column scan that runs in reverse
scan5.addColumn(Bytes.toBytes("colfam1"), Bytes.toBytes("col-5")).
setStartRow(Bytes.toBytes("row-20")).
setStopRow(Bytes.toBytes("row-10")).
setReversed(true);
ResultScanner scanner5 = table.getScanner(scan5);
for (Result res : scanner5) {
System.out.println(res);
}
scanner5.close();
代码插入了 100 行数据,每行有两个列族,每个列族下包含 100 个列。第一个扫描操作扫描全表内容,第二个扫描操作只扫描一个列族,最后一个扫描操作
有严格的限制条件,其中包括对行范围的限制,同时还要求只扫描两个特定的列。输出如下:
...
Scanning table #4...
keyvalues={row-10/colfam1:col-5/1427010030763/Put/vlen=8/seqid=0}
keyvalues={row-100/colfam1:col-5/1427010039565/Put/vlen=9/seqid=0}
...
keyvalues={row-19/colfam1:col-5/1427010031928/Put/vlen=8/seqid=0}
keyvalues={row-2/colfam1:col-5/1427010029560/Put/vlen=7/seqid=0}
Scanning table #5...
keyvalues={row-20/colfam1:col-5/1427010032053/Put/vlen=8/seqid=0}
keyvalues={row-2/colfam1:col-5/1427010029560/Put/vlen=7/seqid=0}
...
keyvalues={row-11/colfam1:col-5/1427010030906/Put/vlen=8/seqid=0}
keyvalues={row-100/colfam1:col-5/1427010039565/Put/vlen=9/seqid=0}
再次强调,匹配的行键是按词典顺序排序的。可以简单地用 0 把行键补齐,这样扫描出来的结果更具可读性。另外要注意的是停止行在扫描结果中是排除的,
意思就是说如果想要 20 到 10 (反向扫描) 之间的所有行,要设定起始行为 row-20, 将 row-10 作为结束行。
5.3 Scanner 缓存 (Scanner Caching)
-----------------------------------------------------------------------------------------------------------------------------------------
如果没有正确配置,那么每次 next() 调用会为每一行产生一次 RPC 调用,即便使用 next(int nbRows) 进行调用也是如此,因为它只不过是在客户端循环
调用 next() 而已。很明显,这在处理小型 cell 时不会获得很好的性能。如果一次 RPC 调用能抓取更多的行会更有意义。这称为扫描缓存(scanner caching)
并且默认情况下是启用的。
有一个集群范围的配置属性 hbase.client.scanner.caching 控制着所有扫描的默认缓存。该属性默认为 100, 从而指示所有的扫描器在每一次 RPC 调用过程
中同时抓取 100 行。可以在 Scan 实例级别改写这个属性:
void setCaching(int caching)
int getCaching()
设定 scan.setCaching(200) 会提升每次远程调用的载荷为 200 行。两种 next() 方法都会受到这个设置的影响。getCaching() 方法返回当前该属性的值。
NOTE:
-------------------------------------------------------------------------------------------------------------------------------------
可以修改整体 HBase 安装的默认 100 的设置,将下面的配置属性添加到 hbase-site.xml 配置文件中:
<property>
<name>hbase.client.scanner.caching</name>
<value>200</value>
</property>
这会设置所有的扫描器缓存为 200 行。
需要在少量的 RPC 调用和客户端及服务器的内存使用方面找到一个平衡点。设置较高的扫描器缓存多数时候会提升扫描性能,但设置太高也会产生相反的效果:
每次 next() 调用会占用更多的时间,因为需要抓取更多的数据,并传输给客户端,并且一旦超出了客户端进程可用的最大堆内存数量,会导致
OutOfMemoryException 而终止。
NOTE:
-------------------------------------------------------------------------------------------------------------------------------------
当传输和处理数据的时间超过配置的扫描器租约超时时间时,客户端会收到 ScannerTimeoutException 异常的租约过期错误(lease expired error) 而
结束。
示例:
//Example timeout while using a scanner
Scan scan = new Scan();
ResultScanner scanner = table.getScanner(scan);
//Get currently configured lease timeout
int scannerTimeout = (int) conf.getLong(HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD, -1);
try {
//Sleep a little longer than the lease allows.
Thread.sleep(scannerTimeout + 5000);
} catch (InterruptedException e) {
// ignore
}
while (true){
try {
Result result = scanner.next();
if (result == null) break;
//Print row content.
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
break;
}
}
scanner.close();
输出如下:
Adding rows to table...
Current (local) lease period: 60000ms
Sleeping now for 65000ms...
Attempting to iterate over scanner...
org.apache.hadoop.hbase.client.ScannerTimeoutException: \
65017ms passed since the last invocation, timeout is currently set to 60000
at org.apache.hadoop.hbase.client.ClientScanner.next(ClientScanner.
java)
at client.ScanTimeoutExample.main(ScanTimeoutExample.java:53)
...
Caused by: org.apache.hadoop.hbase.UnknownScannerException: \
org.apache.hadoop.hbase.UnknownScannerException: Name: 3915, already
closed?
at org.apache.hadoop.hbase.regionserver.RSRpcServices.scan(...)
...
Caused by: org.apache.hadoop.hbase.ipc.RemoteWithExtrasException( \
org.apache.hadoop.hbase.UnknownScannerException): \
org.apache.hadoop.hbase.UnknownScannerException: Name: 3915, already
closed?
at org.apache.hadoop.hbase.regionserver.RSRpcServices.scan(...)
...
Mar 22, 2015 9:55:22 AM org.apache.hadoop.hbase.client.ScannerCallable
close
WARNING: Ignore, probably already closed
org.apache.hadoop.hbase.UnknownScannerException: \
org.apache.hadoop.hbase.UnknownScannerException: Name: 3915, already
closed?
at org.apache.hadoop.hbase.regionserver.RSRpcServices.scan(...)
...
5.3 Scanner 批处理操作 (Scanner Batching)
-----------------------------------------------------------------------------------------------------------------------------------------
到目前为止,已经介绍了如何使用客户端扫描器缓存来从远程 region 服务器向客户端整批传输数据。不过有件事需要注意:对于数据量非常大的行,这些行
有可能超出了客户端进程的内存容量。HBase 及其客户端 API 对此提供了解决办法:批处理(batching). 可以利用如下方法控制批处理:
void setBatch(int batch)
int getBatch()
与缓存机制相对,操作作用于行级别,批处理作用于 cell 级别。它控制 ResultScanner 实例每次调用 next() 方法获取的 cell 数量。例如,setBatch(5)
设置扫描会使每个 Result 实例返回 5 个 cell.
NOTE:
-------------------------------------------------------------------------------------------------------------------------------------
如果一行包括的列数超过了批量设置中设置的值,则会将该行分片返回,扫描器每次 next() 调用返回的 Result 为行的一片。
如果行中所有的列数不被设置的批处理数量整除,最后一次返回的 Result 实例可能会包含比较少的列, 例如,如果一行有 17 列,用户把 batch 值设
为 5,则一共会返回 4 个 Result 实例,这 4 个实例中包括的列数量应当分别为 5、5、5 和 2。
组合使用扫描器缓存和批量大小,可以方便地控制扫描一个范围内的行键时所需要的 RPC 调用次数。下面的示例使用这两个参数来调节每次 Result 实例的
大小。
示例:
// Example using caching and batch parameters for scans
private static void scan(int caching, int batch, boolean small)
throws IOException {
int count = 0;
// Set caching and batch parameters.
Scan scan = new Scan()
.setCaching(caching)
.setBatch(batch)
.setSmall(small)
.setScanMetricsEnabled(true);
ResultScanner scanner = table.getScanner(scan);
//Count the number of Results available.
for (Result result : scanner) {
count++;
}
scanner.close();
ScanMetrics metrics = scan.getScanMetrics();
System.out.println("Caching: " + caching + ", Batch: " + batch +
", Small: " + small + ", Results: " + count +
", RPCs: " + metrics.countOfRPCcalls);
}
public static void main(String[] args) throws IOException {
...
// Test various combinations.
scan(1, 1, false);
scan(1, 0, false);
scan(1, 0, true);
scan(200, 1, false);
scan(200, 0, false);
scan(200, 0, true);
scan(2000, 100, false);
scan(2, 100, false);
scan(2, 10, false);
scan(5, 100, false);
scan(5, 20, false);
scan(10, 10, false);
...
}
代码打印出 caching 和 batching 的值,服务器返回的 result 数量,以及需要多少个 RPC 调用获取它们。例如下面的输出:
Caching: 1, Batch: 1, Small: false, Results: 200, RPCs: 203
Caching: 1, Batch: 0, Small: false, Results: 10, RPCs: 13
Caching: 1, Batch: 0, Small: true, Results: 10, RPCs: 0
Caching: 200, Batch: 1, Small: false, Results: 200, RPCs: 4
Caching: 200, Batch: 0, Small: false, Results: 10, RPCs: 3
Caching: 200, Batch: 0, Small: true, Results: 10, RPCs: 0
Caching: 2000, Batch: 100, Small: false, Results: 10, RPCs: 3
Caching: 2, Batch: 100, Small: false, Results: 10, RPCs: 8
Caching: 2, Batch: 10, Small: false, Results: 20, RPCs: 13
Caching: 5, Batch: 100, Small: false, Results: 10, RPCs: 5
Caching: 5, Batch: 20, Small: false, Results: 10, RPCs: 5
Caching: 10, Batch: 10, Small: false, Results: 20, RPCs: 5
可以修改调整这两个参数来查看它们对输出结果的影响。
5.4 行分片 (Slicing Rows)
-----------------------------------------------------------------------------------------------------------------------------------------
使用如下方法将表数据分片(slicing of table data):
int getMaxResultsPerColumnFamily()
Scan setMaxResultsPerColumnFamily(int limit)
int getRowOffsetPerColumnFamily()
Scan setRowOffsetPerColumnFamily(int offset)
long getMaxResultSize()
Scan setMaxResultSize(long maxResultSize)
前面四个方法一起工作,允许应用将每个选出的行切片,使用 offset 从某个特定的列开始,限制每个列族最大的结果(a max results per column family),
一旦达到就停止返回数据。后面一对方法允许设置和获取扫描返回数据大小的限制上限。它保持一个扫描选取 cell 的计数器,一旦超出了这个限制,就停止
返回数据。
示例:
// Example using offset and limit parameters for scans
private static void scan(int num, int caching, int batch, int offset,
int maxResults, int maxResultSize, boolean dump) throws IOException
{
int count = 0;
Scan scan = new Scan()
.setCaching(caching)
.setBatch(batch)
.setRowOffsetPerColumnFamily(offset)
.setMaxResultsPerColumnFamily(maxResults)
.setMaxResultSize(maxResultSize)
.setScanMetricsEnabled(true);
ResultScanner scanner = table.getScanner(scan);
System.out.println("Scan #" + num + " running...");
for (Result result : scanner) {
count++;
if (dump) System.out.println("Result [" + count + "]:" + result);
}
scanner.close();
ScanMetrics metrics = scan.getScanMetrics();
System.out.println("Caching: " + caching + ", Batch: " + batch +
", Offset: " + offset + ", maxResults: " + maxResults +
", maxSize: " + maxResultSize + ", Results: " + count +
", RPCs: " + metrics.countOfRPCcalls);
}
public static void main(String[] args) throws IOException {
scan(1, 11, 0, 0, 2, -1, true);
scan(2, 11, 0, 4, 2, -1, true);
scan(3, 5, 0, 0, 2, -1, false);
scan(4, 11, 2, 0, 5, -1, true);
scan(5, 11, -1, -1, -1, 1, false);
scan(6, 11, -1, -1, -1, 10000, false);
...
}
示例代码没有给出创建表的过程,表由两个列族组成,10 行数据,每个列族有 10 个列。代码输出类似如下:
Scan #1 running...
Result [1]:keyvalues={row-01/colfam1:col-01/1/Put/vlen=9/seqid=0,
row-01/colfam1:col-02/2/Put/vlen=9/seqid=0,
row-01/colfam2:col-01/1/Put/vlen=9/seqid=0,
row-01/colfam2:col-02/2/Put/vlen=9/seqid=0}
...
Result [10]:keyvalues={row-10/colfam1:col-01/1/Put/vlen=9/seqid=0,
row-10/colfam1:col-02/2/Put/vlen=9/seqid=0,
row-10/colfam2:col-01/1/Put/vlen=9/seqid=0,
row-10/colfam2:col-02/2/Put/vlen=9/seqid=0}
Caching: 11, Batch: 0, Offset: 0, maxResults: 2, maxSize: -1,
Results: 10, RPCs: 3
Scan #2 running...
Result [1]:keyvalues={row-01/colfam1:col-05/5/Put/vlen=9/seqid=0,
row-01/colfam1:col-06/6/Put/vlen=9/seqid=0,
row-01/colfam2:col-05/5/Put/vlen=9/seqid=0,
row-01/colfam2:col-06/6/Put/vlen=9/seqid=0}
...
Result [10]:keyvalues={row-10/colfam1:col-05/5/Put/vlen=9/seqid=0,
row-10/colfam1:col-06/6/Put/vlen=9/seqid=0,
row-10/colfam2:col-05/5/Put/vlen=9/seqid=0,
row-10/colfam2:col-06/6/Put/vlen=9/seqid=0}
Caching: 11, Batch: 0, Offset: 4, maxResults: 2, maxSize: -1,
Results: 10, RPCs: 3
Scan #3 running...
Caching: 5, Batch: 0, Offset: 0, maxResults: 2, maxSize: -1,
Results: 10, RPCs: 5
Scan #4 running...
Result [1]:keyvalues={row-01/colfam1:col-01/1/Put/vlen=9/seqid=0,
row-01/colfam1:col-02/2/Put/vlen=9/seqid=0}
Result [2]:keyvalues={row-01/colfam1:col-03/3/Put/vlen=9/seqid=0,
row-01/colfam1:col-04/4/Put/vlen=9/seqid=0}
...
Result [31]:keyvalues={row-10/colfam1:col-03/3/Put/vlen=9/seqid=0,
row-10/colfam1:col-04/4/Put/vlen=9/seqid=0}
Caching: 11, Batch: 2, Offset: 0, maxResults: 5, maxSize: -1,
Results: 32, RPCs: 5
Scan #5 running...
Caching: 11, Batch: -1, Offset: -1, maxResults: -1, maxSize: 1,
Results: 10, RPCs: 13
Scan #6 running...
Caching: 11, Batch: -1, Offset: -1, maxResults: -1, maxSize: 10000,
Results: 10, RPCs: 5
第一次扫描开始于 offset 0 并请求最多 2 cells, 返回的列为 one and two.
第二次扫描与第一次相同,但设置 offset to 4, 因此获取列 five to six. 注意,offset 实际上定义的是从列起始忽略的 cell 的数量,值为 4 造成
最开始的 4 个列被忽略。
第三次扫描,没有输出任何信息,因为只对 metrics 感兴趣。与 #1 扫描相同,但是用了 caching 值 5. RPC 的最小值应该为 3 (对于小型扫描,执行
打开,抓取,和关闭调用),但我们看到的是 5 RPCs, 因为不能在一次调用中抓取表中的 10 行结果,需要两次调用,每次调用抓取 5 个结果,再加上一
次额外的调用确定没有剩下更多的行要抓取,因此为 5 RPCs.
#4 扫描,使用 batching 值为 2 联合之前的扫描,因此每次调用 next() 返回 2 个 cell, 但同时限制了每个列族返回的 cell 数量为 5。另外,联合
使用 caching 值为 11,因此看到 5 个到服务器的 RPC 调用。
最后 #5 and #6 扫描使用 setMaxResultSize() 限制返回给调用者的数据数量。扫描器的 caching 是设置行的数量,而 max result size 设置的是字节数。
5.5 按需载入列族 (Load Column Families on Demand)
-----------------------------------------------------------------------------------------------------------------------------------------
扫描还有一个高级特性,loading column families on demand, 由如下方法控制:
Scan setLoadColumnFamiliesOnDemand(boolean value)
Boolean getLoadColumnFamiliesOnDemandValue()
boolean doLoadColumnFamiliesOnDemand()
这个功能是读优化的,只对有一个以上列族的表有用,特别是列族中的数据之间有依赖关系的场景有利。例如,假设一个列族持有元数据,另一个列族持有很
大量的载荷数据。要扫描元数据列,如果某个列中含有一个标志,需要访问在另外列族中的载荷数据。如果把这两个列族都包含到扫描中代价是很大的。因为
这样的扫描每一行都要载入载荷数据。
通过 setLoadColumnFamiliesOnDemand(true) 启用这个特性,只需一半的准备工作:也需要一个过滤器实现下面方法,返回一个 boolean 标志:
boolean isFamilyEssential(byte[] name) throws IOException
过滤器的思想是决定某个列族是否为 essential. 当服务器扫描数据时,它们首先为列族设置内部的扫描器。如果 load column families on demand 已启用,
并且设置了过滤器,它会调用过滤器并请求决定一个包含的列族是否扫描。在列族加入之前,过滤器的 isFamilyEssential() 通过列族名称调用,并且如果
加入进来必须返回 true, 如果返回 false, 那么该列族当前被忽略,并且在之后需要时载入。
另一方面,必须添加所有的列族到扫描中,不管它们是否是基本的(essential)。它们只有先加入到扫描器中,框架才会调用过滤器来决定某个列族。
5.6 Scanner 度量 (Scanner Metrics)
-----------------------------------------------------------------------------------------------------------------------------------------
Scan 类的另一特性是使客户端可以探究操作的执行效率,通过如下方法实现:
Scan setScanMetricsEnabled(final boolean enabled)
boolean isScanMetricsEnabled()
ScanMetrics getScanMetrics()
可以通过调用 setScanMetricsEnabled(true) 来启用 scan metrics 集合。一旦扫描完成,可以通过 getScanMetrics() 方法获取 ScanMetrics。
isScanMetricsEnabled() 方法用于检查之前 metrics 集合是否启用。获取的 ScanMetrics 实例有一组字段可以读取以确定操作的开销:
Metrics provided by the ScanMetrics class
+-------------------------------+-------------------------------------------------------------------
| Metric Field | Description
+-------------------------------+--------------------------------------------------------------------
| countOfRPCcalls | The total amount of RPC calls incurred by the scan.
+-------------------------------+--------------------------------------------------------------------
| countOfRemoteRPCcalls | The amount of RPC calls to a remote host
+-------------------------------+--------------------------------------------------------------------
| sumOfMillisSecBetweenNexts | The sum of milliseconds between sequential next() calls.
+-------------------------------+--------------------------------------------------------------------
| countOfNSRE | Number of NotServingRegionException caught.
+-------------------------------+--------------------------------------------------------------------
| countOfBytesInResults | Number of bytes in Result instances returned by the servers.
+-------------------------------+--------------------------------------------------------------------
| countOfBytesInRemoteResults | Same as above, but for bytes transferred from remote servers.
+-------------------------------+--------------------------------------------------------------------
| countOfRegions | Number of regions that were involved in the scan.
+-------------------------------+--------------------------------------------------------------------
| countOfRPCRetries | Number of RPC retries incurred during the scan.
+-------------------------------+--------------------------------------------------------------------
| countOfRemoteRPCRetries | Same again, but RPC retries for non-local servers.
+-------------------------------+--------------------------------------------------------------------
6 其它特性 (Miscellaneous Features)
-----------------------------------------------------------------------------------------------------------------------------------------
6.1 Table 实用方法 (The Table Utility Methods)
-----------------------------------------------------------------------------------------------------------------------------------------
客户端 API 表现为 Table 类的实例,并且给出访问 HBase 表的方法。除了已经讨论过的主要特性之外,这个类还有一些值得注意的方法:
● void close()
-------------------------------------------------------------------------------------------------------------------------------------
这个方法之前提到过,完成一个表的工作之后,要调用一次 close() 方法。在其内部有一些清理工作需要运行,调用该方法会触发这个过程。把打开和
关闭某个表的工作放到 try/catch, 或者最好放到一个 try-with-resources(on Java 7 or later) 块中运行。
● TableName getName()
-------------------------------------------------------------------------------------------------------------------------------------
这个是获取表名称的便捷方法。作为 TableName 类的实例返回,提供对名称空间和实际的表名的访问。
● Configuration getConfiguration()
-------------------------------------------------------------------------------------------------------------------------------------
这个方法允许用户访问 Table 实例使用的配置。因为得到的是 Configuration 实例的引用,所以用户修改的参数立刻生效。
● HTableDescriptor getTableDescriptor()
-------------------------------------------------------------------------------------------------------------------------------------
每个表的定义由 HTableDescriptor 类实例描述,可以通过 getTableDescriptor() 访问底层定义。
6.2 Bytes 类 (The Bytes Class)
-----------------------------------------------------------------------------------------------------------------------------------------
这个类用于将 Java 原生基本类型转换为 HBase 原生支持的原始字节数组格式,例如 String, long
大部分方法有 3 种形式,如:
static long toLong(byte[] bytes)
static long toLong(byte[] bytes, int offset)
static long toLong(byte[] bytes, int offset, int length)
可以只输入一个字节数组,或者一个数组和一个偏移量,或者一个数组,偏移量,以及一个长度值。具体用法取决于最初这个字节数组是如何生成的。如果是
之前由 toBytes() 方法创建的,可以安全地使用第一种形式,简单地将数组输入方法中就可以了。整个数组内容都转换为值。
Bytes 类支持以下原生 Java 类型和字节数组之间的转换:String, boolean, short, int, long, double, float, ByteBuffer, and BigDecimal.
详细信息参考最新版本的 JavaDoc-based API
(本篇完)
系列目录:
参考:
《HBase - The Definitive Guide - 2nd Edition》Early release —— 2015.7 Lars George