HBase CRUD 操作指南 (三)

继  HBase CRUD 操作指南 (二)


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 CRUD 操作指南 (一)

HBase CRUD 操作指南 (二)

HBase CRUD 操作指南 (三)

猜你喜欢

转载自blog.csdn.net/devalone/article/details/80980728